تحسين الأداء من خلال سلاسل المحادثات

ويمكن أن يساعدك الاستخدام السريع لسلاسل المحادثات على Android في تعزيز أداء تطبيقك. تناقش هذه الصفحة العديد من جوانب التعامل مع سلاسل المحادثات وهي: العمل باستخدام واجهة المستخدم أو سلسلة التعليمات الرئيسية، والعلاقة بين دورة حياة التطبيق وأولوية سلسلة التعليمات، والطرق التي توفّرها المنصة للمساعدة في إدارة مدى تعقيد سلاسل المحادثات. في كل مجال من هذه المجالات، تصف هذه الصفحة الصعاب والاستراتيجيات المحتملة لتجنبها.

سلسلة التعليمات الرئيسية

عندما يبدأ المستخدم تشغيل تطبيقك، ينشئ Android عملية Linux جديدة مع سلسلة تعليمات التنفيذ. إنّ سلسلة التعليمات الرئيسية هذه، والمعروفة أيضًا باسم سلسلة رموز واجهة المستخدم، مسؤولة عن كل ما يحدث على الشاشة. يمكن أن يساعدك فهم كيفية عملها في تصميم تطبيقك لاستخدام سلسلة التعليمات الرئيسية للحصول على أفضل أداء ممكن.

أصول داخلية

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

يرتبط أي حظر تقريبًا ينفّذه تطبيقك باستدعاء حدث، مثل الإدخال أو تضخيم التنسيقات أو الرسم. عندما يؤدي حدث ما إلى بدء حدث، تدفع سلسلة المحادثات التي وقعت فيها الحدث الحدث إلى الخروج من نفسها لتنتقل إلى قائمة انتظار رسالة سلسلة التعليمات الرئيسية. ويمكن لسلسلة التعليمات الرئيسية بعد ذلك معالجة الحدث.

أثناء عمل رسوم متحركة أو تحديث للشاشة، يحاول النظام تنفيذ جزء من العمل (المسؤول عن رسم الشاشة) كل 16 ملي ثانية تقريبًا، وذلك بهدف العرض بسلاسة على 60 إطارًا في الثانية. لكي يصل النظام إلى هذا الهدف، يجب تعديل التسلسل الهرمي لواجهة المستخدم/العرض في سلسلة التعليمات الرئيسية. ومع ذلك، عندما تحتوي قائمة انتظار الرسائل في سلسلة التعليمات الرئيسية على مهام كثيرة جدًا أو طويلة جدًا بحيث لا يمكن لسلسلة التعليمات الرئيسية إكمال التحديث بسرعة كافية، يجب أن ينقل التطبيق هذا العمل إلى سلسلة محادثات عامل. إذا لم تتمكن سلسلة التعليمات الرئيسية من إنهاء تنفيذ عمليات الحظر خلال 16 ملي ثانية، فقد يلاحظ المستخدم حدوث توقُّف أو تأخير أو عدم استجابة واجهة المستخدم لإدخال البيانات. إذا تم حظر سلسلة التعليمات الرئيسية لمدة خمس ثوانٍ تقريبًا، يعرض النظام مربع الحوار التطبيق لا يستجيب (ANR)، ما يسمح للمستخدم بإغلاق التطبيق مباشرةً.

نقل العديد من المهام الطويلة أو العديدة من سلسلة التعليمات الرئيسية، بحيث لا تتداخل مع العرض السلس والاستجابة السريعة للبيانات التي يدخلها المستخدم، هو السبب الأكبر لاستخدامك سلاسل التعليمات في تطبيقك.

سلاسل المحادثات ومراجع عناصر واجهة المستخدم

وحسب التصميم، لا تتوافق عناصر عرض Android View مع سلاسل المحادثات. من المتوقع أن ينشئ التطبيق كائنات واجهة المستخدم وتستخدمها وتدمرها، كل ذلك في سلسلة التعليمات الرئيسية. وإذا حاولت تعديل كائن واجهة المستخدم أو حتى الإشارة إليه في سلسلة تعليمات غير سلسلة التعليمات الرئيسية، قد تكون النتيجة استثناءات أو أعطال في الوضع الصامت أو أعطال أو أي سلوك آخر غير معروف.

تنقسم المشاكل المتعلقة بالمراجع إلى فئتَين مختلفتَين: المراجع الصريحة والمراجع الضمنية.

المراجع الفاضحة

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

على سبيل المثال، فكّر في تطبيق يحتوي على إشارة مباشرة إلى كائن واجهة مستخدم في سلسلة تعليمات عاملة. قد يتضمّن الكائن في سلسلة دالة العمل مرجعًا إلى View، ولكن قبل اكتمال العمل، تتم إزالة View من التدرّج الهرمي للعرض. عند تنفيذ هذين الإجراءَين في الوقت نفسه، يحتفظ المرجع بالكائن View في الذاكرة ويضبط الخصائص عليه. ومع ذلك، لا يرى المستخدم هذا الكائن مطلقًا، ويحذف التطبيق الكائن بعد اختفاء المرجع إليه.

في مثال آخر، تحتوي كائنات View على إشارات إلى النشاط الذي يملكها. إذا تم إتلاف هذا النشاط، ولكن ظل هناك كتلة عمل متسلسلة تشير إليه، بشكل مباشر أو غير مباشر، لن يجمع جامع البيانات المهملة الأنشطة حتى تنتهي مجموعة العمل هذه.

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

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

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

المراجع الضمنية

يمكن رصد عيب شائع في تصميم الرمز باستخدام الكائنات التي تتضمن سلاسل محادثات في مقتطف الرمز أدناه:

Kotlin

class MainActivity : Activity() {
    // ...
    inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
        override fun doInBackground(vararg params: Unit): String {...}
        override fun onPostExecute(result: String) {...}
    }
}

Java

public class MainActivity extends Activity {
  // ...
  public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

يكمن العيب في هذا المقتطف في أنّ الرمز يعرّف عن كائن سلسلة التعليمات MyAsyncTask كفئة داخلية غير ثابتة لبعض الأنشطة (أو فئة داخلية في Kotlin). ينشئ هذا البيان مرجعًا ضمنيًا لمثيل Activity المضمّن. ونتيجةً لذلك، يحتوي الكائن على مرجع إلى النشاط حتى يكتمل العمل المتسلسل، ما يتسبب في تأخير إيقاف النشاط المُشار إليه. ويؤدي هذا التأخير بدوره إلى زيادة حجم الذاكرة.

يتمثل الحل المباشر لهذه المشكلة في تحديد مثيلات الفئة المحملة بشكل زائد إما كفئات ثابتة أو في ملفاتها الخاصة، وبالتالي إزالة المرجع الضمني.

ويتوفّر حل آخر وهو إلغاء المهام التي يتم إجراؤها في الخلفية دائمًا وحذفها في معاودة الاتصال المناسبة خلال مراحل نشاط Activity، مثل onDestroy. ومع ذلك، يمكن أن يكون هذا النهج مملاً ومعرضًا للخطأ. كقاعدة عامة، لا ينبغي لك وضع منطق معقد بدون واجهة المستخدم مباشرةً في الأنشطة. بالإضافة إلى ذلك، تم إيقاف السمة AsyncTask نهائيًا ولا يُنصح باستخدامها في الرموز البرمجية الجديدة. راجِع Threading على Android للحصول على مزيد من التفاصيل حول إعدادات التزامن الأساسية المتاحة لك.

دورات حياة أنشطة التطبيقات وسلاسل المحادثات

يمكن أن تؤثر مراحل نشاط التطبيق في طريقة عمل سلاسل المحادثات في تطبيقك. قد تحتاج إلى تحديد أن سلسلة المحادثات يجب أو لا ينبغي أن تستمر بعد تدمير النشاط. يجب أيضًا أن تكون على دراية بالعلاقة بين تحديد أولويات سلسلة المحادثات وما إذا كان النشاط يعمل في المقدمة أو الخلفية.

سلاسل المحادثات المستمرة

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

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

إذا كانت عمليات الحظر ستؤدي إلى تحديث واجهة مستخدم لم تعد موجودة، فلا يوجد سبب لمواصلة العمل. على سبيل المثال، إذا كان العمل هو تحميل معلومات المستخدم من قاعدة بيانات، ثم تحديث طرق العرض، فلن تعد سلسلة التعليمات ضرورية.

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

قد تصبح إدارة استجابات مراحل النشاط يدويًا لجميع عناصر سلاسل المحادثات معقدة للغاية. إذا لم تفهم الإدارة بشكلٍ صحيح، يمكن أن يعاني تطبيقك من مشاكل في تزايد الذاكرة والأداء. يتيح لك دمج ViewModel مع LiveData تحميل البيانات وتلقّي إشعارات عندما تتغيّر بدون الحاجة إلى القلق بشأن مراحل النشاط. تعتبر كائنات ViewModel أحد الحلول لهذه المشكلة. يتم الاحتفاظ بنماذج ViewModels عبر تغييرات الضبط، ما يوفّر طريقة سهلة للاحتفاظ ببيانات العرض. للمزيد من المعلومات حول نماذج View، يُرجى الاطّلاع على دليل ViewModel. ولمعرفة المزيد من المعلومات حول LiveData، يمكنك الاطّلاع على دليل LiveData. إذا كنت تريد أيضًا الحصول على مزيد من المعلومات حول بنية التطبيق، يُرجى الاطّلاع على دليل بنية التطبيقات.

أولوية سلسلة المحادثات

كما هو موضّح في العمليات ودورة حياة التطبيق، تعتمد الأولوية التي تتلقّاها سلاسل محادثات تطبيقك بشكل جزئي على موضع التطبيق في مراحل نشاطه. أثناء إنشاء سلاسل المحادثات وإدارتها في تطبيقك، من المهم تحديد أولويتها لكي تحصل سلاسل المحادثات المناسبة على الأولويات المناسبة في الأوقات المناسبة. وإذا تم ضبط السياسة على مستوى عالٍ جدًا، قد تتقاطع سلسلة التعليمات مع مؤشر ترابط واجهة المستخدم وRenderThread، ما يؤدي إلى إسقاط تطبيقك للإطارات. وفي حال ضبطها على قيمة منخفضة جدًا، يمكنك جعل المهام غير المتزامنة (مثل تحميل الصور) أبطأ مما يجب.

في كل مرة تنشئ فيها سلسلة محادثات، عليك طلب الرقم setThreadPriority(). يعطي برنامج جدولة سلاسل الرسائل في النظام الأفضلية لسلاسل المحادثات ذات الأولويات العالية، ما يوازن بين تلك الأولويات والحاجة إلى إنجاز جميع الأعمال في نهاية المطاف. وبصفة عامة، تحصل سلاسل المحادثات في المجموعة التي تعمل في المقدّمة على حوالي 95% من إجمالي وقت التنفيذ من الجهاز، في حين تحصل مجموعة الخلفية على ما يقرب من 5%.

يعيّن النظام أيضًا قيمة الأولوية الخاصة لكل سلسلة محادثات باستخدام الفئة Process.

يضبط النظام تلقائيًا أولوية سلسلة المحادثات على الأولوية نفسها واشتراكات المجموعة مثل سلسلة المحادثات المنتجة. ومع ذلك، يمكن لتطبيقك تعديل أولوية سلاسل المحادثات بشكل صريح باستخدام setThreadPriority().

تساعد فئة Process في تقليل التعقيد في تعيين قيم الأولوية من خلال توفير مجموعة من الثوابت التي يمكن لتطبيقك استخدامها لتحديد أولويات سلاسل المحادثات. على سبيل المثال، يمثل THREAD_PRIORITY_DEFAULT القيمة التلقائية لسلسلة محادثات. يجب أن يضبط تطبيقك أولوية سلسلة المحادثات على THREAD_PRIORITY_BACKGROUND بالنسبة إلى سلاسل المحادثات التي تنفّذ عملاً أقل إلحاحًا.

يمكن لتطبيقك استخدام الثوابت THREAD_PRIORITY_LESS_FAVORABLE وTHREAD_PRIORITY_MORE_FAVORABLE كعوامل إضافية لتحديد الأولويات النسبية. للحصول على قائمة بأولويات سلسلة التعليمات، اطّلِع على الثوابت THREAD_PRIORITY في الفئة Process.

لمزيد من المعلومات حول إدارة سلاسل المحادثات، يُرجى الاطّلاع على المستندات المرجعية حول فئتَي Thread وProcess.

صفوف مساعدة لسلسلة المحادثات

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

يوفّر إطار العمل أيضًا فئات Java الأساسية نفسها لتسهيل إنشاء سلاسل التعليمات، مثل الفئات Thread وRunnable وExecutors، بالإضافة إلى الفئات الإضافية مثل HandlerThread. لمزيد من المعلومات، يُرجى الاطّلاع على سلسلة المحادثات على Android.

فئة HandlerThread

مؤشر ترابط المعالج هو بشكل فعال سلسلة طويلة الأمد تأخذ العمل من قائمة الانتظار وتعمل عليه.

هناك تحدٍّ شائع يتعلق بالحصول على إطارات معاينة من عنصر Camera. عند التسجيل للحصول على إطارات معاينة الكاميرا، ستتلقّى هذه الإطارات من خلال onPreviewFrame() رد الاتصال الذي يتم استدعاؤه في سلسلة محادثات الحدث الذي تم الاتصال منه. إذا تم استدعاء معاودة الاتصال هذه في مؤشر ترابط واجهة المستخدم، فإن مهمة التعامل مع صفيفات وحدات البكسل الضخمة ستتداخل في عمل العرض ومعالجة الأحداث.

في هذا المثال، عندما يفوّض تطبيقك الأمر Camera.open() إلى مجموعة عمل في سلسلة محادثات المعالج، يتم نقل استدعاء onPreviewFrame() المرتبط إلى سلسلة محادثات المعالج، بدلاً من سلسلة واجهة المستخدم. لذلك، إذا كنت تنوي تنفيذ عمل طويل الأمد على وحدات البكسل، قد يكون هذا حلاً أفضل لك.

عندما ينشئ تطبيقك سلسلة محادثات باستخدام HandlerThread، لا تنسَ ضبط أولوية سلسلة المحادثات بناءً على نوع العمل الذي تقوم به. تذكر أن وحدات المعالجة المركزية (CPU) يمكنها فقط التعامل مع عدد صغير من السلاسل بالتوازي. يساعد تحديد الأولوية النظام في معرفة الطرق الصحيحة لجدولة هذا العمل عندما تقاتل جميع سلاسل المحادثات الأخرى لجذب الانتباه.

فئة ThreadPoolExecutor

هناك أنواع معينة من الأعمال التي يمكن اختصارها إلى مهام متوازية وموزعة إلى حد كبير. إحدى هذه المهام، على سبيل المثال، حساب عامل تصفية لكل كتلة 8×8 من صورة بدقة 8 ميغابكسل. مع الحجم الهائل من حزم العمل التي ينشئها هذا، HandlerThread ليس الفئة المناسبة للاستخدام.

ThreadPoolExecutor هو صف مساعد لتسهيل هذه العملية. تدير هذه الفئة إنشاء مجموعة من سلاسل المحادثات، وتحدد أولوياتها، وتدير كيفية توزيع العمل بين هذه السلاسل. مع زيادة عبء العمل أو انخفاضه، يعمل الصف على تدوير المزيد من سلاسل المحادثات أو إتلافها للتكيّف مع عبء العمل.

ويساعد هذا الصف أيضًا تطبيقك في إنشاء عدد أمثل من سلاسل المحادثات. عند إنشاء عنصر ThreadPoolExecutor، يضبط التطبيق حدًا أدنى وحد أقصى لعدد سلاسل التعليمات. مع زيادة عبء العمل على ThreadPoolExecutor، سيأخذ الصف في الاعتبار الحد الأدنى والأقصى لعدد سلاسل المحادثات التي تم إعدادها، مع مراعاة حجم العمل المعلّق. بناءً على هذه العوامل، يحدّد ThreadPoolExecutor عدد سلاسل المحادثات التي يجب أن تكون متوفرة في أي وقت.

كم عدد سلاسل المحادثات التي يجب إنشاؤها؟

على الرغم من أنه على مستوى البرنامج، يمكن للرمز البرمجي إنشاء المئات من سلاسل التعليمات، إلا أنّ ذلك قد يؤدي إلى حدوث مشاكل في الأداء. يشارك تطبيقك موارد وحدة المعالجة المركزية (CPU) محدودة مع خدمات الخلفية والعارض ومحرك الصوت والشبكات وغير ذلك. لكن وحدات المعالجة المركزية (CPU) قادرة فقط على التعامل مع عدد صغير من سلاسل التعليمات بالتوازي، وكل ما سبق يدور بالنسبة إلى مشكلة الأولوية والجدولة. وبالتالي، من المهم إنشاء أكبر عدد ممكن من سلاسل الرسائل على النحو الذي يحتاجه عبء العمل.

ومن الناحية العملية، هناك عدد من المتغيرات المسؤولة عن ذلك، ولكن اختيار قيمة (مثل 4، كبداية) واختبارها باستخدام Systrace يُعدّ استراتيجية قوية مثل أي استراتيجية أخرى. يمكنك استخدام التجربة والخطأ لاكتشاف الحد الأدنى لعدد السلاسل التي يمكنك استخدامها دون الوقوع في مشكلات.

هناك اعتبار آخر عند اتخاذ قرار بشأن عدد سلاسل المحادثات المتوفرة، وهو أنّ سلاسل المحادثات ليست مجانية، لأنّها تستهلك مساحة من الذاكرة. تبلغ تكلفة كل سلسلة محادثات 64 كيلوبايت كحد أدنى من الذاكرة. ويتساهم ذلك بسرعة في العديد من التطبيقات المثبّتة على الجهاز، لا سيما في الحالات التي تزداد فيها حِزم المكالمات بشكلٍ كبير.

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