عرض بطيء

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

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

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

تحديد إيقاف مؤقت لعرض واجهة المستخدم

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

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

الفحص البصري

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

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

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

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

Systrace

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

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

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

يحتوي مثال Systrace في الشكل 1 على المعلومات التالية لتحديد إيقاف مؤقت لعرض واجهة المستخدم:

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

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

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

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

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

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

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

لمزيد من المعلومات، اطّلِع على بدء استخدام Performance Monitoring على Android.

الإطارات المجمّدة

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

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

تتبُّع التشويش

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

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

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

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

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

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

أفضل الممارسات لتحديد أولويات أخطاء jank وحلّها

يُرجى مراعاة أفضل الممارسات التالية عند محاولة حلّ مشكلة إيقاف مؤقت لعرض واجهة المستخدم في تطبيقك:

  • تحديد حالات التشويش التي يمكن إعادة إنتاجها بسهولة وحلّها
  • تحديد أولويات أخطاء 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: notifyDataSetChanged()

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

على سبيل المثال، لنفترض أنّ هناك تطبيقًا يتلقّى إصدارًا جديدًا من قائمة بمحتوى إخباري من خادم. عند نشر هذه المعلومات في Adapter، يمكن استدعاء 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، لا سيما مع قائمة عمودية من قوائم التمرير الأفقي. ومن الأمثلة على ذلك شبكات التطبيقات في الصفحة الرئيسية من "متجر Google 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 في تجنُّب تكلفة التضخّم من خلال تنفيذ العمل مسبقًا أثناء عدم نشاط سلسلة UI. إذا لاحظت زيادة في عدد اللقطات خلال إطار ولم تلاحظها في قسم يحمل التصنيف RV Prefetch، تأكَّد من أنّك تجري الاختبار على جهاز متوافق وتستخدم إصدارًا حديثًا من Support Library. لا تتوفّر ميزة "الجلب المسبق" إلا على المستوى 21 من واجهة برمجة التطبيقات في نظام التشغيل Android 5.0 والإصدارات الأحدث.

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

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

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

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

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

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

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

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

ListView: Inflation

يمكنك إيقاف إعادة التدوير عن طريق الخطأ في 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، مثل ما يلي:

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

أداء العرض

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

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

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

إذا استغرق Record View#draw وقتًا طويلاً، من الشائع أن يتم رسم صورة نقطية على سلسلة واجهة المستخدم. يستخدم الرسم على صورة نقطية العرض المستند إلى وحدة المعالجة المركزية، لذا ننصحك بتجنُّب ذلك قدر الإمكان. يمكنك استخدام تتبُّع الطرق مع محلّل وحدة المعالجة المركزية (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) ثم يحمّلها إلى وحدة معالجة الرسومات (GPU). إذا كانت لديك مسارات كبيرة، تجنَّب تعديلها من إطار إلى آخر، حتى يمكن تخزينها مؤقتًا ورسمها بكفاءة. تتسم 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,