קורוטין של קוטלין מאפשרות לכתוב קוד אסינכרוני נקי ופשוט ששומר האפליקציה רספונסיבית בזמן ניהול משימות ממושכות, כמו שיחות רשת או פעולות בדיסקים.
בנושא הזה מפורט מידע מפורט על קורוטין ב-Android. אם אתם לא מכירים את קורוטינים, חשוב לקרוא לפני קריאת הנושא, קורוטין של קוטלין ב-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()
עדיין תפעל בשרשור הראשי, אבל היא משעה את
coroutine לפני שהיא מפעילה את בקשת הרשת. כאשר בקשת הרשת
לאחר השלמת הפעולה, get
ימשיך את השימוש בקורוטין המושעה במקום להשתמש בקריאה חוזרת (callback)
כדי לקבל התראות לשרשור הראשי.
Kotlin משתמשים במסגרת סטאק כדי לקבוע איזו פונקציה תפעל לאורך עם משתנים מקומיים כלשהם. כשמשעים קורוטין, הערימה הנוכחית המסגרת מועתקת ונשמרת למועד מאוחר יותר. אם ממשיכים, מסגרת הערימה הועתקה בחזרה מהמקום שבו היא נשמרה, והפונקציה תתחיל לפעול שוב. למרות שהקוד עשוי להיראות כמו חסימה רציפה רגילה הקורוטינה מוודאת שבקשת הרשת לא תיחסם בשרשור הראשי.
שימוש בקורוטינים לשמירה על הבטיחות העיקרית
קורוטינים של Kotlin משתמשים במשגרים כדי לקבוע אילו שרשורים משמשים ביצוע קורוטין. כדי להריץ קוד מחוץ ל-thread הראשי, אפשר לומר ל-Kotlin שגרים (coroutines) לביצוע עבודה בשולחנות ברירת המחדל או ב-IO. לחשבון Kotlin, כל הקורוטינים חייבים לרוץ בסדרנים, גם כשהם פועלים בשרשור הראשי. קורוטינים יכולים להשעות את עצמם, והשולח באחריות לחדש את השימוש בהם.
כדי לציין את המקומות שבהם קורוטין אמורות לפעול, Kotlin מספק שלושה מפיצים שאפשר להשתמש בהם:
- Dispatchers.Main – משתמשים בסדרן הזה כדי להריץ קורוטין
שרשור ב-Android. צריך להשתמש באפשרות הזו רק לאינטראקציה עם ממשק המשתמש
ביצוע עבודה מהירה. לדוגמה: קריאה לפונקציות של
suspend
, הרצה פעולות ועדכון של framework של ממשק המשתמש ב-AndroidLiveData
אובייקטים. - Dispatchers.IO – השולח הזה מותאם לביצוע דיסק או רשת קלט/פלט (I/O) מחוץ ל-thread הראשי. לדוגמה: רכיב החדר, קריאה מקבצים או כתיבה אליהם, והרצת פעולות רשת כלשהן.
- Dispatchers.Default – המשלח הזה עבר אופטימיזציה לביצועים עבודה אינטנסיבית במעבד (CPU) מחוץ ל-thread הראשי. תרחישים לדוגמה כוללים מיון ליצירת רשימה ולניתוח של JSON.
בהמשך לדוגמה הקודמת, אתם יכולים להשתמש בסדרנים כדי להגדיר מחדש
get
. בתוך הגוף של get
, יש להתקשר למספר withContext(Dispatchers.IO)
כדי
ליצור בלוק שרץ על מאגר השרשורים של ה-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()
אפשר לשלוט במאגר השרשורים של כל שורת קוד בלי
באמצעות קריאות חוזרות (callback), אפשר להחיל אותן על פונקציות קטנות מאוד כמו קריאה
ממסד נתונים או מביצוע בקשת רשת. אחת מהשיטות המומלצות היא להשתמש
withContext()
כדי לוודא שכל פונקציה היא ראשית בטוחה. כלומר,
הוא יכול לקרוא לפונקציה מה-thread הראשי. כך, המתקשר אף פעם לא צריך
כדאי לחשוב באיזה שרשור להשתמש כדי להפעיל את הפונקציה.
בדוגמה הקודמת, fetchDocs()
יופעל ב-thread הראשי. עם זאת,
יכול להתקשר בבטחה ל-get
, שמבצע בקשת רשת ברקע.
מאחר שקורוטינים תומכים בsuspend
ובresume
, הקורוטין
השרשור מתחדש עם התוצאה get
ברגע שהבלוק withContext
בוצע.
הביצועים של withContext()
withContext()
לא מוסיפה תקורה נוספת בהשוואה לשיטה מקבילה של קריאה חוזרת
יישום בפועל. בנוסף, אפשר לבצע אופטימיזציה של שיחות ב-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()
,
ה-builder של coroutineScope
לא ממשיך את התהליך של קורוטין
fetchTwoDocs
עד לסיום כל הקורוטינים החדשים.
בנוסף, coroutineScope
מזהה חריגים שהקורוטינים גורמים
ומנתב אותם חזרה למתקשר.
למידע נוסף על פירוק מקביל ראו כתיבת פונקציות השעיה.
מושגי קורוטינה
היקף קואוטין
CoroutineScope
עוקב אחרי כל קורוטין שנוצר באמצעות launch
או async
.
ניתן לבטל עבודה מתמשכת (כלומר
scope.cancel()
בכל שלב. ב-Android, ספריות KTX מסוימות מספקות
CoroutineScope
משלהם לסיווגים מסוימים של מחזורי חיים. לדוגמה,
ל-ViewModel
יש
viewModelScope
,
וב-Lifecycle
יש lifecycleScope
.
עם זאת, בשונה משליח, CoroutineScope
לא מפעיל את הקורוטינים.
viewModelScope
משמש גם בדוגמאות שמופיעות ב-
הפעלת שרשורים ברקע ב-Android עם Coroutine.
עם זאת, אם אתם צריכים ליצור 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
: טיפול בחריגים לא מזוהים.
בשביל קורוטינים חדשים שנוצרו במסגרת היקף, מופע 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)
}
}
}
משאבים נוספים בנושא קורוטינים
למשאבים נוספים בנושא קורוטינים, ראו את הקישורים הבאים: