برنامههای 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)
}
}
}
منابع اضافی کوروتین
برای دریافت منابع بیشتر به پیوندهای زیر مراجعه کنید: