تحسين أداء التطبيق باستخدام الكوروتينات في لغة 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) خارج نطاق سلسلة التعليمات الرئيسية. تتضمن أمثلة حالات الاستخدام فرز سرد وتحليل 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() للتأكد من أن كل دالة main-safe، مما يعني أنك يمكننا استدعاء الدالة من سلسلة التعليمات الرئيسية. بهذه الطريقة، لا يحتاج المتصل إلى فكر في مؤشر الترابط الذي يجب استخدامه لتنفيذ الدالة.

في المثال السابق، يتم تنفيذ 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() تطلق كوروتينات جديدة باستخدام async، لا يمكن اعتبار الدالة تستخدم awaitAll() لانتظار انتهاء الكوروتينات التي تم إطلاقها قبل تَعُودْ. ومع ذلك، تجدر الإشارة إلى أنه حتى لو لم نكُن قد استدعينا 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 تُحدِّد سلوك الكوروتين باستخدام المجموعة التالية من العناصر:

  • 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)
        }
    }
}

موارد إضافية حول الكوروتينات

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