Understanding Gradle Tasks

Dec 10 2022
Learn how to create, use, and see the outcomes of tasks, and more
You may rarely deal with Gradle Tasks directly, and most of the time, new Tasks are added either as a part of a plugin or as a copy-pasted piece of code from libraries’ “How to start” guide. Having no understanding of how Tasks work, their structure, and their lifecycle makes developers more likely to stay away from adding any changes to those tasks, even if there is room for improvement.
image by author

คุณอาจไม่ค่อยจัดการกับ Gradle Tasks โดยตรง และส่วนใหญ่แล้วTasks ใหม่จะถูกเพิ่มเข้ามาเป็นส่วนหนึ่งของปลั๊กอินหรือเป็นโค้ดที่คัดลอกและวางจากคู่มือ "วิธีเริ่มต้น" ของไลบรารี่ การไม่มีความเข้าใจในวิธีการทำงานของ Tasks โครงสร้าง และวงจรชีวิตของงาน ทำให้นักพัฒนามีแนวโน้มที่จะไม่เพิ่มการเปลี่ยนแปลงใดๆ ให้กับงานเหล่านั้น แม้ว่าจะมีช่องว่างให้ปรับปรุงก็ตาม

ในบทความนี้ เราจะมาทำความเข้าใจ ว่า Gradle Tasks คืออะไร วิธีการสร้างและใช้งาน และสามารถใช้รูปแบบใดได้บ้าง

สิ่งพื้นฐานที่คุณควรรู้เกี่ยวกับTask:

  • A Taskเป็นรหัสปฏิบัติการที่มีลำดับของการกระทำ
  • การดำเนินการจะถูกเพิ่มTaskผ่านทางdoFirst{}และ การ doLast{}ปิด
  • สามารถเข้าถึงรายการงานที่มีอยู่ได้โดยดำเนิน./gradlew tasksการ

เพิ่มเติมในบทความ เนื่องจากคุณกำลังทดสอบโค้ดในโปรเจ็กต์ Android หรือโปรเจกต์อื่นๆ ที่มี Gradle wrapper อยู่ในนั้น การเรียกใช้งาน X หมายถึงการทำงาน./gradlew Xบน Mac หรือgradlew.bat XWindows

งานมีลักษณะอย่างไร?

อย่างที่คุณทราบอยู่แล้วว่าใน Gradle นั้น สิ่งต่าง ๆ มาในรูปแบบต่าง ๆ มากมาย และงานก็ไม่มีข้อยกเว้น คำจำกัดความของงานสามารถทำได้หลายวิธี ส่วนใหญ่จะใช้วิธีนี้:

เมื่อเรียกใช้ taskName1จากด้านบน ผลลัพธ์จะไม่ชัดเจนนัก:

> Configure project :app
Why is this printed first?
> Task :app:taskName
First?
Last?

รหัสด้านบนมีคำอธิบายเพิ่มเติมและพูดเพื่อตัวเอง อย่างที่คุณเห็น เรากำลังเรียก ใช้ วัตถุTaskContainercreate()ของโครงการหลังจากนั้นเรากำหนดค่าคุณสมบัติกลุ่มของการกระทำที่สร้างขึ้นใหม่และเพิ่มการกระทำลงในรายการtasksTask

ลองดำเนินการของเราtaskName1อีกครั้งและดูผลลัพธ์:

> Configure project :app
Why is this printed first?
Why is this printed first?
> Task :app:taskName
First?
Last?

สร้างวงจรชีวิตและระยะต่างๆ

ไม่เหมือนกับงานที่ประกาศไว้ข้างต้น งานส่วนใหญ่ขึ้นอยู่กับงานอื่น ๆ ในการดำเนินงาน Gradle จะต้องเข้าใจงานที่มีอยู่ในโครงการและสิ่งที่ขึ้นอยู่กับงานนั้น สำหรับสิ่งนั้น Gradle จะสร้างกราฟการพึ่งพาแบบอะไซคลิกโดยตรงและดำเนินการตามนั้น

อย่าตกใจกับคำว่ากำกับกราฟการขึ้นต่อกันแบบวัฏจักร หมายความว่า:

  • งานและการขึ้นต่อกันนั้นประกอบกันเป็นโครงสร้างกราฟโดยที่โหนดเป็นตัวแทนของงาน และจุดยอด/เส้นแสดงถึงการขึ้นต่อกัน
  • ทิศทางของจุดยอดแสดงว่างานหนึ่งขึ้นอยู่กับงานอื่นอย่างไร
  • Acyclic หมายความว่าไม่มีงาน A และ B ที่ทั้งสองต้องพึ่งพาอาศัยกันโดยตรงหรือแบบสกรรมกริยา
  • การเริ่มต้น — เริ่มต้นด้วยการสร้างSettingsวัตถุตามsettings.gradleไฟล์และสร้างลำดับชั้นของโครงการย่อย (ใน Android Studio เรียกว่าโมดูล) ที่รวมอยู่ในโครงการ Gradle
  • การกำหนดค่า — กำหนดค่าแต่ละโครงการที่ค้นพบในขั้นตอนการเริ่มต้น จากนั้นไปที่build.gradleไฟล์ที่เกี่ยวข้องและกำหนดค่าProjectอินสแตนซ์และสร้างกราฟของงานซึ่งงานเป้าหมายขึ้นอยู่กับโดยตรงหรือชั่วคราว
  • การดำเนินการ — ดำเนินงานและงานทั้งหมดขึ้นอยู่กับงานที่ดำเนินการ ซึ่งทราบได้จากขั้นตอนการกำหนดค่า

สิ่งนี้สามารถหลีกเลี่ยงได้หรือไม่?

ใช่ Gradle มีAPI การหลีกเลี่ยงการกำหนดค่า เป็นวิธีที่แนะนำสำหรับการสร้างงานที่ช่วยลดเวลาการกำหนดค่าโดยหลีกเลี่ยงการทำงานกับTaskอินสแตนซ์โดยตรงและทำโดยใช้TaskProviderและสร้างการอ้างอิงไปยังไฟล์Task.

การใช้TaskContainer.register()จะป้องกันไม่ให้งานรวมอยู่ในขั้นตอนการกำหนดค่า เว้นแต่งานที่ลงทะเบียนจะถูกดำเนินการโดยตรงหรือรวมอยู่ในกราฟการพึ่งพาของงานที่กำลังดำเนินการ

ลองเรียกใช้taskName1อีกครั้งและดูว่าผลลัพธ์เหมือนเดิมก่อนที่จะเพิ่มไฟล์taskName3. ในเวลาเดียวกัน การเรียกใช้taskName3เพิ่มอีกหนึ่งบรรทัดในส่วนการกำหนดค่าของบันทึก เนื่องจากตอนนี้รวมอยู่ในขั้นตอนการกำหนดค่าพร้อมกับtaskName2และtaskName1

> Configure project :app
Why is this printed first?
Why is this printed first?
Why is this printed first?
> Task :app:taskName
First?
Last?

เหตุใดการวาง Actions ในลำดับการดำเนินการที่ถูกต้องจึงไม่เพียงพอ และเหตุใดเราจึงต้องการdoFirst{}และdoLast{}?

สักครู่ ให้ถือว่าการปิดเหล่านี้เป็นสิ่งที่ต้องดำเนินการก่อนและหลัง X แล้ว X คืออะไร

เพื่อตอบคำถามนั้น เรามากำหนดคลาสงานง่ายๆ และรันงานของคลาสงานที่สร้างขึ้นใหม่ เพื่อสาธิตวิธีการกำหนดงานอีกวิธีหนึ่ง

วางส่วนที่เกี่ยวข้องกับการกำหนดค่าของบันทึกไว้ เอาต์พุตสำหรับทั้งสองงานของประเภทที่กำหนดไว้ใหม่จะเป็น:

> Task :app:taskName5
First?
Before and after actions annotated with @TaskAction
Last?

ดังที่คุณเห็นจากข้อมูลโค้ด #4 คลาส CustomTaskTypeขยายDefaultTaskซึ่งเป็นคลาสพื้นฐานที่คุณสามารถขยายเพื่อใช้คลาสงานที่กำหนดเอง Gradle มีประเภทงานที่มีประโยชน์และพร้อมใช้งานหลายประเภท ซึ่งคุณจะ พบได้ใน Github ของGradle

มีอะไรอีกบ้างที่ต้องรู้เกี่ยวกับ Gradle Tasks

Taskมีผลลัพธ์ที่ระบุสิ่งที่เกิดขึ้นกับงานในระหว่างกระบวนการสร้าง โดยสัญชาตญาณ คุณอาจเดาได้ว่างานหนึ่งๆ สามารถมีผลลัพธ์สามอย่าง — ไม่ได้ดำเนินการ ดำเนินการโดยใช้ผลลัพธ์ที่แคชไว้ และเพิ่งดำเนินการ

ใน Gradle มีผลลัพธ์ของงานห้าอย่าง:

  1. NO-SOURCE — งานไม่ได้ดำเนินการเนื่องจากไม่พบข้อมูลอินพุตที่จำเป็นสำหรับการดำเนินการ ตัวอย่างของอินพุตอาจเป็นไฟล์ที่มีคำอธิบายประกอบ@InputFiles @SkipWhenEmptyและ@Incrementalไม่สามารถสร้างโดยงานก่อนหน้าได้
  2. SKIPPED— ข้ามด้วยเหตุผลบางประการ เหตุผลดังกล่าวสามารถเป็นได้ เช่น งานที่ทำเครื่องหมายไว้enabled = falseในเนื้อหาของงานหรือไม่รวมอยู่ในกระบวนการดำเนินการผ่านอาร์กิวเมนต์บรรทัดคำสั่ง-xและอื่นๆ อีกเล็กน้อย
  3. UP-TO-DATE— ผลลัพธ์ของงานไม่มีการเปลี่ยนแปลงตั้งแต่บิลด์ล่าสุดและสามารถใช้ซ้ำได้ สิ่งนี้เกิดขึ้นโดยเป็นส่วนหนึ่งของคุณสมบัติ การ สร้างส่วนเพิ่ม
  4. FROM-CACHE - ภารกิจสามารถนำมาจากงานสร้างก่อนหน้านี้ มันใช้คุณสมบัติที่เรียกว่าการแคชผลลัพธ์ของ งาน เป็นความก้าวหน้าของ Incremental build ที่ใช้UP-TO-DATEเนื่องจากสามารถใช้รีโมตแคชซ้ำได้โดยการรับแคชจาก CI เว้นแต่ว่าคุณจะมีorg.gralde.caching=trueหรือgradle.propertiesคุณใช้--build-cacheแฟล็กเมื่อเรียกใช้งาน สิ่งนี้จะไม่นำไปใช้กับงานสร้างของคุณ สำหรับงานที่จะจับได้ ควรใส่คำอธิบายประกอบ@CacheableTaskเป็น
  5. EXECUTED- งานสำเร็จลุล่วง ป้ายกำกับนี้ไม่แสดงในบันทึก

./gradlew assembleDebug --console=plain

การสั่งงานและการพึ่งพา

มีการกล่าวถึงว่า a Taskสามารถขึ้นอยู่กับงานอื่น ๆ แต่มันมีลักษณะอย่างไรในโค้ด? ตัวบ่งชี้เหล่านี้ด้านล่างแสดงว่างานต้องพึ่งพาซึ่งกันและกัน:

กำหนดความสัมพันธ์อย่างชัดเจนระหว่างสองงาน:

  • dependsOn— task X { dependsOn Y }งาน X ต้องการงาน Y สำหรับการดำเนินการ และถ้า Y ล้มเหลวในการดำเนินการ การดำเนินการของ X จะไม่เกิดขึ้น
  • finalizedBy— task X { finalizedBy Y }งาน Y จะถูกดำเนินการหลังจากงาน X แม้ว่า X จะล้มเหลวในการดำเนินการหรือถูกข้ามไปก็ตาม
  • @OutputFileและ@InputFile— เป็นวิธีโดยปริยายในการสร้างการพึ่งพาโดยการใส่คำอธิบายประกอบของอินพุตและเอาต์พุตของงาน วิธีการนี้ต้องการการกำหนดค่างานที่มีอินพุตและเอาต์พุตที่ตรงกัน

สามารถกำหนดงานได้หลายวิธี แต่ไม่ใช่ทั้งหมดที่ดีเท่ากัน สำหรับเวลาการกำหนดค่า ให้ใช้ API การหลีกเลี่ยงการกำหนดค่า และลงทะเบียนงานโดยTaskContainer.register()ใช้

เป็นการดีที่จะเข้าใจTaskผลลัพธ์เพื่อระบุจุดอ่อนของบิลด์และพยายามแคชผลลัพธ์การดำเนินการตามที่เป็นไปได้โดยการจัดโครงสร้างการพึ่งพาระหว่างงานอย่างเหมาะสมและทำให้กลไกการแคชเอาต์พุตของบิลด์ส่วนเพิ่มและผลลัพธ์ของงานทำงาน

Want to Connect?

Connect with me on Twitter and LinkedIn.