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

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

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

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

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

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

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

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

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

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

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

عملکرد withContext()‎

withContext() در مقایسه با پیاده‌سازی معادل مبتنی بر بازخوانی، سربار اضافی اضافه نمی‌کند. علاوه‌براین، در برخی‌از شرایط می‌توان تماس‌های withContext() را فراتر از پیاده‌سازی معادل مبتنی بر تماس برگشتی بهینه‌سازی کرد. برای مثال، اگر تابعی ده بار با شبکه تماس می‌گیرد، می‌توانید بااستفاده از withContext() بیرونی به Kotlin بگویید فقط یک‌بار رشته‌ها را عوض کند. سپس، حتی اگر کتابخانه شبکه چندین بار از 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() در هر زمانی لغو کرد. در 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 (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)
        }
    }
}

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

برای منابع بیشتر مربوط به روتین‌های همکار، پیوندهای زیر را ببینید: