كيف يتم عرض شاشة رسالة VK؟

ماذا تفعل VK لتقليل تأخيرات العرض؟ كيفية عرض رسالة كبيرة جدًا وعدم قتل UiThread؟ كيف تقلل من تأخيرات التمرير في RecyclerView؟



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

لقد كنت برمجة لـ Android منذ ما يقرب من عشر سنوات ، كنت في السابق حرة لـ PHP / Node.js. الآن - أحد كبار مطوري Android VKontakte.

تحت المقطع - فيديو ونسخة لتقريري من مؤتمر Mobius 2019 في موسكو.


يكشف التقرير ثلاثة مواضيع



انظر الى الشاشة:


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

  • تواريخ ومؤشرات التحميل ،
  • رسائل الخدمة
  • نص (رمز تعبيري ، رابط ، بريد إلكتروني ، علامات تصنيف) ،
  • لوحة مفاتيح بوت
  • 40 طريقة لعرض المرفقات ،
  • شجرة الرسائل المعاد توجيهها.

السؤال الذي يطرح نفسه: كيف نجعل عدد التأخيرات أصغر ما يمكن؟ سواء في حالة الرسائل البسيطة ، وفي حالة الرسائل المجمعة (حالة حافة من الفيديو أعلاه).


الحلول القياسية


RecyclerView وإضافاته


هناك العديد من الوظائف الإضافية لـ RecyclerView.

  • setHasFixedSize ( boolean)

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

  • setNestedScrollingEnabled ( boolean)

تحسين ثانوي يعطل دعم NestedScroll. ليس لدينا CollapsingToolbar أو ميزات أخرى بناءً على NestedScroll على هذه الشاشة ، حتى نتمكن من ضبط هذه العلامة بأمان على false.

  • setItemViewCacheSize ( cache_size)

إعداد ذاكرة التخزين المؤقت الداخلية RecyclerView.

يعتقد الكثير من الناس أن آليات RecyclerView هي:

  • يوجد ViewHolder معروض على الشاشة ؛
  • هناك RecycledViewPool تخزين ViewHolder ؛
  • ViewHolder — RecycledViewPool.

عمليًا ، كل شيء أكثر تعقيدًا قليلاً ، لأن هناك ذاكرة وسيطة بين هذين الشيئين. يطلق عليه ItemViewCache. ما هو جوهره؟ عندما يترك ViewHolder الشاشة ، لا يتم وضعه في RecycledViewPool ، ولكن في ذاكرة التخزين المؤقت المتوسطة (ItemViewCache). تنطبق جميع التغييرات في المحول على كل من ViewHolder المرئي و ViewHolder داخل ItemViewCache. وبالنسبة لـ ViewHolder داخل RecycledViewPool ، لا يتم تطبيق التغييرات.

من خلال setItemViewCacheSize يمكننا ضبط حجم ذاكرة التخزين المؤقت الوسيطة.
كلما كان أكبر ، كلما كان التمرير أسرع عبر مسافات قصيرة ، لكن عمليات التحديث ستستغرق وقتًا أطول (بسبب ViewHolder.onBind ، وما إلى ذلك).

كيفية تنفيذ برنامج RecyclerView وكيفية تنظيم ذاكرة التخزين المؤقت الخاصة به هو موضوع كبير ومعقد إلى حد ما. يمكنك قراءة مقال رائعحيث يتحدثون بالتفصيل عن كل شيء.

التحسين OnCreate / OnBind


حل كلاسيكي آخر هو تحسين onCreateViewHolder / onBindViewHolder:

  • تخطيط سهل (نحاول استخدام FrameLayout أو Custom ViewGroup قدر الإمكان) ،
  • تتم العمليات الثقيلة (روابط التحليل / الرموز التعبيرية) بشكل غير متزامن في مرحلة تحميل الرسالة ،
  • StringBuilder لتنسيق الاسم والتاريخ وما إلى ذلك ،
  • والحلول الأخرى التي تقلل من وقت عمل هذه الأساليب.

Tracking Adapter.onFailedToRecyclerView ()




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

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



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



تتبع تجاوز ومحلل


الطريقة الكلاسيكية للكشف عن المشاكل هي الإفراط في التعقب وتتبع المحلل.

Overdraw - عدد عمليات إعادة رسم البكسل: كلما قل عدد الطبقات وأقل إعادة رسم البكسل ، كلما كانت أسرع. لكن بحسب ملاحظاتي ، في الحقائق الحديثة ، لا يؤثر ذلك كثيرًا على الأداء.



Profiler - المعروف أيضًا باسم Android Monitor ، الموجود في Android Studio. في ذلك ، يمكنك تحليل جميع الأساليب المطلوبة. على سبيل المثال ، افتح الرسائل ، وانتقل لأعلى ولأسفل وشاهد الطرق التي تم استدعاؤها والمدة التي استغرقتها.



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

النصف الصحيح هو رمزنا الذي يعمل في ViewHolder.

حظر المكالمة في الرقم 1 هو استدعاء للتعبيرات العادية: في مكان ما تم تجاهله ونسي وضع العملية على مؤشر ترابط الخلفية ، وبالتالي إبطاء التمرير بنسبة 20٪ تقريبًا.

حظر المكالمات على الرقم 2 - فريسكو ، مكتبة لعرض الصور. إنه ليس مثاليًا في بعض الأماكن ، ولم يتضح بعد ما يجب فعله مع هذا التأخير ، ولكن إذا تمكنا من حله ، فسوف نوفر 15٪ تقريبًا.

أي ، لحل هذه المشاكل ، يمكننا الحصول على زيادة بنسبة 35 ٪ تقريبًا ، وهو أمر رائع جدًا.

ديفيف


يستخدم العديد منكم DiffUtil في شكله القياسي: هناك قائمتان - تسمى التغييرات ، ومقارنتها ، ودفعها. القيام بكل هذا على الموضوع الرئيسي مكلف بعض الشيء لأن القائمة يمكن أن تكون كبيرة جدًا. لذلك عادةً ما يتم حساب DiffUtil على مؤشر ترابط في الخلفية.

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

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

الرسوم المتحركة


ذات مرة كان هناك إطار للرسوم المتحركة - إلى حد ما هزيل ، ولكن لا يزال شيء. لقد عملنا معه على هذا النحو:

view.startAnimation(TranslateAnimation(fromX = 0, toX = 300))

تكمن المشكلة في أن المعلمة getTranslationX () ستُرجع نفس القيمة قبل الحركة وبعدها. وذلك لأن الرسوم المتحركة غيرت التمثيل البصري ، لكنها لم تغير الخصائص الفيزيائية.



في Android 3.0 ، ظهر إطار Animator ، وهو أكثر صحة لأنه غير الخاصية المادية المحددة للكائن.



في وقت لاحق ، ظهر ViewPropertyAnimator ولا يزال الجميع لا يفهمون اختلافه عن Animator.

سوف أشرح. لنفترض أنك بحاجة إلى إجراء الترجمة بشكل قطري - قم بتحريك العرض على طول محاور x و y. على الأرجح ستكتب رمز نموذجي:

val animX = ObjectAnimator.ofFloat(view, “translationX”, 100f)
val animY = ObjectAnimator.ofFloat(view, “translationY”, 200f)
AnimatorSet().apply {
    playTogether(animX, animY)
    start()
}

ويمكنك جعلها أقصر:

view.animate().translationX(100f).translationY(200f) 

عند تنفيذ view.animate () ، فإنك تقوم ببدء تشغيل ViewPropertyAnimator ضمنًا.

لماذا هو مطلوب؟

  1. أسهل لقراءة التعليمات البرمجية والحفاظ عليها.
  2. الرسوم المتحركة للعمليات دفعة واحدة.

في حالتنا الأخيرة ، قمنا بتغيير خاصيتين. عندما نقوم بذلك من خلال الرسوم المتحركة ، سيتم استدعاء علامات الرسوم المتحركة بشكل منفصل لكل متحرك. بمعنى ، سيتم استدعاء setTranslationX و setTranslationY بشكل منفصل ، وسيقوم View بعمليات التحديث بشكل منفصل.

في حالة ViewPropertyAnimator ، يحدث التغيير في نفس الوقت ، لذلك هناك وفورات بسبب عمليات أقل ويتم تحسين التغيير في الخصائص نفسها بشكل أفضل.

يمكنك تحقيق ذلك باستخدام Animator ، ولكن سيكون عليك كتابة المزيد من التعليمات البرمجية. بالإضافة إلى ذلك ، باستخدام ViewPropertyAnimator ، يمكنك التأكد من أنه سيتم تحسين الرسوم المتحركة قدر الإمكان. لماذا ا؟ يحتوي Android على RenderNode (قائمة العرض). تقريبًا ، يقومون بتخزين نتائج onDraw في ذاكرة التخزين المؤقت ويستخدمونها عند إعادة الرسم. يعمل ViewPropertyAnimator مباشرة مع RenderNode ويطبق الرسوم المتحركة عليه ، وتجنب مكالمات onDraw.

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

الرسوم المتحركة: TransitionManager


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



إذا كان اللاعب جزءًا من القائمة ، فستكون الرسوم المتحركة بسيطة للغاية. لكن اللاعب معنا ليس عنصرًا في القائمة ، ولكنه عرض مستقل تمامًا.

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



باستخدام TransitionManager ، يمكنك جعل كل شيء أبسط:

TransitionManager.beginDelayedTransition(
        viewGroup = <LinearLayoutManager>,
        transition = AutoTransition())
playerView.visibility = View.GONE

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

حلول مخصصة




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

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



هناك وحدات عرض بسيطة - وهي فواصل التاريخ الكلاسيكية ، وتحميل المزيد ، وما إلى ذلك. و BaseViewHolder - ViewHolder الأساسية بشكل مشروط للرسالة. لديها تطبيق أساسي والعديد من التطبيقات المحددة - TextViewHolder و PhotoViewHolder و AudioViewHolder و ReplyViewHolder وما إلى ذلك. هناك حوالي 70 منهم.

ما هو المسؤول عن BaseViewHolder؟


BaseViewHolder مسؤول فقط عن رسم الصورة الرمزية والقطعة التي تريدها من الفقاعة ، بالإضافة إلى خط الرسائل المعاد توجيهها - الأزرق إلى اليسار.



يتم تنفيذ التطبيق الفعلي للمحتوى بواسطة ورثة BaseViewHolder الآخرين: TextViewHolder يعرض النص فقط ، FwdSenderViewHolder - مؤلف الرسالة المعاد توجيهها ، AudioMsgViewHolder - الرسالة الصوتية ، وما إلى ذلك.



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



في هذه الحالة ، استخدم محتويات اللف الكلاسيكية. بالنسبة للحالة المعقدة ، عندما تتكون الرسالة من عدة قطع ، فإننا نأخذ كل عرض ViewHolder ونفرضه بعرض ثابت. هنا بالتحديد - 220 ديسيبل.



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



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

انقسمنا إلى ViewHolders في مرحلة تحميل الرسائل: نبدأ تحميل الخلفية للرسالة ، ونحولها إلى عنصر ، ويتم عرضها مباشرة في ViewHolders.



Global RecycledViewPool


آليات استخدام رسولنا هي أن الناس لا يجلسون في نفس الدردشة ، ولكنهم يتنقلون باستمرار. في النهج القياسي ، عندما ذهبنا إلى الدردشة وتركناها ، يتم تدمير RecycledViewPool (و ViewHolder فيها) بكل بساطة ، وفي كل مرة ننفق فيها الموارد لإنشاء ViewHolder.

يمكن حل ذلك عن طريق RecycledViewPool العالمية:

  • في إطار التطبيق ، يعيش RecycledViewPool كأفراد.
  • إعادة استخدامها على شاشة الرسائل عندما يتنقل المستخدم بين الشاشات ؛
  • تعيين ك RecyclerView.setRecycledViewPool (تجمع).

هناك مطبات ، من المهم تذكر شيئين:

  • تذهب إلى الشاشة ، انقر مرة أخرى ، الخروج. المشكلة هي أن هؤلاء ViewHolders الذين كانوا على الشاشة يتم التخلص منهم ، ولا يتم إعادتهم إلى التجمع. تم إصلاح هذا على النحو التالي:
    LinearLayoutManager.recycleChildrenOnDetach = true
  • يحتوي RecycledViewPool على قيود: لا يمكن تخزين أكثر من خمسة ViewHolders لكل نوع ViewType.

إذا تم عرض 9 TextViews على الشاشة ، فسيتم إرجاع خمسة عناصر فقط إلى RecycledViewPool ، وسيتم التخلص من العناصر المتبقية. يمكنك تغيير حجم RecycledViewPool:
RecycledViewPool.setMaxRecycledViews (viewType ، الحجم)

ولكن من المحزن أن تكتب على كل ViewType بيديك ، لأنه يمكنك كتابة RecycledViewPool ، وتوسيع المستوى القياسي ، وجعله NoLimit. عن طريق الرابط يمكنك تحميل التنفيذ النهائي.

DiffUtil ليست مفيدة دائمًا


إليك حالة كلاسيكية - قم بتنزيل وتشغيل مسار صوتي ورسالة صوتية. في هذه الحالة ، يستدعي DiffUtil البريد العشوائي.



يحتوي BaseViewHolder الخاص بنا على طريقة تحديث مجردة.

abstract class BaseViewHolder : ViewHolder {
    fun updateUploadProgress(attachId: Int, progress: Float)
    …        
}

لإلقاء حدث ، نحتاج إلى تجاوز كل ViewHolder المرئي:

fun onUploadProgress(attachId: Int, progress: Float) {
    forEachActiveViewHolder {
        it.updateUploadProgress(attachId, progress)
    }
}

هذه عملية بسيطة ، من غير المحتمل أن يكون لدينا أكثر من عشرة ViewHolder على الشاشة. مثل هذا النهج لا يمكن أن يتأخر من حيث المبدأ. كيف تجد ViewHolder المرئي؟ التنفيذ الساذج سيكون مثل هذا:

val firstVisiblePosition = <...>
val lastVisiblePosition = <...>
for (i in firstVisiblePosition.. lastVisiblePosition) {
    val viewHolder = recycler.View.findViewHolderForAdapterPosition(i)
    viewHolder.updateUploadProgress(..)
}

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

class Adapter : RecyclerView.Adapter {
    val activeViewHolders = WeakSet<ViewHolder>()
        
    fun onBindViewHolder(holder: ViewHolder, position: Int) {
        activeViewHolders.add(holder)
    }

    fun onViewRecycled(holder: ViewHolder) {
        activeViewHolders.remove(holder)
    }
}

تراكب ViewHolder


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



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



هناك مشكلة: تم تقسيم رسالتنا إلى ViewHolder ، فهي تقع تحت بعضها البعض بشكل صارم ، ولكن هنا تتداخل. على الفور ليس من الواضح كيفية حل هذه المشكلة. يمكنك إنشاء ViewType آخر "history + sticker" أو "history + voice message". لذا ، بدلاً من 70 ViewType ، سيكون لدينا 140 ... لا ، نحن بحاجة إلى ابتكار شيء أكثر ملاءمة.



يتبادر إلى الذهن أحد عكازاتك المفضلة في Android. على سبيل المثال ، كنا نفعل شيئًا ، لكن Pixel Perfect لا يتقارب. لإصلاح ذلك ، تحتاج إلى حذف كل شيء والكتابة من الصفر ، ولكن الكسل. ونتيجة لذلك ، يمكننا أن نجعل الهامش = -2dp (سلبيًا) ، والآن أصبح كل شيء في مكانه. ولكن لا يمكن استخدام هذا النهج فقط هنا. إذا قمت بتعيين هامش سلبي ، فسوف يتحرك الملصق ، لكن المكان الذي يشغله سيظل فارغًا. ولكن لدينا ItemDecoration ، حيث يمكننا أن نبذل رقمًا سلبيًا. ويعمل! ونتيجة لذلك ، نحصل على التراكب المتوقع ، وفي نفس الوقت لا يزال هناك نموذج حيث يكون كل ViewHolder صديقًا.

حل جميل في سطر واحد.

class OffsetItemDecoration : RecyclerViewItemDecoration() {
    overrride fun getItemOffsets(offset: Rect, …) {
        offset.top = -100dp
    }
}

Idlehandler


هذه حالة بعلامة النجمة ، فهي معقدة ولا تحتاج إليها كثيرًا في الممارسة العملية ، ولكن من المهم معرفة وجود مثل هذه الطريقة.

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



في حقائقنا المعتادة ، يتحمل Handler مسؤولية وضع المهام في قائمة الانتظار ، ويتجاوز Looper إلى ما لا نهاية قائمة الانتظار. هناك مهام ليست مهمة جدًا لواجهة المستخدم. على سبيل المثال ، يقرأ المستخدم رسالة - ليس من المهم بالنسبة لنا عندما نعرضها على واجهة المستخدم ، الآن أو بعد 20 مللي ثانية. لن يلاحظ المستخدم الفرق. ثم ، ربما يجدر تشغيل هذه المهمة على الموضوع الرئيسي فقط عندما تكون مجانية؟ أي أنه سيكون من الجيد لنا أن نعرف متى يتم استدعاء سطر awaitNewTask. في هذه الحالة ، يحتوي Looper على addIdleHandler يتم تشغيله عند انطلاق المهام. isEmpty.

Looper.myQueue (). AddIdleHandler ()

وبعد ذلك سيبدو أبسط تطبيق لـ IdleHandler كما يلي:

@AnyThread
class IdleHandler {
    private val handler = Handler(Looper.getMainLooper())

    fun post(task: Runnable) {
        handler.post {
            Looper.myQueue().addIdleHandler {
                task.run()
                return@addIdleHandler false
            }
        }
    }
}

بنفس الطريقة ، يمكنك قياس بداية باردة صادقة للتطبيق.

رمز تعبيري


نستخدم الرموز التعبيرية المخصصة لدينا بدلاً من رموز النظام. في ما يلي مثال لكيفية ظهور الرموز التعبيرية على منصات مختلفة في سنوات مختلفة. الرموز التعبيرية اليمنى واليسرى جميلة جدًا ، ولكن في المنتصف ...



هناك مشكلة ثانية:



كل صف هو نفس الرمز التعبيري ، لكن العواطف التي يعيدون إنتاجها مختلفة. يعجبني الجزء السفلي الأيمن ، ما زلت لا أفهم ما يعنيه.

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

طريقة عمل الرموز التعبيرية: لدينا صورة نقطية كبيرة ، حيث يتم تجميعها جميعًا في "أطلس" كبير. هناك العديد منهم - تحت إدارة شؤون الإعلام المختلفة. وهناك EmojiSpan يحتوي على معلومات: أرسم الرموز التعبيرية "كذا وكذا" ، وهي في صورة نقطية كذا وكذا في موقع كذا وكذا (x ، y).
وهناك بديلبدلة تتيح لك عرض شيء ما بدلاً من النص تحت Span.
أي أنك تجد الرموز التعبيرية في النص ، وتلفها باستخدام EmojiSpan ، ويرسم النظام الرموز التعبيرية المطلوبة بدلاً من النظام.



البدائل


تضخم


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

يحتوي Android على فئة Debug ، ولديه startMethodTracing و stopMethodTracing.

Debug.startMethodTracing(“trace»)
inflate(...)
Debug.stopMethodTracing()

سيسمح لنا بجمع معلومات حول وقت تنفيذ جزء معين من التعليمات البرمجية.



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

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

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

أنكو


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

ليثو / رفرفة


لماذا جمعت بين شيئين لا علاقة لهما تمامًا؟ لأن هذا لا يتعلق بالتكنولوجيا ، ولكن حول تعقيد الهجرة إليها. لا يمكنك الاقتراض والانتقال إلى مكتبة جديدة.

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

يؤلف Jetpack


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

عرض مخصص واحد كبير


هناك نهج آخر يتحدث عنه أولئك الذين يستخدمون العديد من المراسلات الفورية: "خذ وكتب عرضًا مخصصًا كبيرًا ، ولا يوجد تسلسل هرمي معقد".

ما هي عيوبه؟

  • من الصعب الحفاظ عليها.
  • لا معنى له في الحقائق الحالية.

باستخدام Android 4.3 ، تم ضخ نظام التخزين المؤقت الداخلي داخل View. على سبيل المثال ، لا يتم استدعاء onMeasure إذا لم تتغير طريقة العرض. وتستخدم نتائج القياس السابق.

مع Android 4.3-4.4 ، ظهرت RenderNode (DisplayList) ، مما يجعل التخزين المؤقت. لنلقي نظرة على مثال. افترض أن هناك خلية في قائمة الحوارات: الصورة الرمزية ، العنوان ، العنوان الفرعي ، حالة القراءة ، الوقت ، صورة رمزية أخرى. بشروط - 10 عناصر. وكتبنا طريقة العرض المخصصة. في هذه الحالة ، عند تغيير خاصية واحدة ، سنعيد قياس جميع العناصر. أي إنفاق الموارد الإضافية فقط. في حالة ViewGroup ، حيث يكون كل عنصر عبارة عن طريقة عرض منفصلة ، عندما تقوم بتغيير طريقة عرض واحدة ، فإننا سنبطل طريقة عرض واحدة فقط (باستثناء عندما تؤثر طريقة العرض هذه على حجم العناصر الأخرى).

ملخص


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

من المفيد أيضًا زيادة كل شيء علىWorkerThread: تحليل الروابط ، DiffUtils - وبالتالي إلغاء تحميلUiThead قدر الإمكان.

يتيح لك RecycledViewPool العالمي التنقل بين شاشات الرسائل وعدم إنشاء ViewHolder في كل مرة.

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

, Mobius 2019 Piter , . , , SQLite, . Mobius 2020 Piter .

All Articles