ปรับปรุงประสิทธิภาพของแอปด้วยโครูทีน Kotlin

โครูทีน 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)
        }
    }
}

แหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีน

ดูแหล่งข้อมูลเพิ่มเติมเกี่ยวกับโครูทีนได้จากลิงก์ต่อไปนี้