عملکرد برنامه را با کوروتین های Kotlin بهبود بخشید

برنامه‌های Kotlin به شما امکان می‌دهند کدهای ناهمزمان ساده و تمیزی بنویسید که همزمان با مدیریت وظایف طولانی‌مدت مانند تماس‌های شبکه یا عملیات دیسک، برنامه شما را پاسخگو نگه می‌دارد.

این موضوع نگاهی دقیق به کوروتین ها در اندروید ارائه می دهد. اگر با کوروتین ها آشنا نیستید، قبل از خواندن این مبحث، حتماً کوروتین های Kotlin را در اندروید بخوانید.

وظایف طولانی مدت را مدیریت کنید

Coroutine ها بر اساس توابع منظم با افزودن دو عملیات برای انجام وظایف طولانی مدت ساخته می شوند. علاوه بر 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() همچنان روی رشته اصلی اجرا می‌شود، اما قبل از شروع درخواست شبکه، coroutine را به حالت تعلیق در می‌آورد. هنگامی که درخواست شبکه تکمیل شد، به جای استفاده از تماس پاسخ برای اطلاع دادن به رشته اصلی، get از سر می‌گیرد.

کاتلین از یک قاب پشته برای مدیریت اینکه کدام تابع همراه با متغیرهای محلی اجرا می شود استفاده می کند. هنگام تعلیق یک کوروتین، قاب پشته فعلی کپی شده و برای بعد ذخیره می شود. هنگام از سرگیری، قاب پشته از جایی که ذخیره شده است کپی می شود و عملکرد دوباره شروع به اجرا می کند. حتی اگر کد ممکن است شبیه یک درخواست مسدود متوالی معمولی به نظر برسد، Coroutine تضمین می کند که درخواست شبکه از مسدود کردن رشته اصلی جلوگیری می کند.

برای ایمنی اصلی از کوروتین ها استفاده کنید

کوروتین های Kotlin از توزیع کننده ها برای تعیین اینکه کدام رشته ها برای اجرای کوروتین استفاده می شوند استفاده می کنند. برای اجرای کد خارج از رشته اصلی، می‌توانید به Kotlin دستور دهید که کار را روی دیسپچر پیش‌فرض یا IO انجام دهد. در Kotlin، همه کوروتین ها باید در یک توزیع کننده اجرا شوند، حتی زمانی که در رشته اصلی اجرا می شوند. کوروتین ها می توانند خود را به حالت تعلیق درآورند و توزیع کننده مسئول از سرگیری آنها است.

برای مشخص کردن جایی که کوروتین ها باید اجرا شوند، Kotlin سه توزیع کننده ارائه می دهد که می توانید از آنها استفاده کنید:

  • Dispatchers.Main - از این توزیع کننده برای اجرای یک برنامه در موضوع اصلی اندروید استفاده کنید. این باید فقط برای تعامل با UI و انجام کار سریع استفاده شود. به عنوان مثال می توان به فراخوانی توابع suspend ، اجرای عملیات چارچوب رابط کاربری Android و به روز رسانی اشیاء LiveData اشاره کرد.
  • Dispatchers.IO - این دیسپچر برای انجام ورودی/خروجی دیسک یا شبکه در خارج از رشته اصلی بهینه شده است. مثال‌ها عبارتند از: استفاده از مؤلفه اتاق ، خواندن یا نوشتن روی فایل‌ها، و اجرای هرگونه عملیات شبکه.
  • Dispatchers.Default - این توزیع کننده برای انجام کارهای فشرده CPU در خارج از رشته اصلی بهینه شده است. موارد استفاده مثال شامل مرتب سازی یک لیست و تجزیه JSON است.

در ادامه مثال قبلی، می توانید از توزیع کننده ها برای تعریف مجدد تابع get استفاده کنید. در داخل بدنه get ، withContext(Dispatchers.IO) را فراخوانی کنید تا بلوکی ایجاد کنید که روی مخزن رشته IO اجرا شود. هر کدی که داخل آن بلوک قرار می دهید همیشه از طریق IO Dispatcher اجرا می شود. از آنجایی که 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 هر خطی از کد را بدون ارائه پاسخ تماس کنترل کنید، می‌توانید آن را برای توابع بسیار کوچک مانند خواندن از پایگاه داده یا انجام درخواست شبکه اعمال کنید. یک تمرین خوب این است که از withContext() استفاده کنید تا مطمئن شوید که هر تابع در حالت main-safe است، به این معنی که می توانید تابع را از رشته اصلی فراخوانی کنید. به این ترتیب، تماس گیرنده هرگز نیازی به فکر کردن در مورد اینکه کدام رشته باید برای اجرای تابع استفاده شود.

در مثال قبلی، fetchDocs() روی رشته اصلی اجرا می شود. با این حال، می‌تواند با خیال راحت get فراخوانی کند، که درخواست شبکه را در پس‌زمینه انجام می‌دهد. از آنجایی که کوروتین‌ها از suspend و resume پشتیبانی می‌کنند، به محض اینکه بلوک withContext تمام شد، روتین روی رشته اصلی با نتیجه get از سر گرفته می‌شود.

عملکرد withContext()

withContext() سربار اضافی را در مقایسه با پیاده سازی مبتنی بر callback معادل اضافه نمی کند. علاوه بر این، در برخی موقعیت‌ها می‌توان فراخوانی‌های withContext() را فراتر از یک پیاده‌سازی مبتنی بر callback معادل بهینه کرد. به عنوان مثال، اگر یک تابع ده تماس با یک شبکه برقرار کند، می‌توانید به Kotlin بگویید که تنها یک بار با استفاده از یک withContext() خارجی، رشته‌ها را تغییر دهد. سپس، حتی اگر کتابخانه شبکه چندین بار از withContext() استفاده می کند، در همان توزیع کننده باقی می ماند و از تغییر رشته ها اجتناب می کند. علاوه بر این، Kotlin جابجایی بین Dispatchers.Default و Dispatchers.IO را بهینه می‌کند تا در صورت امکان از سوئیچ‌های رشته اجتناب شود.

یک کوروتین را شروع کنید

شما می توانید کوروتین ها را به یکی از دو روش شروع کنید:

  • launch یک کوروتین جدید را شروع می کند و نتیجه را به تماس گیرنده بر نمی گرداند. هر کاری که "آتش و فراموش" در نظر گرفته می شود را می توان با استفاده از launch شروع کرد.
  • async یک کوروتین جدید را شروع می کند و به شما امکان می دهد یک نتیجه را با یک تابع تعلیق به نام await برگردانید.

به طور معمول، باید یک کوروتین جدید از یک تابع معمولی launch ، زیرا یک تابع معمولی نمی‌تواند await فراخوانی شود. از async فقط زمانی که داخل کوروتین دیگری هستید یا در داخل یک تابع تعلیق و انجام تجزیه موازی استفاده کنید.

تجزیه موازی

تمام کوروتین‌هایی که در داخل یک تابع suspend شروع می‌شوند باید با بازگشت آن تابع متوقف شوند، بنابراین احتمالاً باید تضمین کنید که آن کوروتین‌ها قبل از بازگشت تمام می‌شوند. با همزمانی ساختاریافته در Kotlin، می توانید یک coroutineScope تعریف کنید که یک یا چند کوروتین را شروع می کند. سپس، با استفاده از await() (برای یک کوروتین واحد) یا awaitAll() (برای چندین کوروتین)، می توانید تضمین کنید که این کوروتین ها قبل از بازگشت از تابع به پایان می رسند.

به عنوان مثال، اجازه دهید یک coroutineScope را تعریف کنیم که دو سند را به صورت ناهمزمان واکشی می کند. با فراخوانی await() روی هر مرجع معوق، تضمین می کنیم که هر دو عملیات async قبل از برگرداندن یک مقدار به پایان می رسند:

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() در هر نقطه از زمان لغو کرد. در اندروید، برخی از کتابخانه‌های KTX CoroutineScope خود را برای کلاس‌های چرخه حیات خاص ارائه می‌کنند. برای مثال، ViewModel دارای viewModelScope و Lifecycle دارای lifecycleScope است. با این حال، برخلاف دیسپچر، یک CoroutineScope کوروتین ها را اجرا نمی کند.

viewModelScope همچنین در نمونه‌هایی که در Background Threading در 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()
        }
    }
}

CoroutineContext

یک CoroutineContext رفتار یک کوروتین را با استفاده از مجموعه عناصر زیر تعریف می کند:

  • Job : چرخه زندگی کوروتین را کنترل می کند.
  • CoroutineDispatcher : ارسال ها به موضوع مناسب کار می کنند.
  • CoroutineName : نام کوروتین که برای اشکال زدایی مفید است.
  • CoroutineExceptionHandler : استثنائات ناشناخته را کنترل می کند.

برای coroutine های جدید ایجاد شده در یک محدوده، یک Job instance جدید به coroutine جدید اختصاص داده می شود و سایر عناصر 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)
        }
    }
}

منابع اضافی کوروتین

برای دریافت منابع بیشتر به پیوندهای زیر مراجعه کنید: