عرض بطيء

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

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

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

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

تحديد مشكلة التخزين

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

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

الفحص البصري

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

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

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

عند العثور على حالات استخدام تؤدي إلى البيانات غير المحتملة، قد تكون لديك فكرة جيدة عن سبب الخلل في تطبيقك. وإذا كنت بحاجة إلى مزيد من المعلومات، يمكنك استخدام 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، إذا توفرت. في Systrace، يمكنك النقر على RecyclerViewالأقسام المتتبّعة للاطّلاع على شرح للعمل الذي يجريه RecyclerView.

RecyclerView: notificationsDataSetChanged()

إذا لاحظت أنّ كل عنصر في RecyclerView يتم ارتداده، وبالتالي تتم إعادة بنائه وإعادة رسمه في إطار واحد، يُرجى التأكّد من عدم الاتصال بـ notifyDataSetChanged() أو setAdapter(Adapter) أو swapAdapter(Adapter, boolean) إجراء تعديلات بسيطة. تشير هاتان الطريقتان إلى أنّ هناك تغييرات في محتوى القائمة بالكامل وتظهران في Systrace كـ RV AllInvalidate. بدلاً من ذلك، استخدِم 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 OnAndView وقتًا طويلاً، تأكّد من أنّك تنفّذ الحد الأدنى من العمل في رمز الربط.

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

RecyclerView أو 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 على مرحلتَين:

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

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

إذا استغرقت عملية تسجيل العرض#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) تحميل الهيئة x الارتفاع. قد يستغرق ذلك عدة مللي ثانية، كما هو موضح في الشكل 2، ولكن من الضروري عرض الصورة مع وحدة معالجة الرسومات.

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

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

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

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

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

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

يميّز فترة تكون فيها سلسلة واجهة المستخدم في وضع السكون
الشكل 3. تمييز فترة تكون فيها سلسلة واجهة المستخدم في وضع السكون.

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

إذا كانت لديك معاملات بين الصناديق، يمكنك تسجيل حِزم المكالمات باستخدام أوامر 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 وإزالتها.

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

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

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

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

تعرض رمز GC لمدة 94 ملي ثانية على HeapTaskDaemon
الشكل 5. رسالة GC مدتها 94 ملي ثانية على سلسلة HeapTaskDaemon

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