عرض بطيء

عرض واجهة المستخدم هو إنشاء إطار من تطبيقك وعرضه على الشاشة. للمساعدة في ضمان أن يكون تفاعل المستخدم مع التطبيق سلسًا، يجب أن يعرض تطبيقك الإطارات في أقل من 16 ملي ثانية للوصول إلى 60 لقطة في الثانية (لقطة في الثانية). لفهم سبب تفضيل استخدام 60 إطارًا في الثانية، يمكنك الاطّلاع على أنماط أداء Android: لماذا يتم استخدام 60 لقطة في الثانية؟. وإذا كنت تحاول تسجيل 90 لقطة في الثانية، ينخفض حجم هذه النافذة إلى 11 ملي ثانية، وبالنسبة إلى 120 لقطة في الثانية يكون حجمها 8 ملي ثانية.

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

إذا كنت تطوّر ألعابًا لا تستخدم نظام View، يعني ذلك أنّك تتجاوز Choreographer. في هذه الحالة، تساعد مكتبة Frame Pacing ألعاب OpenGL وVulkan على توفير عرض سلس ووتيرة صحيحة للإطار على Android.

للمساعدة في تحسين جودة التطبيق، يراقب نظام التشغيل Android تطبيقك تلقائيًا بحثًا عن البيانات غير المرغوب فيها ويعرض المعلومات في لوحة بيانات "مؤشرات Android الحيوية". للحصول على معلومات حول طريقة جمع البيانات، راجِع مراقبة الجودة الفنية لتطبيقك باستخدام وظائف Android الحيوية.

التعرّف على مشكلة البيانات غير المحتملة

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

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

الفحص البصري

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

في ما يلي بعض النصائح لإجراء فحوصات بصرية:

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

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

سيستراس

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

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

مثال على Systrace
الشكل 1. مثال على Systrace.

يحتوي مثال Systrace في الشكل 1 على المعلومات التالية لتحديد البيانات غير المحتملة:

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

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

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

لمزيد من المعلومات، يُرجى الاطّلاع على فهم Systrace.

تتبُّع الأداء المخصّص

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

لإجراء ذلك، يجب جمع أوقات عرض اللقطات من أجزاء معيّنة في تطبيقك باستخدام FrameMetricsAggregator وتسجيل البيانات وتحليلها باستخدام مراقبة الأداء في Firebase.

لمزيد من المعلومات، راجع بدء استخدام مراقبة الأداء لنظام التشغيل Android.

إطارات ثابتة

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

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

تُعد الإطارات المجمّدة شكلاً متشدّدًا من العرض البطيء، وبالتالي فإنّ إجراءات تشخيص المشكلة وإصلاحها هي نفسها.

البيانات غير المحتملة للتتبع

يمكن أن يساعد FrameTimeline في Perfetto في تتبُّع الإطارات البطيئة أو المجمدة.

العلاقة بين الإطارات البطيئة والإطارات الثابتة وأخطاء ANR

الإطارات البطيئة والإطارات الثابتة وأخطاء ANR جميعها أشكال مختلفة من البيانات غير المحتملة التي قد يواجهها تطبيقك. اطّلِع على الجدول التالي لمعرفة الفرق.

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

تتبُّع اللقطات البطيئة واللقطات الثابتة بشكل منفصل

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

أفضل الممارسات لتحديد الأولوية وحل مشكلة البيانات غير المحتملة

ضع في اعتبارك أفضل الممارسات التالية عند البحث عن حل مشكلة البيانات غير المحتملة في تطبيقك:

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

إصلاح مشكلة البيانات غير المحتملة

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

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

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

المصادر الشائعة للبيانات غير المحتملة

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

القوائم القابلة للتمرير

تُستخدم ListView، وخاصةً RecyclerView، بشكل شائع لقوائم التمرير المعقّدة الأكثر عرضة للاختراق. وكلا الخدمتين يحتويان على علامات Systrace، لذلك يمكنك استخدام Systrace لمعرفة ما إذا كانت تساهم في إيقاف أخطاء في تطبيقك أم لا. لعرض وسيطة سطر الأوامر -a <your-package-name> للحصول على أقسام التتبع في RecyclerView، بالإضافة إلى أي علامات تتبّع أضفتها. اتّبِع إرشادات التنبيهات التي تم إنشاؤها في مخرجات نظام التشغيل، إن توفّرت. في Systrace، يمكنك النقر على الأقسام المتتبّعة RecyclerView للاطّلاع على توضيح للعمل الذي يجريه RecyclerView.

RecyclerView: notificationsDataSetChanged()

إذا لاحظت ارتداد كل عنصر في RecyclerView، وبالتالي تمت إعادة تركيبه وإعادة رسمه في إطار واحد، تأكَّد من عدم الاتصال بـ notifyDataSetChanged() أو setAdapter(Adapter) أو swapAdapter(Adapter, boolean) للحصول على التحديثات الصغيرة. تشير هذه الطرق إلى أن هناك تغييرات في محتوى القائمة بالكامل وتظهر في Systrace على أنه RV FullSaveate بدلاً من ذلك، استخدِم SortedList أو DiffUtil لإجراء تعديلات على الأقل عند تغيير المحتوى أو إضافته.

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

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

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

نقترح عليك استخدام DiffUtil، التي تحسب الحد الأدنى من التحديثات وترسله لك:

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

لإبلاغ DiffUtil بكيفية فحص قوائمك، يُرجى تحديد MyCallback كعملية تنفيذ Callback.

RecyclerView: مكوّنات RecyclerView المتداخلة

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

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

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

إذا كنت تريد إجراء المزيد من التحسينات، يمكنك أيضًا استدعاء setInitialPrefetchItemCount(int) على LinearLayoutManager الداخلية RecyclerView. على سبيل المثال، إذا كان لديك دائمًا 3.5 عناصر مرئية في صف واحد، يمكنك استدعاء innerLLM.setInitialItemPrefetchCount(4). يشير هذا إلى RecyclerView أنّه عندما يوشك صف أفقي على الظهور على الشاشة، يجب أن يحاول جلب العناصر مسبقًا في الداخل إذا كان هناك وقت فراغ في سلسلة محادثات واجهة المستخدم.

RecyclerView: استغراق مقدار كبير جدًا من التضخم أو الإنشاء وقتًا طويلاً جدًا

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

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

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

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

RecyclerView: تستغرق عملية الربط وقتًا طويلاً جدًا

يجب أن يكون الربط، أي onBindViewHolder(VH, int)، مباشرًا ويستغرق أقل من مللي ثانية لكل العناصر ما عدا العناصر الأكثر تعقيدًا. يجب أن يأخذ عناصر كائن Java القديم (POJO) العادي من بيانات العناصر الداخلية للمحوِّل وأدوات ضبط الاستدعاءات في طرق العرض في ViewHolder. إذا كانت RV OnSelectView تستغرق وقتًا طويلاً، تأكّد من أنّك تبذل جهدًا بسيطًا في رمز الربط.

إذا كنت تستخدم عناصر POJO الأساسية للاحتفاظ بالبيانات في المحوِّل، يمكنك تجنُّب كتابة رمز الربط في onBindViewHolder تمامًا باستخدام مكتبة ربط البيانات.

RecyclerView أو ListView: يستغرق التخطيط أو الرسم وقتًا طويلاً جدًا

بالنسبة إلى المشاكل المتعلّقة بالرسم والتنسيق، يُرجى الاطّلاع على القسمَين أداء التنسيق وأداء العرض.

ListView: التضخم

يمكنك إيقاف ميزة إعادة التدوير عن طريق الخطأ في ListView إذا لم تكن حريصًا. إذا لاحظت تضخمًا في كل مرة يظهر فيها منتج على الشاشة، تأكّد من أنّ تنفيذ Adapter.getView() يعدّل معلَمة convertView وتعيد ربطها وعرضها. إذا كان تنفيذ getView() متضخمًا دائمًا، لن يحصل تطبيقك على مزايا إعادة التدوير في ListView. يجب أن تكون بنية getView() مشابهة في جميع الأوقات لطريقة التنفيذ التالية:

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

أداء التصميم

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

أداء التنسيق: التكلفة

إذا كانت مدة القطع أطول من بضعة ملي ثانية، من المحتمل أن تكون قد سجّلت أسوأ حالة تداخل في أداء RelativeLayouts أو weighted-LinearLayouts. يمكن أن يؤدي كل تنسيق من هذه التنسيقات إلى تشغيل عدة مقاييس وتمريرات تصميم لعناصره الثانوية، وبالتالي قد يؤدي دمجها إلى سلوك O(n^2) في ما يتعلق بعمق التداخل.

حاوِل تجنُّب RelativeLayout أو ميزة الوزن الخاصة بـ LinearLayout في كل العقد الطرفية باستثناء أدنى الأجزاء في التسلسل الهرمي. وفي ما يلي طُرق لتنفيذ ذلك:

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

أداء التنسيق: معدّل التكرار

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

بشكل عام، يجب تشغيل الصور المتحركة من خلال خصائص الرسم View، مثل ما يلي:

يمكنك تغيير كل هذه العناصر بتكلفة أقل بكثير من خصائص التنسيق، مثل المساحة المتروكة أو الهوامش. وبشكل عام، من المفيد أيضًا تغيير خصائص رسم طريقة العرض من خلال استدعاء دالة setter تؤدي إلى تشغيل invalidate()، متبوعةً بـ draw(Canvas) في الإطار التالي. يعيد هذا تسجيل عمليات الرسم للعرض المُلغى وهو أيضًا أقل تكلفة بشكل عام من التخطيط.

أداء العرض

تعمل واجهة مستخدم Android على مرحلتَين:

  • تسجيل View#draw في سلسلة محادثات واجهة المستخدم، والتي تستخدم draw(Canvas) في كل عرض غير صالح، ويمكنها استدعاء استدعاءات في طرق عرض مخصصة أو في رمزك.
  • DrawFrame على RenderThread، الذي يعمل على النظام الأصلي RenderThread ولكنه يعمل بناءً على العمل الذي أنتجته مرحلة Record View#draws.

أداء العرض: سلسلة محادثات واجهة المستخدم

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

غالبًا ما يتم الرسم على صورة نقطية عندما يريد أحد التطبيقات تزيين صورة نقطية قبل عرضها، وأحيانًا يكون ذلك بمثابة زينة مثل إضافة زوايا دائرية:

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

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

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

يمكنك استبداله بما يلي:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

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

إذا كنت ترسم صورة نقطية لسبب آخر، وقد يتم استخدامها كذاكرة تخزين مؤقت، حاوِل جذب رمز Canvas الذي تم تسريعه بالأجهزة التي يتم تمريرها إلى View أو Drawable مباشرةً. وإذا لزم الأمر، ننصحك أيضًا باستدعاء setLayerType() باستخدام LAYER_TYPE_HARDWARE تخزين ناتج العرض المعقد في ذاكرة التخزين المؤقت مع الاستفادة من عرض وحدة معالجة الرسومات.

أداء العرض: RenderThread

تسجيل بعض عمليات Canvas رخيص ولكنه يؤدي إلى عمليات حسابية باهظة الثمن على RenderThread. يستدعي Systrace بشكل عام ذلك باستخدام التنبيهات.

تحريك المسارات الكبيرة

عند استدعاء Canvas.drawPath() على Canvas الذي يتم تسريعه بالأجهزة والتي يتم تمريرها إلى View، يستعين Android بهذه المسارات أولاً على وحدة المعالجة المركزية (CPU) ويحمّلها إلى وحدة معالجة الرسومات. إذا كانت لديك مسارات كبيرة، فتجنب تعديلها من إطار إلى آخر، بحيث يمكن تخزينها مؤقتًا ورسمها بكفاءة. تُعد drawPoints() وdrawLines() وdrawRect/Circle/Oval/RoundRect() أكثر كفاءة وأفضل من حيث الاستخدام حتى إذا كنت تستخدم المزيد من مكالمات الرسم.

مسار Canvas.clipPath

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

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

بدلاً من ذلك، عبِّر عن المثال السابق على النحو التالي:

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
تحميلات الصور النقطية

يعرض Android الصور النقطية على هيئة زخارف OpenGL، وفي أول مرة يتم فيها عرض صورة نقطية في إطار، يتم تحميلها إلى وحدة معالجة الرسومات. يمكنك الاطّلاع على ذلك في Systrace على النحو التالي: عرض(id) تحميل الهيئة × الارتفاع. قد يستغرق ذلك عدة ميلي ثانية، كما هو موضح في الشكل 2، ولكن من الضروري عرض الصورة مع وحدة معالجة الرسومات.

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

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

يقضي التطبيق وقتًا طويلاً في
  إطار لتحميل صورة نقطية كبيرة
الشكل 2. يقضي التطبيق وقتًا طويلاً في إطار لتحميل صورة نقطية كبيرة. ويمكنك إما تقليل حجمها أو تشغيلها مبكرًا عند فك ترميزها باستخدام prepareToDraw().

تأخير في جدولة سلسلة المحادثات

أداة جدولة سلاسل المحادثات هي جزء من نظام التشغيل Android مسؤول عن تحديد سلاسل المحادثات في النظام التي يجب تشغيلها ووقت تشغيلها ومدة تشغيلها.

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

يؤدي هذا الخيار إلى تحديد الفترة التي تكون فيها سلسلة واجهة المستخدم في وضع السكون.
الشكل 3. تمييز الفترة التي تكون فيها سلسلة واجهة المستخدم في وضع السكون.

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

إذا كانت لديك معاملات حافظة، يمكنك تسجيل حزم المكالمات باستخدام أوامر adb التالية:

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

في بعض الأحيان، يمكن أن تؤدي المكالمات التي تبدو غير مبررة، مثل getRefreshRate()، إلى تحفيز المعاملات وحدوث مشاكل كبيرة عند استدعائها بشكل متكرّر. يمكن أن يساعدك التتبع دوريًا في العثور على هذه المشكلات وإصلاحها عند ظهورها.

تعرض سلسلة محادثات واجهة المستخدم وهي نائمة بسبب معاملات الصنف
  في مركبة ترفيهية. عليك التركيز على منطق الربط واستخدام trace-ipc لتتبُّع طلبات الصنف Binder وإزالتها.
الشكل 4. سلسلة واجهة المستخدم في وضع السكون بسبب معاملات الصنف في مركبة ترفيهية. أبقِ منطق الربط بسيطًا واستخدِم trace-ipc لتتبُّع طلبات الصنف Binder وإزالتها.

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

تخصيص العناصر وجمع البيانات غير المرغوب فيها

تُعد مشاكل تخصيص العناصر وجمع البيانات غير المرغوب فيها (GC) أقل بكثير من المشاكل حيث تم تقديم ART كوقت التشغيل التلقائي في Android 5.0، ولكن لا يزال من الممكن تقييم سلاسل المحادثات من خلال هذا العمل الإضافي. من الجيد التخصيص استجابةً لحدث نادر لا يحدث عدة مرات في الثانية، مثل نقر المستخدم على زرّ، ولكن تذكَّر أنّ كل عملية تخصيص لها تكلفة. إذا كانت الشبكة في حلقة مغلقة يتم استدعائها بشكل متكرر، ننصحك بتجنّب التخصيص لتخفيف الحمل على تجميع البيانات المهملة.

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

إظهار GC لمدة 94 ملي ثانية على HeapTaskDaemon
الشكل 5. تجميع البيانات المهملة بحجم 94 ميلي ثانية في سلسلة HeapTaskDaemon.

في الإصدارات الحديثة من نظام التشغيل Android، يتم تشغيل تجميع البيانات المهملة بشكل عام على سلسلة محادثات في الخلفية باسم HeapTaskDaemon. تعني الكميات الكبيرة من التخصيص إنفاق المزيد من موارد وحدة المعالجة المركزية (CPU) على تجميع البيانات المهملة، كما هو موضّح في الشكل 5.