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

โครูทีน Kotlin ช่วยให้คุณเขียนโค้ดแบบอะซิงโครนัสที่สะอาดและเรียบง่ายซึ่งจะช่วยให้ แอปตอบสนองได้ดีขณะจัดการงานที่ใช้เวลานาน เช่น การเรียกเครือข่าย หรือการดำเนินการในดิสก์

หัวข้อนี้จะเจาะลึกเกี่ยวกับโครูทีนใน Android หากคุณไม่คุ้นเคยกับ Coroutines โปรดอ่านKotlin Coroutines ใน Android ก่อนอ่านหัวข้อนี้

จัดการงานที่ใช้เวลานาน

โครูทีนสร้างขึ้นจากฟังก์ชันปกติโดยเพิ่มการดำเนินการ 2 อย่างเพื่อจัดการ งานที่ใช้เวลานาน นอกจาก invoke (หรือ call) และ return แล้ว โครูทีนยังเพิ่ม 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() ยังคงทำงานในเทรดหลัก แต่จะระงับ โครูทีนก่อนที่จะเริ่มคำขอเครือข่าย เมื่อคำขอเครือข่าย เสร็จสมบูรณ์ get จะกลับมาทำงานต่อในโครูทีนที่ระงับไว้แทนที่จะใช้โค้ดเรียกกลับ เพื่อแจ้งเตือนเทรดหลัก

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

ใช้โครูทีนเพื่อความปลอดภัยของ Main

โคโรทีน Kotlin ใช้ตัวจัดสรรเพื่อกำหนดว่าควรใช้เธรดใดในการ ดำเนินการโคโรทีน หากต้องการเรียกใช้โค้ดนอกเทรดหลัก คุณสามารถบอกให้โคโรทีนของ Kotlin ดำเนินการใน Dispatcher Default หรือ IO ก็ได้ ใน Kotlin โครูทีนทั้งหมดต้องทำงานใน Dispatcher แม้ว่าจะทำงานในเทรดหลักก็ตาม โครูทีนสามารถระงับตัวเองได้ และ Dispatcher มีหน้าที่รับผิดชอบในการกลับมาทำงานต่อ

Kotlin มี Dispatcher 3 รายการที่คุณใช้ได้เพื่อระบุตำแหน่งที่ควรเรียกใช้ Coroutine

  • Dispatchers.Main - ใช้ Dispatcher นี้เพื่อเรียกใช้ Coroutine ในเทรดหลักของ Android ควรใช้เพื่อโต้ตอบกับ UI และ ทำงานอย่างรวดเร็วเท่านั้น ตัวอย่างเช่น การเรียกใช้suspendฟังก์ชัน การเรียกใช้การดำเนินการเฟรมเวิร์ก UI ของ Android และการอัปเดตออบเจ็กต์ LiveData
  • Dispatchers.IO - Dispatcher นี้ได้รับการเพิ่มประสิทธิภาพเพื่อดำเนินการ I/O ของดิสก์หรือเครือข่าย นอกเทรดหลัก ตัวอย่างเช่น การใช้ คอมโพเนนต์ Room การอ่านหรือเขียนไฟล์ และการเรียกใช้การดำเนินการเครือข่าย
  • Dispatchers.Default - Dispatcher นี้ได้รับการเพิ่มประสิทธิภาพเพื่อทำงานที่ใช้ CPU สูงนอกเทรดหลัก ตัวอย่างกรณีการใช้งาน ได้แก่ การจัดเรียงรายการและการแยกวิเคราะห์ JSON

จากตัวอย่างก่อนหน้า คุณสามารถใช้ Dispatcher เพื่อกำหนดฟังก์ชัน get ใหม่ได้ ภายในเนื้อหาของ get ให้เรียกใช้ withContext(Dispatchers.IO) เพื่อ สร้างบล็อกที่ทำงานในพูลเธรด I/O โค้ดที่คุณใส่ไว้ในบล็อกนั้นจะทำงานผ่านตัวส่ง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

โดย Coroutine ช่วยให้คุณส่งเธรดด้วยการควบคุมแบบละเอียดได้ เนื่องจาก withContext() ช่วยให้คุณควบคุม Thread Pool ของโค้ดบรรทัดใดก็ได้โดยไม่ต้อง ใช้ Callback คุณจึงใช้กับฟังก์ชันขนาดเล็กมากได้ เช่น การอ่าน จากฐานข้อมูลหรือการส่งคำขอเครือข่าย แนวทางปฏิบัติที่ดีคือการใช้ withContext() เพื่อให้แน่ใจว่าทุกฟังก์ชันเป็น main-safe ซึ่งหมายความว่าคุณ เรียกใช้ฟังก์ชันจากเทรดหลักได้ ด้วยวิธีนี้ ผู้โทรจึงไม่จำเป็นต้อง คิดว่าควรใช้เธรดใดเพื่อเรียกใช้ฟังก์ชัน

ในตัวอย่างก่อนหน้า fetchDocs() จะดำเนินการในเธรดหลัก แต่สามารถเรียกใช้ get ได้อย่างปลอดภัย ซึ่งจะส่งคำขอเครือข่ายในเบื้องหลัง เนื่องจากโครูทีนรองรับ suspend และ resume โครูทีนในเทรดหลักจะกลับมาทำงานต่อพร้อมผลลัพธ์ get ทันทีที่บล็อก withContext เสร็จสิ้น

ประสิทธิภาพของ withContext()

withContext() ไม่ได้เพิ่มค่าใช้จ่ายเพิ่มเติมเมื่อเทียบกับการติดตั้งใช้งานที่เทียบเท่าซึ่งอิงตามการเรียกกลับ นอกจากนี้ คุณยังเพิ่มประสิทธิภาพwithContext()การโทร ให้ดีกว่าการติดตั้งใช้งานที่ใช้การเรียกกลับที่เทียบเท่าได้ในบางสถานการณ์ ตัวอย่างเช่น หากฟังก์ชันโทรไปยังเครือข่าย 10 ครั้ง คุณสามารถบอก Kotlin ให้ เปลี่ยนเธรดเพียงครั้งเดียวได้โดยใช้ withContext() ด้านนอก จากนั้นแม้ว่าไลบรารีเครือข่ายจะใช้ withContext() หลายครั้ง แต่ก็จะยังคงอยู่ใน Dispatcher เดียวกันและหลีกเลี่ยงการสลับเธรด นอกจากนี้ Kotlin ยังเพิ่มประสิทธิภาพการสลับ ระหว่าง Dispatchers.Default กับ Dispatchers.IO เพื่อหลีกเลี่ยงการสลับเธรด ทุกครั้งที่ทำได้

เริ่มโครูทีน

คุณเริ่มโครูทีนได้ 2 วิธีดังนี้

  • launch เริ่มโครูทีนใหม่และไม่ส่งคืนผลลัพธ์ไปยังผู้เรียก คุณสามารถเริ่มงานใดก็ตามที่ถือว่าเป็น "ยิงแล้วทิ้ง" ได้โดยใช้ launch
  • async จะเริ่มโครูทีนใหม่และช่วยให้คุณส่งคืนผลลัพธ์ด้วยฟังก์ชันระงับ ที่ชื่อ await

โดยปกติแล้ว คุณควรlaunchโครูทีนใหม่จากฟังก์ชันปกติ เนื่องจากฟังก์ชันปกติเรียกใช้ await ไม่ได้ ใช้ async เฉพาะเมื่ออยู่ภายใน โครูทีนอื่นหรือเมื่ออยู่ภายในฟังก์ชันระงับและทำการ แยกย่อยแบบขนาน

การแยกย่อยแบบคู่ขนาน

Coroutine ทั้งหมดที่เริ่มต้นภายในฟังก์ชัน suspend จะต้องหยุดเมื่อฟังก์ชันนั้นแสดงผล ดังนั้นคุณจึงอาจต้องตรวจสอบว่า Coroutine เหล่านั้นเสร็จสิ้นก่อนที่จะแสดงผล การทำงานพร้อมกันแบบมีโครงสร้างใน Kotlin ช่วยให้คุณกำหนด coroutineScope ที่เริ่มคอร์รูทีนอย่างน้อย 1 รายการได้ จากนั้นใช้ await() (สำหรับโครูทีนเดียว) หรือ awaitAll() (สำหรับโครูทีนหลายรายการ) คุณจะ รับประกันได้ว่าโครูทีนเหล่านี้จะทำงานเสร็จก่อนที่จะกลับจากฟังก์ชัน

ตัวอย่างเช่น มากำหนด 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 ยังดักจับข้อยกเว้นใดๆ ที่โครูทีนส่ง และกำหนดเส้นทางกลับไปยังผู้เรียกใช้

ดูข้อมูลเพิ่มเติมเกี่ยวกับการแยกย่อยแบบขนานได้ที่ การเขียนฟังก์ชันระงับ

แนวคิดของโครูทีน

CoroutineScope

CoroutineScope จะติดตามโครูทีนที่สร้างโดยใช้ launch หรือ async คุณยกเลิก งานที่กำลังดำเนินการอยู่ (เช่น โครูทีนที่กำลังทำงาน) ได้โดยเรียกใช้ scope.cancel() ได้ทุกเมื่อ ใน Android ไลบรารี KTX บางรายการมีCoroutineScopeของตัวเอง สำหรับคลาสวงจรบางคลาส เช่น ViewModel มี viewModelScope และ Lifecycle มี lifecycleScope อย่างไรก็ตาม CoroutineScope ไม่ได้เรียกใช้โครูทีนเหมือนกับ Dispatcher

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 (condition) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

CoroutineContext กำหนดลักษณะการทำงานของโครูทีนโดยใช้ชุดองค์ประกอบต่อไปนี้

  • Job: ควบคุมวงจรของโครูทีน
  • CoroutineDispatcher: ส่งงานไปยังเธรดที่เหมาะสม
  • CoroutineName: ชื่อของโครูทีน ซึ่งมีประโยชน์สำหรับการแก้ไขข้อบกพร่อง
  • 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)
        }
    }
}

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

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