الأدوات والأداء

تفوَّق على نفسك في Compose

سماعات

لمحة

لمحة عن المحاضرة

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

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

ملخّص

  • تسجّل عمليات تتبُّع Perfetto النشاط على الجهاز وتعرض أجزاء Compose، مثل التركيب والتنسيق، على طول سلسلة التعليمات الرئيسية. استخدِمها لفحص المخططات الزمنية للإطارات وتتبُّع مُدد المراحل.
  • كل عملية داخل الدوال المركّبة لها تكلفة، بما في ذلك تشغيل الكوروتين وإلغاؤه. راجِع واجهات برمجة التطبيقات الخاصة بالآثار الجانبية وفكِّر في دمج التأثيرات التي تتضمّن مفاتيح متطابقة لتقليل عبء الإنشاء.
  • يضيف تتبُّع الإصدار تعليقات توضيحية إلى الدوال المركّبة، بينما يتتبّع تتبُّع إعادة الإصدار (الإصدار 1.13 والإصدارات الأحدث) عمليات قراءة الحالة وكتابتها التي تؤدي إلى إعادة الإصدار. وتساعد في تشخيص العمل غير المتوقّع مباشرةً ضمن عمليات تتبُّع Perfetto.
  • يتجنّب ComputedState (الإصدار 1.13 من Compose والإصدارات الأحدث) عمليات الإبطال غير المرغوب فيها، وهو عادةً أسرع من DerivedState أثناء عمليات الإبطال من نوع "القراءة بعد الكتابة". تظل DerivedState مفيدة عندما تستفيد العمليات الحسابية أو عمليات التخصيص المكلفة من التخزين المؤقت.
  • يؤدي تخطّي الدوال المركّبة إلى تحمُّل تكاليف الصيانة وأعباء مقارنة المَعلمات. اعتمد على ميزة التخطّي القوي ومساواة هوية العناصر، أو تجنَّب عمليات الإبطال تمامًا، بدلاً من فرض الثبات.

المهارات

الموارد

Gemini Notebook

المواضيع

  • jetpack-compose
  • android-performance
  • perfetto
  • قياس الأداء
  • كوروتين
  • composition-tracing
  • android-cli

النص

تنزيل النص

مقدمة

  • مرحبًا

  • مرحبًا،

  • آمل أنّك استمتعت بوقت راحتك.

  • آمل أنّك تناولت قهوتك، وأنّك في حالة جيدة.

  • سنتعمّق اليوم في تفاصيل هذه الميزة.

  • حسنًا، نعم، ها نحن ذا.

  • سنتعمّق أكثر اليوم.

  • اسمي "أندريه شيكوف"، ويشاركني اليوم "أندرو بيلي".

  • نعمل معًا في فريق وقت تشغيل برنامج تجميع Compose، بالإضافة إلى العديد من المهام الأخرى، بما في ذلك الأداء.

  • موضوع حديثنا اليوم هو الإجابة عن أسئلتكم حول الأداء، لأنّ Jetpack Compose أصبحت مجموعة أدوات واجهة المستخدم التلقائية لتطوير تطبيقات Android.

  • والآن، يتساءل الكثيرون عن كيفية التأكّد من أنّ واجهة مستخدم Compose تعمل بأفضل أداء ممكن.

  • لذا، سنتعمّق اليوم في نظرة متعمّقة، كما ذكرت سابقًا.

  • وسنقدّم لك أيضًا بعض الأدوات التي تساعدك في تحسين أدائك في Compose.

  • أولاً، أريد أن أتحدث عن سبب أهمية الأداء بالنسبة إلينا.

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

  • لذا، إذا كانت الصور المتحركة متقطعة، ينخفض مستوى الإدراك على الفور.

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

  • لذا، فإنّ تحسين تطبيقك هو في الأساس تحسين لتجربة المستخدم.

  • هذه هي العقلية التي أريدكم أن تتبنّوها بينما نتعمّق في الكثير من المواضيع اليوم.

  • الآن، عندما تبدأ في النظر إلى أداء تطبيقك، أريدك أن تفعل ثلاثة أشياء سريعة، ثلاثة أشياء سريعة.

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

  • ثانيًا، تفعيل R8 أو أدوات التصغير الأخرى

  • في هذا العام، قدّمنا تحسينًا يضاعف أداء الروتين المشترك، ونقوم أيضًا بالكثير من الأمور الأخرى.

  • نعتمد كثيرًا على R8 عند تحسين Compose.

  • وأخيرًا، بالنسبة إلى مقاييس بدء التشغيل تحديدًا، يجب التأكّد من استخدام ملفات التعريف الأساسية.

  • وبالتالي، يتم تجميع الفئات التي يتم تحميلها في بدء التشغيل مسبقًا وتحميلها مسبقًا عند بدء تشغيل هذا التطبيق.

  • كما ترون، هذا يمثّل فرقًا بمقدار 6 أضعاف تقريبًا عن الإصدار المعتاد من التطبيق القابل للتصحيح.

  • وعند قياس كل هذه الأمور، ستتراكم مع الإنجازات الأخرى التي تحقّقها في الرمز البرمجي.

  • الآن، لقد فعلت كل ذلك، ولا يزال التطبيق متأخرًا قليلاً، أو لا تزال هناك مشكلات أخرى في الأداء، وهذا هو المكان الذي تريد الدخول فيه إلى حلقة الأداء هذه.

  • تريد البدء في النظر في بعض المقاييس في بعض السيناريوهات المحددة.

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

  • أو ربما يتلعثم التطبيق قليلاً، لذا تقوم بالتمرير، وتتأكد من أن هذا التمرير قابل للتكرار ومستقر.

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

  • تُعدّ مقاييس الأداء مفيدة جدًا في هذا الصدد، لأنّها تسجّل الكثير من البيانات، وتكرّر ذلك عدة مرات للتأكّد من عدم وجود الكثير من التشويش في جهازك، كما أنّها تُجري الكثير من عمليات إعداد الجهاز التي تساعدك في ذلك.

  • وتكون مقاييس الأداء أيضًا سيناريوهات قابلة للتكرار تلقائيًا، وهذا ما نسعى إليه هنا.

  • ولكن يمكنك أيضًا القيام بذلك باستخدام مجموعة من النصوص البرمجية إذا أردت.

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

تحديد المواصفات الشخصية لصاحب البيانات باستخدام عمليات تتبُّع Perfetto

  • عند الاطّلاع على البيانات، فإنّ الأداة الأولى التي نقدّمها اليوم والتي نريد أن تتعرّف عليها جيدًا هي تتبُّعات Perfetto.

  • تسجّل تتبُّعات Perfetto كل ما يحدث على الجهاز أثناء تسجيل هذا التتبُّع، على سبيل المثال، أثناء تشغيل مقياس الأداء الكلي أو ربما بعض النصوص البرمجية التي تسجّلها.

  • وهي مفيدة جدًا لكل مهندس يعمل على تحسين أداء Android.

  • بالنسبة إليّ، أقضي معظم وقت عملي اليومي في الاطّلاع على تتبُّعات Perfetto هذه الأيام.

  • وتبدو على النحو التالي:

  • هذا قدر كبير من البيانات.

  • كما قلت، فإنها تلتقط كل ما يحدث على الجهاز.

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

  • ابحث عن Compose، وابحث عن حزمة تطبيقك، وأشياء من هذا القبيل.

  • ما عليك سوى التركيز على الأشياء المألوفة.

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

  • إذا عزلنا أحدها، فسترى شيئًا كهذا.

  • الأول هو عمل سلسلة التعليمات الرئيسية.

  • هنا ينفّذ Compose معظم مهامه.

  • بعد ذلك، هناك RenderThread.

  • هنا تتم معالجة أمر لوحة الرسم وترميزه إلى وحدة معالجة الرسومات.

  • بعد ذلك، تؤدي وحدة معالجة الرسومات عملها، ثم يدمج النظام ذلك.

  • سنلقي نظرة على سلسلة التعليمات الرئيسية فقط اليوم.

  • لأن هذا هو المكان الذي يقوم فيه Compose بعمله.

  • بما أنّنا نتحدّث عن Compose، من المنطقي التركيز عليه فقط.

  • وعندما نعزل هذا الإطار الواحد على سلسلة التعليمات الرئيسية، نبدأ فعليًا في رؤية شرائح Compose الآن.

  • اخترتُ لك بعضًا منها.

  • إنّه مثل التكوين والقياس والتخطيط والرسم.

  • إذا سمعت عن مراحل Compose، فستراها على الأرجح عند الاطّلاع على الأداء.

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

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

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

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

  • والآن، سيتحدث "أندرو" عن ذلك.

  • شكرًا، أندريه.

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

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

  • لذا، سنتناول اليوم ما تبقّى في التركيب بعد إجراء هذه التحسينات، وسنتناول ما تفعله الدوال المركّبة نفسها.

  • الحقيقة المؤسفة لكل شيء داخل رمز Compose الخاص بك، وكل شيء تشحنه مع تطبيقك فيما يتعلق بالأداء، هو أن كل ذلك له تكلفة مرتبطة به.

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

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

  • إذا أردت إحداث تأثير في أدائك، لديك خياران.

  • يمكنك إزالة عدد كبير جدًا من العمليات غير المكلفة، أو عدد أقل من العمليات الأكثر تكلفة.

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

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

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

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

  • لذلك، من المهم الانتباه إلى كلتا التكلفتَين.

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

  • لدينا هنا بعض الميزانية التي نحتاج إلى تقليصها، وستكون الكوروتين مثالاً جيدًا للبدء به.

إدارة الآثار الجانبية

  • في Compose، تظهر الكوروتين في أغلب الأحيان مع واجهات برمجة التطبيقات الخاصة بالتأثيرات.

  • هناك مجموعة من واجهات برمجة التطبيقات التي يمكنك استخدامها مع SideEffects، بدءًا من الوظائف العالية إلى وقت الاستجابة المنخفض.

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

  • إذا لم تكن بحاجة إلى ذلك، ويمكنك إنجاز مهمتك باستخدام واجهة برمجة تطبيقات أقل تكلفة، قد يكون من المفيد استخدامها بدلاً من ذلك إذا كانت تناسب احتياجات حالة الاستخدام لديك.

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

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Asynchronous work and animations
        LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
    }
  • يمكننا البدء بـ LaunchedEffect.

  • في هذا المثال، يتم استخدامها لتحميل البيانات بشكل غير متزامن داخل مشغل الوسائط.

  • إذا كنت بحاجة إلى نطاق دالة كوروتين، أو إذا كنت بحاجة إلى دوال كوروتين بشكل عام أو لإجراء مكالمة معلّقة، استخدِم LaunchedEffect.

  • على الرغم من أنّها لا تزال باهظة الثمن وهناك تكلفة مرتبطة بالروتينات المساعدة، طالما أنّك تستفيد منها وتبرّرها، استخدِم واجهة برمجة التطبيقات المناسبة للمهمة.

  • ولكن إذا وجدت نفسك في موقف تستخدم فيه LaunchedEffect، ولكنك لا تستفيد من الكوروتين، فقد يكون من الجيد التفكير في واجهات برمجة تطبيقات أخرى يمكنها القيام بالشيء نفسه، ولكن بتكلفة أقل.

  • لذلك، بالانتقال إلى أسفل الطيف، يمكننا رؤية أمثلة أخرى على الآثار الجانبية مثل DisposableEffect.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val activity = LocalActivity.current
    
        // Setup and teardown
        DisposableEffect(Unit) {
            activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
            onDispose {
                activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
            }
        }
    }
  • تُعدّ دالة DisposableEffect مفيدة عندما تكون هناك عملية إعداد وإزالة مرتبطة بالتأثير.

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

  • إذا أردت استخدام التأثير لمرة واحدة من خلال LaunchedEffect وDisposableEffect، ولكنك لست بحاجة إلى وظائف coroutines، وليس لديك تأثير dispose، يمكنك أيضًا استخدام واجهة برمجة التطبيقات الجديدة SideEffect مع المفاتيح التي قدّمناها في الإصدار 1.12 من Compose.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // One-shot events
        SideEffect(Unit) { mediaPlayer.play() }
    }
  • يُعدّ هذا بديلاً رائعًا لواجهتَي برمجة التطبيقات الأُخريَين إذا كنت قد استخدمتهما، إما LaunchedEffect من أجل الراحة أو DisposableEffect بدافع الضرورة للحصول على تأثير لمرة واحدة، ويمكن أن يؤدي الانتقال إلى SideEffect باستخدام واجهات برمجة التطبيقات ذات المفاتيح إلى تحقيق الشيء نفسه مع تقليل النفقات العامة وتحسين الأداء، خاصةً إذا تم الانتقال عدة مرات على مدار تطبيق كامل.

  • وأخيرًا، واجهة برمجة التطبيقات الأقل تكلفة هي واجهة برمجة التطبيقات SideEffect الأساسية.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Synchronize the outside world to the state of composition
        SideEffect { mediaPlayer.playbackRate = playbackRate }
    }
  • ويكون هذا الإجراء مناسبًا عندما تريد مزامنة الحالة الخارجية للعالم مع ما يحدث في المقطوعة الموسيقية.

  • في مثال مشغّل الوسائط، لديّ مثال آخر يوضّح مزامنة بعض حالات التشغيل مع بعض المَعلمات التي يتم تلقّيها من خلال التركيب.

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

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

  • إذا كنت تعتمد على هذا التوقيت في التأثيرات التي تم إطلاقها، تذكَّر ذلك إذا كنت تفكّر في نقل البيانات.

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

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

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) { viewModel.loadCart() }
        LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
        LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }
    
        CartContent(...)
    }
  • ويُعدّ ذلك مفيدًا بشكل خاص إذا كانت لديك تأثيرات متعددة تستخدم المفتاح نفسه.

  • وهو مرشح جيد للنظر في دمجه معًا لأنه يمكن تنفيذه في الوقت نفسه.

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

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

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) {
            launch { viewModel.loadCart() }
            launch { viewModel.loadPaymentMethods() }
            launch { viewModel.loadDeliveryAddresses() }
        }
    
        CartContent(...)
    }
  • لذلك، من الجيد التفكير في نقل البيانات في أماكن في مثل هذه المواقف لتقليل عدد التأثيرات عندما يكون ذلك ممكنًا.

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

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

تتبُّع الإنشاء وإعادة الإنشاء

  • حسنًا.

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

  • العنصر

  • نعم، ما يحدث داخل التركيب الذي يتم تشغيل الدوال المركّبة فيه.

  • ويساعدنا تتبُّع التركيب في ذلك.

  • لذلك، نقدّم بعض العلامات في برنامج تجميع Compose التي تضيف تعليقات توضيحية إلى كل دالة وتجعلها مرئية في تتبُّع Perfetto، حتى تتمكّن من التحقّق من الوقت الذي تستغرقه كل دالة.

  • ويمكنك استخدام ذلك لمعرفة الدوال التي تتم إعادة إنشائها بالضبط.

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

  • ومع ذلك، يكون تتبُّع التركيب مفيدًا جدًا إذا أردت معرفة الوقت الذي تستغرقه كل دالة مركّبة على حدة.

  • في بعض الأحيان، قد ترغب أيضًا في معرفة سبب إعادة إنشاء العناصر، وهذا ما نحاول حله في الإصدار التالي من تتبُّع Compose، وهو الإصدار 1.13.

  • نقدّم الآن ميزة تتبُّع إعادة الإنشاء، والتي تضيف تعليقات توضيحية لكل قراءة وكتابة للحالة أدّت إلى إعادة الإنشاء.

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

  • إذا أردت معرفة المزيد، يمكنك الاطّلاع على الرابط هنا.

  • وتتضمّن جميع المستندات المتعلّقة بتتبُّع الإنشاء وتتبُّع إعادة الإنشاء.

  • والآن، سيتحدث "أندرو" عن كيفية استخدام هذه الميزة بشكل أكثر فعالية لتجنُّب عمليات كتابة الحالة.

تحسين الحالة وعمليات الإبطال

  • شكرًا، أندريه.

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

  • على سبيل المثال، المثال الأكثر شيوعًا الذي لديّ هو زر الانتقال إلى أعلى الصفحة الذي يعتمد على مستوى الرؤية أو تقدّم الانتقال في الصفحة.

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton = scrollState.value > 0
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • هنا، لأنّني أقرأ قيمة حالة الانتقال داخل التركيب، هذه حالة تتغيّر بشكل متكرّر جدًا، وقد تتغيّر بمعدّل مرة واحدة لكل لقطة، وسيؤدي ذلك إلى الكثير من عمليات إعادة الإنشاء التي يمكن تجنّبها لأنّ هذا الشرط، على الرغم من أنّ حالة الانتقال نفسها تتغيّر، فإنّ الشرط الفعلي الذي نهتم به هنا لا يتغيّر كثيرًا.

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

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

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • إذا سبق لك رؤية هذا النمط، من المحتمل أنك رأيت هذا الاستخدام مع الحالة المشتقة بدلاً من ذلك.

  • ‫ComputedState هي واجهة جديدة في Compose 1.13، وعلى الرغم من أنّ ComputedState وDerivedState لهما هدف مشابه يتمثل في الإبطال المشروط عند تغيُّر عملية الحساب، إلا أنّ هناك اختلافًا دقيقًا للغاية في طريقة تنفيذ ذلك.

  • ‫DerivedState، التي أطلقناها منذ بداية استخدام Compose، تخزّن القيمة التي تحتسبها مؤقتًا وتعيد استخدامها إلى أن تتغيّر تبعياتها.

  • باستخدام ComputedState، يتم احتساب القيمة في كل مرة تقرأها.

  • قد يبدو التخزين المؤقت مفيدًا، ولكن في كثير من الحالات، يكون التخزين المؤقت أكثر تكلفة من مجرد قراءة النتيجة.

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

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

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    // First read
    derivedState.value
    computedState.value
    
    // Second read
    derivedState.value
    computedState.value
  • بعد ذلك، أقرأ الحالة مرتين وسنحصل على القيمة نفسها في المرتين، ولكن ما يحدث في الخلفية يختلف بينهما.

  • لتبسيط تتبُّع ما سيظهر هنا في أداة مثل Perfetto، يمكننا فحص ما يحدث فعليًا عند قراءة هاتين الحالتين.

  • هنا، نبدأ باستدعاء computedState.value.

  • ستنفّذ دالة lambda الحسابية، والتي تقرأ بعد ذلك قيمة الحالة، وهناك أيضًا القليل من الصيانة لإعلامك بحدوث القراءة.

  • وينطبق ذلك على جميع حالات اللقطات في Compose.

  • تتصرّف القراءة الثانية تمامًا مثل القراءة الأولى ولها خصائص الأداء نفسها طوال الوقت.

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

  • لذلك يقوم ببعض الصيانة للتخزين المؤقت نفسه ثم ينجز نفس الشيء في وقت أطول قليلاً.

  • ومع ذلك، فإن القراءة الثانية لـ DerivedState تفوز فعليًا على القراءة الثانية لـ ComputedState، على الرغم من أن كلاهما حالتان تافهتان إلى حد ما.

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

  • ولكن السيناريو المثير للاهتمام هو ما يحدث عندما يجب إبطال الحالة.

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

  • ما يبدو عليه ذلك في الرمز هو أننا نفعل شيئًا مثل تعديل a، وربما زيادته، ثم قراءة القيمة مرة أخرى أو ربما إعادة التركيب.

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    ...
    
    // Read-after-write
    a++
    derivedState.value
    computedState.value
  • ما يبدو عليه ذلك في المخطط الفعلي سيكون، مرة أخرى، هو نفسه تمامًا بالنسبة إلى ComputedState.

  • لا يحتوي على ذاكرة تخزين مؤقت لإدارتها، ويبدو هذا تمامًا كما هو، أولاً، ثانيًا، ثالثًا، أيًا كانت هذه القراءة في الواقع.

  • ومع ذلك، فإنّ DerivedState لديها مهمة صعبة.

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

  • كل هذا يتراكم ويجعل تكلفة إبطال DerivedState باهظة للغاية مقارنةً بـ ComputedState.

  • لإثبات صحة هذه الادعاءات بالأرقام، تكون ComputedState عادةً أسرع في القراءة الأولى بنحو 200 نانو ثانية.

  • يخسر ComputedState حوالي 350 نانو ثانية على هاتف Pixel 6، تحديدًا في ظروف قياس أداء معيّنة.

  • لذلك، في القراءة الثانية، ستخسر ComputedState بشكل طفيف مقارنةً بـ DerivedState.

  • ولكن في الحالة التي تهمّنا، وهي الحالة التي نُبطل فيها الحالة وربما الحالة التي يكون لديك فيها عمل فعلي على الإطار، يكون ComputedState أسرع بمقدار الضعف تقريبًا، بل وأكثر من الضعف مقارنةً بـ DerivedState.

  • لذا، استنادًا إلى الغرض من استخدام الحالات، يمكن أن تكون ComputedState خيارًا أوليًا جيدًا جدًا.

  • بشكل عام، لا يزال هناك قرار يجب اتخاذه عند استخدام DerivedState وComputedState.

  • لن نوقف ComputedState، فلا يزال هناك استخدامات لكليهما.

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

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

التخطّي والثبات في Compose

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

  • ويختلف ذلك عن تخطّي دالة مركّبة، وهو ما نتحدث عنه عادةً عندما نفكر في الإبطال المشروط أو عدم تنفيذ العمل داخل دالة مركّبة.

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

  • كما ذكرنا سابقًا، كل شيء في التركيب له بعض التكاليف المرتبطة به، ويشمل ذلك التخطّي.

    // What you write
    @Composable fun ContactListItem(
        contact: Contact
    ) { ... }
    
    // What you get
    @Composable fun ContactListItem(
        contact: Contact,
        $composer: Composer?
        $changed: Int
    ) {
        val $dirty = $changed or
            $composer.changed(contact)
    
        if ($dirty) { /* Your function body */ }
        else $composer.skipToGroupEnd()
    }
  • قد لا يكون تخطّي الإعلانات أمرًا بسيطًا.

  • هناك الكثير من أعمال الصيانة التي يتعيّن علينا القيام بها لتفعيلها في المقام الأول.

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

  • وعلينا أيضًا مقارنة القيم الحالية بالقيم السابقة لمعرفة ما إذا كان يجب تخطّي الدالة المركّبة أم لا.

  • وبعد ذلك، كل هذا هو مجرد قوة التحقق من "if" الذي يتخطى الكود أم لا.

  • قد يكون هذا مكلفًا حسب ما يحدث هنا.

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

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

    data class ContactsModel(
        val header: String,
        val contacts: List<Contact>
    )
    
    data class Contact(...)
    
    @Composable
    fun ContactsScreen(model: ContactsModel) {
        Column {
            TopAppBar(title = { Text(model.header) })
            ContactsList(model.contacts)
        }
    }
    
    @Composable
    fun ContactsList(contacts: List<Contact>) {
        LazyColumn {
            items(contacts) { ContactListItem(it) }
        }
    }
    
    @Composable
    fun ContactListItem(contact: Contact) { ... }
  • لذا، يحتوي نموذج الشاشة هنا على شيء مثل شاشة العنوان ثم كل قائمة جهات الاتصال التي يمررها بعد ذلك من خلال دالتين مركبتين تم استدعاؤهما في النهاية كقائمة بطيئة ثم عرضهما على الشاشة مع بعض خلايا العناصر.

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

  • إذا نظرنا إلى كيفية تمرير بياناتنا، فسنبدأ بالنموذج الموجود في الأعلى.

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

  • لنلقِ نظرة على ما يحدث عندما نعدّل جهة الاتصال الأخيرة في هذه القائمة.

  • ما نريده هو إبطال العنصر الأخير في القائمة فقط، ولكن هذا ليس ما سيحدث هنا.

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

  • وهذه طريقة مكلفة للغاية للوصول إلى هنا.

  • يمكننا أيضًا أن نرى أنّ هناك كل هذه الأقسام التي يبدو فيها أنّه لا يحدث أي شيء داخل التتبُّع.

  • وعلى الرغم من أنّ ذلك غير واضح هنا، فإنّ هذا هو المكان الذي تقرّر فيه الدوال المركّبة ما إذا كانت على وشك التخطّي أم لا.

  • في هذا المثال، يتم تخطّي دالتَين مركّبتَين فقط بسبب طريقة إعادة تركيب القائمة الكسولة.

  • عندما تمنحه دالة lambda جديدة للمحتوى، سيتم إلغاء صلاحية جميع العناصر التي يحتوي عليها.

  • وبالتالي، سيتم إلغاء صلاحية جهات الاتصال التي لم تتغيّر بالإضافة إلى جهة الاتصال التي تغيّرت، وهاتان هما جهتا الاتصال الوحيدتان اللتان سيتم تخطّيهما هنا.

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

  • إذا كانت الكائنات مستقرة، فقد تكون هذه المقارنة مكلفة إلى حد ما.

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

  • والأسوأ من ذلك، لأننا قمنا بالتفكيك، علينا في الواقع إجراء نفس المقارنة في القائمة مرتين.

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

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

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

  • ويمكن أن تكون هذه المقاييس مؤشرًا مفيدًا، ولكن ليس من الضروري التركيز عليها بشكل مفرط أو الاعتماد عليها بشكل كبير طوال عملية تحسين تطبيقاتك.

  • تذكَّر أنّه بدلاً من محاولة تخطّي الدالة المركّبة، تجنَّب إبطالها من الأساس.

تحليل التنفيذ باستخدام سجلّات الإجراءات

  • رأينا في هذا المثال أنّ تتبُّعات التركيب يمكن أن تقدّم الكثير من المعلومات حول ما يحدث، ولكنّها لا تعرض بالضرورة الصورة الكاملة.

  • لذلك، سيخبرنا "أندريه" عن سجلّات الإجراءات.

  • حسنًا.

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

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

  • ما عليك فعله الآن هو الحصول على سجلّ الإجراءات.

  • هناك شيء في المحلل، مثل عنصر في المحلل في استوديو Android، أو يمكنك أيضًا استخدام أوامر Adb للحصول على التتبّع، وما سيعطيك هو كل استدعاء دالة يحدث في عالم Java.

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

  • هناك شيء واحد، أريد أن - أرى هذا العنصر في المنتصف.

  • أريد أن أشير إلى أنّ تتبُّع الطرق يتطلّب موارد كثيرة جدًا.

  • يكون لديك عبء إضافي يتراوح بين 10 و20 مرة حسب مدى تعقيد شجرة الدوال، لأنّ كل استدعاء دالة يضيف القليل من الشيء.

  • ولتوضيح ذلك، يمكننا إلقاء نظرة على لقطة الشاشة.

  • هذا هو الشكل المعتاد لسجلّ الإجراءات عند بدء تشغيل التطبيق، وقد تتساءل: "كيف يمكنني فهم هذا؟"

  • حسنًا، تبدأ في تتبع التركيب عادةً، وتنظر إلى الدوال التي لديك هنا.

  • هذا يشبه تتبُّع الإصدار المبسّط، حيث يتوفّر ScreenContent وبعض الأزرار وبعض الدوال الأخرى.

  • لنفترض أنّنا نريد معرفة ما يحدث في ScreenContent.

  • يحتوي Perfetto على شريط بحث.

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

  • يمكنك هنا رؤية بعض Composer.startGroup والمكالمات الجماعية.

  • هذا ما تفعله بيئات التشغيل وبرنامج التجميع في الخلفية للتأكّد من أنّ التركيب يؤدي وظيفته، ولكن الاستدعاء المثير للاهتمام هنا هو Composer.changed.

  • هذه هي الطريقة التي نتحقّق من خلالها مما إذا كانت المَعلمات قد تغيّرت بين عمليات الاستدعاء السابقة والحالية للدالة المركّبة.

  • وتستحوذ ScreenModel.equals على معظم هذه الـ Composer.changed.

  • لذا، ربما يكون ScreenModel نوعًا من فئات البيانات الضخمة، وربما جعلناه ثابتًا للمساعدة في عدد عمليات إعادة الإنشاء، لذا ربما يكون شيئًا ظهر في تقرير برنامج التجميع.

  • وقد تساعد إزالة التعليق التوضيحي الثابت في تحسين الأداء في هذه الحالة تحديدًا.

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

  • حسنًا.

  • لقد تناولنا الكثير من المواضيع اليوم، والآن سأترككم مع "أندرو" لإنهاء الجلسة.

أدوات سريعة لتصميم الدوال المركّبة وتحسين الأداء

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

  • أولاً، تذكَّر أنّ الدوال المركّبة الأقل تكلفة والتي لا يتم إبطالها أفضل من الدوال المركّبة التي يتم تخطّيها.

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

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

  • يجب الانتباه إلى كيفية تراكم هذه التكاليف، وإلى أنّه قد تكون هناك تكاليف تفكيك مرتبطة بها قد تظهر في أسوأ وقت ممكن لتطبيقك.

  • تذكَّر أيضًا أنّه من المهم أن تكون مواطنًا صالحًا عند إجراء تحسينات داخل تطبيقك.

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

  • أيضًا، تذكر أنه يمكن أن تكون هناك مفاضلة بين قابلية الصيانة والأداء داخل تطبيقك.

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

  • ولمزيد من المساعدة، هناك أداة أخرى تحت تصرّفك، وهي وكيل الذكاء الاصطناعي الذي لديك حاليًا.

  • لقد أطلقنا كجزء من واجهة سطر الأوامر في Android وكجزء من المهارات التي نقدّمها هناك مهارة أداء Jetpack Compose التي تحتوي على الكثير من المعلومات التي تحدثنا عنها اليوم ويمكنها مراجعة تطبيقك وتقديم اقتراحات لهذه الأنواع من التحسينات.

  • ويمكنك تثبيته في الوكيل الحالي باستخدام Android CLI.

    android skills add jetpack-compose-performance
  • للاطّلاع على مزيد من المعلومات حول Android CLI، يمكنك الانتقال إلى developer.android.com.

  • يمكنك أيضًا معرفة المزيد عن مهاراتنا في مستودع GitHub.

  • بهذا نكون قد انتهينا، نتمنى لك تجربة قياس أداء آمنة وتأليفًا سعيدًا.

الرمز

مقتطفات الرمز البرمجي

مقتطف Kotlin في الدقيقة 09:34 في فيديو "إدارة التأثيرات الجانبية"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Asynchronous work and animations
    LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
}
مقتطف Kotlin في الدقيقة 10:15 في "إدارة الآثار الجانبية"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val activity = LocalActivity.current

    // Setup and teardown
    DisposableEffect(Unit) {
        activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
        onDispose {
            activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
        }
    }
}
مقتطف Kotlin في الدقيقة 10:36 في "إدارة الآثار الجانبية"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // One-shot events
    SideEffect(Unit) { mediaPlayer.play() }
}
مقتطف Kotlin في الدقيقة 11:12 في "إدارة التأثيرات الجانبية"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Synchronize the outside world to the state of composition
    SideEffect { mediaPlayer.playbackRate = playbackRate }
}
مقتطف Kotlin في الدقيقة 12:12 في "إدارة الآثار الجانبية"
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) { viewModel.loadCart() }
    LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
    LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }

    CartContent(...)
}
مقتطف Kotlin في الدقيقة 12:36 في "إدارة الآثار الجانبية"
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) {
        launch { viewModel.loadCart() }
        launch { viewModel.loadPaymentMethods() }
        launch { viewModel.loadDeliveryAddresses() }
    }

    CartContent(...)
}
مقتطف Kotlin في الدقيقة 15:12 في "تحسين الحالة وعمليات الإبطال"
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton = scrollState.value > 0
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
مقتطف Kotlin في الدقيقة 15:51 في "تحسين الحالة والإبطال"
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
مقتطف Kotlin في الدقيقة 16:49 في "تحسين الحالة وعمليات الإبطال"
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

// First read
derivedState.value
computedState.value

// Second read
derivedState.value
computedState.value
مقتطف Kotlin في الدقيقة 18:35 في "تحسين الحالة والإبطال"
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

...

// Read-after-write
a++
derivedState.value
computedState.value
مقتطف Kotlin في الدقيقة 21:07 في فيديو "التخطّي والثبات في Compose"
// What you write
@Composable fun ContactListItem(
    contact: Contact
) { ... }

// What you get
@Composable fun ContactListItem(
    contact: Contact,
    $composer: Composer?
    $changed: Int
) {
    val $dirty = $changed or
        $composer.changed(contact)

    if ($dirty) { /* Your function body */ }
    else $composer.skipToGroupEnd()
}
مقتطف Kotlin في الدقيقة 21:53 في فيديو "التخطّي والثبات في Compose"
data class ContactsModel(
    val header: String,
    val contacts: List<Contact>
)

data class Contact(...)

@Composable
fun ContactsScreen(model: ContactsModel) {
    Column {
        TopAppBar(title = { Text(model.header) })
        ContactsList(model.contacts)
    }
}

@Composable
fun ContactsList(contacts: List<Contact>) {
    LazyColumn {
        items(contacts) { ContactListItem(it) }
    }
}

@Composable
fun ContactListItem(contact: Contact) { ... }
مقتطف Shell في الدقيقة 29:40 في فيديو "أدوات التصميم والأداء السريع القابل للإنشاء"
android skills add jetpack-compose-performance