تحسين أداء التطبيق باستخدام الكوروتينات في لغة Kotlin

الكوروتينات في كوتلين تمكنك من كتابة شفرة غير متزامنة واضحة ومبسطة تحافظ على استجابة تطبيقك أثناء إدارة المهام طويلة المدى مثل اتصالات الشبكة أو عمليات القرص.

يوفّر هذا الموضوع نظرة تفصيلية على الكوروتينات على 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() في سلسلة التعليمات الرئيسية، لكنه يعلّق الكوروتين قبل بدء طلب الشبكة. عند اكتمال طلب الشبكة، يستأنف get تشغيل الكوروتين المعلّق بدلاً من استخدام معاودة الاتصال لإشعار سلسلة التعليمات الرئيسية.

تستخدم لغة Kotlin إطار تكديس لإدارة الدالة التي تعمل مع أي متغيرات محلية. عند تعليق الكوروتين، يتم نسخ إطار المكدس الحالي وحفظه لوقت لاحق. عند الاستئناف، يتم نسخ إطار المكدس مرة أخرى من حيث تم حفظه، ويبدأ تشغيل الدالة مرة أخرى. وعلى الرغم من أن الرمز البرمجي قد يبدو كطلب حظر تسلسلي عادي، يضمن الكوروتين أن طلب الشبكة يتجنّب حظر سلسلة التعليمات الرئيسية.

استخدام الكوروتين للحفاظ على الأمان الرئيسي

تستخدم الكوروتينات في لغة Kotlin المُرسِلين لتحديد سلاسل التعليمات التي يتم استخدامها لتنفيذ الكوروتين. لتشغيل رمز خارج سلسلة التعليمات الرئيسية، يمكنك ضبط الكوروتينات في لغة Kotlin على تنفيذ العمل على جهاز إرسال التلقائي أو IO. في Kotlin، يجب أن تعمل جميع الكوروتينات في المرسل، حتى عندما تعمل على سلسلة التعليمات الرئيسية. يمكن لبرامج الكوروتين تعليق نفسها، ويكون المرسل مسئولاً عن استئنافها.

لتحديد مكان تشغيل الكوروتينات، توفر لغة Kotlin ثلاثة مرسِلين يمكنك استخدامهم:

  • Dispatchers.Main - يمكنك استخدام جهة الإرسال هذه لتشغيل الكوروتين على سلسلة محادثات Android الرئيسية. يجب استخدام هذا فقط للتفاعل مع واجهة المستخدم وتنفيذ العمل السريع. وتشمل الأمثلة استدعاء دوال suspend وتشغيل عمليات إطار عمل واجهة المستخدم في Android وتعديل عناصر LiveData.
  • Dispatchers.IO - تم تحسين أداة الإرسال هذه لأداء وحدات الإدخال والإخراج على القرص أو الشبكة خارج سلسلة التعليمات الرئيسية. وتشمل الأمثلة استخدام مكوِّن الغرفة، والقراءة من الملفات أو الكتابة إليها، وتشغيل أي عمليات على الشبكة.
  • Dispatchers.Default - تم تحسين أداة الإرسال هذه لأداء عمل يستخدم وحدة المعالجة المركزية (CPU) خارج سلسلة التعليمات الرئيسية. تتضمن أمثلة حالات الاستخدام فرز القائمة وتحليل 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() تتيح لك التحكّم في مجموعة سلاسل التعليمات لأي سطر من الرموز بدون تقديم عمليات استدعاء، يمكنك تطبيقها على وظائف صغيرة جدًا مثل القراءة من قاعدة بيانات أو تنفيذ طلب شبكة. من الممارسات الجيدة استخدام withContext() للتأكد من أنّ كل دالة سليمة، ما يعني أنّه يمكنك استدعاء الدالة من سلسلة التعليمات الرئيسية. بهذه الطريقة، لا يحتاج المتصل أبدًا إلى التفكير في سلسلة التعليمات التي يجب استخدامها لتنفيذ الدالة.

في المثال السابق، يتم تنفيذ fetchDocs() على سلسلة التعليمات الرئيسية، ومع ذلك، يمكنها استدعاء الدالة get بأمان، والتي تنفذ طلب الشبكة في الخلفية. بما أنّ الكوروتينات متوافقة مع suspend وresume، يتم استئناف تشغيل الكوروتين في سلسلة التعليمات الرئيسية بالحصول على نتيجة get فور اكتمال حظر withContext.

أداء withContext()

withContext() لا يضيف أي تكاليف إضافية مقارنةً بالتنفيذ المستند إلى معاودة الاتصال. بالإضافة إلى ذلك، من الممكن في بعض الحالات تحسين طلبات withContext() بما يتجاوز عمليات التنفيذ المكافئة المستندة إلى معاودة الاتصال. على سبيل المثال، إذا أجرت دالة ما عشرة استدعاءات لشبكة، يمكنك الطلب من لغة 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() تُطلق برامج coroutine جديدة باستخدام async، تستخدم الدالة awaitAll() لانتظار انتهاء عمليات coroutine الجديدة التي تم إطلاقها قبل العودة. ومع ذلك، يُرجى ملاحظة أنّه حتى لو لم نتواصل مع awaitAll()، لا تستأنف أداة إنشاء coroutineScope الكوروتين الذي استدعى fetchTwoDocs إلا بعد اكتمال جميع الكوروتينات الجديدة.

بالإضافة إلى ذلك، يرصد coroutineScope أي استثناءات تُطرحها الكوروتينات ويعيد توجيهها إلى المتصل.

لمزيد من المعلومات عن التحليل المتوازٍ، يُرجى الاطّلاع على إنشاء دوال التعليق.

مفاهيم الكوروتين

نطاق الكوروتين

يتتبّع CoroutineScope أي الكوروتين الذي ينشئه باستخدام launch أو async. يمكن إلغاء العمل الجاري (أي تشغيل الكوروتينات) من خلال طلب scope.cancel() في أي وقت. في نظام Android، توفّر بعض مكتبات KTX رمز CoroutineScope الخاص بها لفئات معيّنة من مراحل النشاط. على سبيل المثال، يحتوي ViewModel على viewModelScope وLifecycle يتضمّن lifecycleScope. مع ذلك، لا يشغّل CoroutineScope الكوروتينات، على عكس المرسِل.

تُستخدم viewModelScope أيضًا في الأمثلة الموجودة في سلاسل المحادثات في الخلفية على نظام التشغيل Android باستخدام تنسيق "الكوروتين". مع ذلك، إذا كنت بحاجة إلى إنشاء 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)
        }
    }
}

مراجع إضافية لعناصر الكوروتين

للحصول على مزيد من موارد الكوروتين، اطلع على الروابط التالية: