โครูทีน Kotlin ทำให้คุณสามารถเขียนโค้ดแบบอะซิงโครนัสที่สะอาดตาและเรียบง่ายซึ่งจะช่วยให้ แอปของคุณตอบสนองได้ขณะจัดการงานที่ใช้เวลานาน เช่น การโทรจากเครือข่าย หรือการทำงานของดิสก์
หัวข้อนี้จะให้ข้อมูลเกี่ยวกับโครูทีนใน Android โดยละเอียด หากคุณ ไม่คุ้นกับโครูทีน อย่าลืมอ่าน โครูทีนของ Kotlin ใน Android ก่อนที่จะอ่านหัวข้อนี้
จัดการงานที่ใช้เวลานาน
โครูทีนสร้างจากฟังก์ชันปกติโดยการเพิ่มการดำเนินการ 2 อย่างเพื่อจัดการ
งานที่ใช้เวลานาน นอกเหนือจาก invoke
(หรือ call
) และ return
coroutine เพิ่ม suspend
และ resume
:
suspend
หยุดการดำเนินการของโครูทีนปัจจุบันไว้ชั่วคราว ซึ่งจะบันทึกในเครื่องทั้งหมด ตัวแปรresume
จะดำเนินการเกี่ยวกับโครูทีนที่ถูกระงับต่อไปจากตำแหน่งดังกล่าว ที่ถูกระงับ
คุณสามารถเรียกใช้ฟังก์ชัน suspend
จากฟังก์ชัน suspend
อื่นๆ เท่านั้น หรือ
โดยใช้เครื่องมือสร้างโครูทีน เช่น launch
เพื่อเริ่มสร้างโครูทีนใหม่
ตัวอย่างต่อไปนี้แสดงการใช้งานโคโรทีนอย่างง่ายสำหรับ งานสมมติที่ยาวนาน:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
ในตัวอย่างนี้ get()
ยังคงทำงานในเทรดหลัก แต่ระงับพารามิเตอร์
coroutine ไปให้เริ่มคำขอเครือข่าย เมื่อคำขอเครือข่าย
เสร็จสิ้นแล้ว get
จะนำโครูทีนที่ถูกระงับไปใช้ต่อแทนการใช้ Callback
เพื่อแจ้งเตือนเทรดหลัก
Kotlin ใช้เฟรมสแต็กเพื่อจัดการฟังก์ชันที่ทำงานอยู่ ด้วยตัวแปรภายใน เมื่อระงับ Coroutine สแต็กปัจจุบัน ระบบจะคัดลอกและบันทึกไว้ ใช้ในภายหลัง เมื่อกลับมาดำเนินการต่อ สแตกเฟรมจะ คัดลอกกลับจากจุดที่บันทึก และฟังก์ชันจะเริ่มทำงานอีกครั้ง แม้ว่าโค้ดอาจดูเหมือนการบล็อกตามลำดับทั่วไป ส่วน Coroutine จะตรวจสอบว่าคำขอเครือข่ายหลีกเลี่ยงการบล็อก เทรดหลัก
ใช้โครูทีนเพื่อความปลอดภัยหลัก
โครูทีนของ Kotlin ใช้ผู้จ่ายเพื่อกำหนดชุดข้อความที่จะใช้ ของคำสั่ง Coroutine หากต้องการเรียกใช้โค้ดนอกเทรดหลัก ให้แจ้ง Kotlin coroutine เพื่อทำงานให้กับผู้มอบหมายงานเป็น Default หรือ IO ใน Kotlin โครูทีนทั้งหมดต้องทำงานในผู้มอบหมายงาน แม้จะทำงานบน เทรดหลัก Coroutine สามารถระงับตัวเองได้ และผู้มอบหมายงาน มีหน้าที่กลับมาทำงานอีกครั้ง
ในการระบุว่าโครูทีนควรทำงานที่ไหน Kotlin จะมีผู้แจกจ่ายงาน 3 คน ที่คุณสามารถใช้ได้มีดังนี้
- Dispatchers.Main - ใช้ผู้มอบหมายงานนี้เพื่อเรียกใช้โครูทีน (Cooutine) บนเมน
Android Thread ซึ่งควรใช้สำหรับการโต้ตอบกับ UI และ
กำลังทำงานด่วน ตัวอย่างเช่น การเรียกฟังก์ชัน
suspend
, การวิ่ง การดำเนินการและอัปเดตเฟรมเวิร์ก UI ของ Android ออบเจ็กต์LiveData
รายการ - Dispatchers.IO - ผู้มอบหมายงานนี้ได้รับการปรับให้เหมาะสมสำหรับการทำงานของดิสก์หรือเครือข่าย I/O นอกเทรดหลัก ตัวอย่างเช่น การใช้ คอมโพเนนต์ห้อง การอ่านหรือการเขียนไปยังไฟล์ และเรียกใช้การดำเนินการของเครือข่าย
- Dispatchers.Default - ผู้มอบหมายงานนี้ได้รับการปรับให้มีประสิทธิภาพสูงสุด การทำงานที่ใช้ CPU นอกเทรดหลัก ตัวอย่างกรณีการใช้งานรวมถึงการจัดเรียง รายการและการแยกวิเคราะห์ JSON
จากตัวอย่างก่อนหน้านี้ คุณสามารถใช้ผู้มอบหมายงานเพื่อกำหนด
get
ภายในส่วนเนื้อหาของ get
ให้โทร withContext(Dispatchers.IO)
เพื่อ
สร้างบล็อกที่ทำงานบน Thread Pool ของ IO โค้ดที่คุณใส่ไว้ในส่วนนั้น
จะทำงานผ่านผู้มอบหมายงาน IO
เสมอ เนื่องจาก withContext
เองก็เป็น
ฟังก์ชันระงับ ฟังก์ชัน get
เป็นฟังก์ชันระงับด้วย
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
โครูทีนจะช่วยให้คุณส่งชุดข้อความด้วยการควบคุมแบบละเอียดได้ เพราะ
withContext()
ให้คุณควบคุม Thread Pool ของโค้ดบรรทัดใดก็ได้โดยไม่ต้อง
คุณสามารถใช้ Callback กับฟังก์ชันที่มีขนาดเล็กมาก เช่น การอ่าน
จากฐานข้อมูลหรือดำเนินการตามคำขอเครือข่าย แนวทางปฏิบัติที่ดีคือใช้
withContext()
เพื่อให้ทุกฟังก์ชันปลอดภัยหลัก ซึ่งหมายความว่าคุณ
สามารถเรียกใช้ฟังก์ชันจากเทรดหลัก วิธีนี้จะช่วยให้ผู้โทรไม่ต้อง
ลองพิจารณาว่าควรใช้เทรดใดเพื่อเรียกใช้ฟังก์ชัน
ในตัวอย่างก่อนหน้านี้ fetchDocs()
ดำเนินการกับเทรดหลัก แต่
สามารถเรียกใช้ get
ซึ่งส่งคำขอเครือข่ายในเบื้องหลังได้อย่างปลอดภัย
เนื่องจากโครูทีนรองรับ suspend
และ resume
โครูทีนที่อยู่บน
ชุดข้อความจะกลับมาทำงานอีกครั้งโดยมีผลลัพธ์ get
ทันทีที่การบล็อก withContext
เสร็จสิ้น
ประสิทธิภาพของ withContext()
withContext()
ไม่เพิ่มค่าใช้จ่ายเพิ่มเติมเมื่อเทียบกับค่า Callback ที่เทียบเท่า
การใช้งานของคุณ นอกจากนี้ คุณยังเพิ่มประสิทธิภาพการโทร withContext()
ได้ด้วย
นอกเหนือจากการใช้งานตาม Callback ที่เทียบเท่าในบางสถานการณ์ สำหรับ
เช่น ถ้าฟังก์ชันหนึ่งเรียกใช้เครือข่าย 10 ครั้ง คุณสามารถบอก Kotlin ให้
เปลี่ยนชุดข้อความเพียงครั้งเดียวโดยใช้ withContext()
ด้านนอก และแม้ว่า
ไลบรารีเครือข่ายใช้ withContext()
หลายครั้ง และเป็นเหมือนเดิม
และหลีกเลี่ยงการสลับชุดข้อความ นอกจากนี้ Kotlin ยังเพิ่มประสิทธิภาพในการเปลี่ยนผลิตภัณฑ์
ระหว่าง Dispatchers.Default
ถึง Dispatchers.IO
เพื่อหลีกเลี่ยงการเปลี่ยนชุดข้อความ
หากเป็นไปได้
เริ่มโครูทีน
คุณเริ่มสร้างโครูทีนได้ด้วย 2 วิธีดังนี้
launch
เริ่มโครูทีนใหม่และไม่แสดงผลให้ผู้โทร ช่วง งานที่ถือว่าเป็น "ไฟไหม้แล้วลืม" สามารถเริ่มใช้launch
ได้async
เริ่มโครูทีนใหม่และช่วยให้คุณแสดงผลลัพธ์ที่มีการระงับ ฟังก์ชันชื่อawait
โดยปกติแล้ว คุณควรlaunch
โครูทีนใหม่จากฟังก์ชันปกติ
เป็นฟังก์ชันปกติ ไม่สามารถเรียก await
ใช้ async
เมื่ออยู่ภายในเท่านั้น
โครูทีนอื่นหรือเมื่ออยู่ในฟังก์ชันการระงับและดำเนินการ
การแยกส่วนแบบขนาน
การแยกส่วนขนาน
โครูทีนทั้งหมดที่เริ่มต้นภายในฟังก์ชัน suspend
ต้องหยุดเมื่อ
ฟังก์ชันนั้นจะกลับมา คุณจึงควรตรวจสอบว่าโครูทีนเหล่านั้น
ให้เสร็จเรียบร้อยก่อนส่งคืน เมื่อใช้การเกิดขึ้นพร้อมกันที่มีโครงสร้างใน Kotlin คุณสามารถกำหนด
coroutineScope
ที่เริ่มโครูทีนอย่างน้อย 1 ตัว จากนั้นใช้ await()
(สำหรับโครูทีน 1 ลูก) หรือ awaitAll()
(สำหรับโครูทีน 1 ลูก) คุณสามารถทำสิ่งต่อไปนี้
รับประกันว่าโครูทีนเหล่านี้เสร็จก่อนกลับไปจากฟังก์ชันดังกล่าว
ตัวอย่างเช่น สมมติว่ามี coroutineScope
ที่ดึงเอกสาร 2 รายการ
แบบไม่พร้อมกัน การเรียกใช้ await()
ในข้อมูลอ้างอิงที่เลื่อนเวลาแต่ละรายการทำให้เรารับประกัน
ที่การดำเนินการของ async
ทั้ง 2 รายการเสร็จสิ้นก่อนแสดงผลค่า:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
คุณยังใช้ awaitAll()
กับคอลเล็กชันได้ด้วย ดังที่แสดงในตัวอย่างต่อไปนี้
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
แม้ว่า fetchTwoDocs()
จะเปิดตัวโครูทีนใหม่ที่มี async
แต่ฟังก์ชันนี้
ใช้ awaitAll()
เพื่อรอให้โครูทีนที่เปิดตัวนั้นเสร็จสิ้นก่อน
ที่กลับมา อย่างไรก็ตาม โปรดทราบว่าแม้เราจะไม่ได้โทรหา awaitAll()
เครื่องมือสร้าง coroutineScope
ไม่เรียกใช้โครูทีนที่เรียกใช้ต่อ
fetchTwoDocs
จนกว่าโครูทีนใหม่ทั้งหมดจะเสร็จสมบูรณ์
นอกจากนี้ coroutineScope
จะตรวจจับข้อยกเว้นที่โครูทีนส่ง
และนำกลับไปยังผู้โทร
โปรดดูข้อมูลเพิ่มเติมเกี่ยวกับการแยกส่วนแบบขนานที่ การเขียนฟังก์ชันการระงับ
แนวคิดของ Coroutine
โครูทีนสโคป
CoroutineScope
ติดตามโครูทีนที่สร้างขึ้นโดยใช้ launch
หรือ async
งานที่กำลังดำเนินอยู่ (เช่น โครูทีนที่กำลังทำงานอยู่) สามารถยกเลิกได้โดยการโทร
scope.cancel()
ได้ทุกเมื่อ ใน Android ไลบรารี KTX บางส่วนมี
CoroutineScope
ของตัวเองสําหรับคลาสวงจรบางคลาส ตัวอย่างเช่น
ViewModel
มี
viewModelScope
,
และ Lifecycle
มี lifecycleScope
แต่ CoroutineScope
จะไม่เรียกใช้โครูทีน ซึ่งต่างจากผู้มอบหมายงาน
viewModelScope
ก็ใช้ในตัวอย่างที่พบใน
การแยกชุดข้อความพื้นหลังใน Android ด้วย Coroutines
แต่หากคุณต้องการสร้าง CoroutineScope
ของคุณเองเพื่อควบคุม
วงจรของโครูทีนในเลเยอร์เฉพาะของแอป
ดังนี้
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
ขอบเขตที่ถูกยกเลิกไม่สามารถสร้างโครูทีนเพิ่มได้ ดังนั้น คุณควร
เรียกใช้ scope.cancel()
เฉพาะเมื่อคลาสที่ควบคุมวงจร
ถูกทำลาย เมื่อใช้ viewModelScope
พารามิเตอร์
ชั้นเรียน ViewModel
ยกเลิก
ให้คุณโดยอัตโนมัติในเมธอด onCleared()
ของ ViewModel โดยอัตโนมัติ
งาน
Job
เป็นแฮนเดิลของโครูทีน โครูทีนแต่ละรายการที่คุณสร้างด้วย launch
หรือ async
จะแสดงผลอินสแตนซ์ Job
ที่ระบุพารามิเตอร์
โครูทีนและจัดการวงจร นอกจากนี้คุณยังส่ง Job
ไปยัง
CoroutineScope
เพื่อจัดการวงจรต่อไป ดังที่แสดงใน
ตัวอย่าง:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
บริบท Coroutine
CoroutineContext
กำหนดพฤติกรรมของโคโรทีนโดยใช้ชุดองค์ประกอบต่อไปนี้
Job
: ควบคุมวงจรของโครูทีนCoroutineDispatcher
: มอบหมายงานไปยังชุดข้อความที่เหมาะสมCoroutineName
: ชื่อของ Coroutine มีประโยชน์สำหรับการแก้ไขข้อบกพร่องCoroutineExceptionHandler
: จัดการข้อยกเว้นที่ตรวจไม่พบ
สำหรับโครูทีนใหม่ที่สร้างขึ้นภายในขอบเขต อินสแตนซ์ Job
ใหม่จะเป็น
ให้กับโครูทีนใหม่และองค์ประกอบ CoroutineContext
อื่นๆ
จะรับค่ามาจากขอบเขตที่มี คุณสามารถลบล้าง
เอลิเมนต์โดยการส่ง CoroutineContext
ใหม่ไปยัง launch
หรือ async
โปรดทราบว่าการส่ง Job
ไปยัง launch
หรือ async
จะไม่มีผล
เมื่อมีการกำหนดอินสแตนซ์ใหม่ของ Job
ให้กับโครูทีนใหม่เสมอ
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
แหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีน
ดูแหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนได้จากลิงก์ต่อไปนี้