ابزارها و عملکرد

در Compose از خودتان پیشی بگیرید

بلندگو

درباره

درباره سخنرانی

Jetpack Compose طراحی میاناهای کاربر با کیفیت بالا را آسان می‌کند، اما ردیابی مشکلاتی که بر عملکرد میانای کاربر تأثیر می‌گذارند می‌تواند دشوار باشد. این نوع مشکلات می‌تواند به مشکلاتی منجر شود که کاربران متوجه آن می‌شوند، مانند تأخیر در راه‌اندازی و فریم‌های ازدست‌رفته که به‌صورت لرزش پیمایش یا پویانمایی‌های منقطع ظاهر می‌شوند.

در این گفتگو، ابزارهایی را برای آشکار کردن آنچه بر عملکرد میانای کاربر شما تأثیر می‌گذارد معرفی می‌کنیم، با تمرکز بر Perfetto و ردیابی (باز)ترکیب‌بندی. سپس الگوهای منفی خاصی را که ممکن است استفاده کنید و مخفیانه برای شما هزینه داشته باشند، نحوه پیدا کردن آن‌ها در کد، و الگوهای مثبتی را که می‌توانید جایگزین آن‌ها کنید بررسی خواهیم کرد. در پایان این گفتگو، متوجه خواهید شد که چگونه با ذهنیت عملکردی در Compose فکر کنید و چگونه بهینه‌سازی‌های عملکرد را در برنامه خود نمایه، تجزیه‌وتحلیل و درستی‌سنجی کنید.

خلاصه

  • ردیابی‌های Perfetto فعالیت درون‌دستگاهی را ضبط می‌کنند و برش‌های Compose مانند ترکیب‌بندی و چیدمان را درطول رشته اصلی نشان می‌دهند. از آن‌ها برای بازرسی خطوط زمانی قاب و پیگیری مدت‌زمان فازها استفاده کنید.
  • هر عملیات داخل ترکیب‌پذیرها هزینه‌ای دارد، ازجمله راه‌اندازی و لغو کردن کوروتین‌ها. میاناهای برنامه‌سازی کاربردی عوارض جانبی را ممیزی کنید و برای کاهش سربار ترکیب‌بندی، جلوه‌های دارای کلیدهای یکسان را ادغام کنید.
  • ردیابی ترکیب‌بندی توابع ترکیب‌پذیر را حاشیه‌نویسی می‌کند و ردیابی بازترکیب‌بندی (نسخه ۱.۱۳ به بالا) خواندن و نوشتن وضعیت را که باعث بازترکیب‌بندی می‌شود ردیابی می‌کند. این رویدادها به تشخیص کارهای غیرمنتظره مستقیماً در ردگیری‌های Perfetto کمک می‌کنند.
  • ‫ComputedState (Compose نسخه ۱.۱۳ به بالا) از باطل‌سازی‌های ناخواسته جلوگیری می‌کند و معمولاً درطول باطل‌سازی‌های خواندن پس‌از نوشتن سریع‌تر از DerivedState است. DerivedState زمانی مفید باقی می‌ماند که محاسبات یا تخصیص‌های پرهزینه از ذخیره‌سازی مؤقت بهره‌مند شوند.
  • رد شدن از composableها هزینه‌های نگهداری و سربار مقایسه پارامترها را به‌همراه دارد. به‌جای اجبار به پایداری، به رد شدن قوی و برابری هویت شیء تکیه کنید یا به‌طور کامل از باطل‌سازی اجتناب کنید.

مهارت‌ها

منابع

Gemini Notebook

موضوع

  • jetpack-compose
  • android-performance
  • perfetto
  • ارزیابی مقایسه‌ای
  • روال‌های مشترک
  • ردیابی ترکیب‌بندی
  • android-cli

آوانویسی

مقدمه

  • سلام.

  • سلام به همه.

  • امیدوارم استراحت خوبی داشته باشید.

  • امیدوارم قهوه‌تان را خورده باشید، مرطوب‌کننده زده باشید، و از این چیزها.

  • امروز می‌خواهیم کمی عمیق‌تر کاوش کنیم.

  • بسیارخوب، بله، شروع می‌کنیم.

  • امروز کمی عمیق‌تر کاوش می‌کنیم.

  • نام من آندری شیکوف است و امروز اندرو بیلی هم در این صحنه با من همراه است.

  • ما هر دو در تیم زمان اجرای کامپایلر Compose کار می‌کنیم، اما کارهای زیاد دیگری هم انجام می‌دهیم، ازجمله عملکرد.

  • و موضوع صحبت امروز پاسخ دادن به سؤال شما است، سؤالاتی درباره عملکرد، زیرا Jetpack Compose به‌عنوان یک جعبه‌ابزار میانای کاربر پیش‌فرض برای توسعه Android ایجاد شده است.

  • و اکنون افراد زیادی می‌پرسند که چگونه می‌توانیم مطمئن شویم که Compose UI ما تا حد امکان کارآمد است.

  • بنابراین امروز، همان‌طور که قبلاً گفتم، می‌خواهیم بررسی عمیق و دقیقی انجام دهیم.

  • همچنین، ابزارهایی را معرفی می‌کنیم تا بتوانید عملکرد خود را در Compose بهبود ببخشید.

  • اولین چیزی که می‌خواهم با آن شروع کنم، صحبت درباره این است که چرا عملکرد برای ما مهم است.

  • وقتی به کیفیت و ساخت برنامه‌های باکیفیت فکر می‌کنید، معمولاً به پویانمایی‌ها فکر می‌کنید، ما به مجموعه ویژگی‌های غنی فکر می‌کنیم، و گاهی‌اوقات عملکرد از قلم می‌افتد، اما درک کاربر واقعاً به عملکرد برنامه شما بستگی دارد.

  • بنابراین اگر پویانمایی‌ها دچار لکنت شوند، درک بلافاصله کاهش می‌یابد.

  • بنابراین همه سنجه‌ها و اعدادی که امروز به آن‌ها اشاره می‌کنیم درواقع سنجه‌های نیابتی هستند که انتخاب کرده‌ایم تا نشان دهیم کاربران چه احساسی درباره برنامه دارند، چه احساسی درباره همه آن پویانمایی‌ها و همه اتفاقاتی که روی صفحه می‌افتد دارند.

  • بنابراین، اساساً بهینه‌سازی برنامه شما بهینه‌سازی برای تجربه بهتر است.

  • بنابراین این فقط یک چیز ذهنی است که می‌خواهم داشته باشید، زیرا امروز در بسیاری از موضوعات عمیق می‌شویم.

  • اکنون، وقتی شروع به بررسی عملکرد برنامه‌تان می‌کنید، می‌خواهم سه کار سریع انجام دهید، سه کار خیلی سریع.

  • اولین نکته این است که اگر عملکرد را اندازه‌گیری می‌کنید، آن را در حالت انتشار اندازه‌گیری می‌کنید، بنابراین اطلاعات قابل‌اشکال‌زدایی زیادی نداریم و همه این‌ها سرعت برنامه ما را کاهش می‌دهد.

  • مورد دوم، R8 یا سایر کوچک‌کننده‌ها را فعال می‌کنید.

  • همین امسال، بهینه‌سازی‌ای را معرفی کردیم که عملکرد هم‌روال را دوبرابر بهبود می‌بخشد و کارهای بسیار دیگری نیز انجام می‌دهیم.

  • ما هنگام بهینه‌سازی Compose، به‌شدت به R8 تکیه می‌کنیم.

  • و آخرین نکته، به‌ویژه برای سنجه‌های راه‌اندازی، باید مطمئن شوید که از نمایه خط مبنا استفاده می‌کنید.

  • بنابراین کلاس‌هایی که در راه‌اندازی بار می‌کنید، هنگام شروع آن برنامه ازپیش کامپایل و ازپیش بارگیری می‌شوند.

  • بنابراین همان‌طور که می‌بینید، این حدود ۶ برابر با ساخت قابل‌اشکال‌زدایی معمول برنامه شما تفاوت دارد.

  • و وقتی همه این موارد را اندازه‌گیری می‌کنید، آن‌ها با سایر موفقیت‌هایی که در کدتان به‌دست می‌آورید ترکیب می‌شوند.

  • اکنون، شما همه این کارها را انجام داده‌اید، برنامه هنوز کمی تأخیر دارد، یا هنوز مشکلات دیگری در عملکرد وجود دارد، و اینجاست که می‌خواهید وارد این حلقه عملکرد شوید.

  • می‌خواهید شروع به بررسی برخی سنجه‌ها در برخی سناریوهای خاص کنید.

  • برای مثال، فرض کنید بار شدن برنامه‌تان خیلی طول می‌کشد، بنابراین شاید بخواهید برنامه‌تان را بازنشانی کنید، آن را درست مثل راه‌انداز معمولی شروع کنید، و بررسی کنید چقدر طول می‌کشد تا برنامه به اولین صفحه داده برسد.

  • یا شاید برنامه کمی لکنت داشته باشد، بنابراین آن را پیمایش می‌کنید و مطمئن می‌شوید که این پیمایش تکرارپذیر و پایدار است.

  • بنابراین محیط پایدار در این اسلاید واقعاً مهم است، زیرا وقتی بعداً بهینه‌سازی می‌کنید، می‌خواهید مطمئن شوید که بهبودهایی را در سنجه‌هایتان می‌بینید و می‌توانید تأیید کنید که این بهینه‌سازی‌ها واقعاً طبق انتظار عمل می‌کنند.

  • معیارها برای این کار واقعاً خوب هستند، زیرا معیارها ازقبل داده‌های زیادی را ضبط می‌کنند، آن‌ها تکرارهای زیادی انجام می‌دهند تا مطمئن شوند که نویز زیادی در دستگاه شما وجود ندارد، و همچنین تنظیمات دستگاه زیادی را انجام می‌دهند که به شما کمک می‌کند.

  • و محک‌ها نیز به‌طور پیش‌فرض سناریوهای تکرارپذیر هستند و این همان چیزی است که ما در اینجا برای آن تلاش می‌کنیم.

  • اما اگر بخواهید می‌توانید این کار را با چند دستورات هم انجام دهید.

  • و آخرین چیز، نمایه شما، چیزی را در برنامه‌تان بهبود می‌دهید، به داده‌ها نگاه می‌کنید، و سپس دوباره تکرار می‌کنید، و شاید دوباره با سنجه‌های سناریوهای مختلف که برنامه‌تان با آن‌ها مشکل دارد تکرار می‌کنید.

نمایه‌سازی با ردیابی‌های Perfetto

  • همان‌طور که به داده‌ها نگاه می‌کنید، اولین ابزاری که امروز معرفی می‌کنیم و می‌خواهیم واقعاً با آن آشنا شوید، ردگیری‌های Perfetto است.

  • ردگیری‌های Perfetto هر اتفاقی را که در دستگاه رخ می‌دهد، درحین ضبط کردن آن ردگیری، ضبط می‌کند، مثلاً درطول اجرای محک کلان، یا شاید برخی‌از اسکریپت‌هایی که ضبط می‌کنید.

  • و این کمک بزرگی برای هر مهندسی است که روی عملکرد Android کار می‌کند.

  • برای من، این روزها بیشتر وقت کاری‌ام صرف نگاه کردن به ردهای Perfetto می‌شود.

  • آن‌ها چیزی شبیه به این هستند.

  • این مقدار داده بسیار زیاد است.

  • همان‌طور که گفتم، آن‌ها هر اتفاقی را که در دستگاه می‌افتد ضبط می‌کنند.

  • بنابراین، ممکن است در ابتدا برای پیدا کردن مکان دقیق برنامه‌تان مشکل داشته باشید، بنابراین توصیه می‌کنم از این نوار جستجوی مفید استفاده کنید.

  • Compose را جستجو کنید، بسته برنامه‌تان را جستجو کنید، و چیزهایی از این قبیل.

  • فقط خودتان را در چیزهای آشنا قرار دهید.

  • و وقتی خودتان را آماده کردید و فرایند برنامه‌تان را پیدا کردید، خطوط زمان پردازش فریم و مدت‌زمان انجام بسیاری از کارها را در برنامه‌تان خواهید دید.

  • اگر یکی از آن‌ها را جدا کنیم، چیزی شبیه به این خواهید دید.

  • اولی کار رشته اصلی است.

  • اینجا جایی است که Compose بیشتر کارهایش را انجام می‌دهد.

  • سپس RenderThread وجود دارد.

  • اینجا جایی است که فرمان بوم را پردازش می‌کنیم و آن‌ها را به GPU کدگذاری می‌کنیم.

  • سپس GPU کارش را انجام می‌دهد و بعد سیستم آن را ترکیب می‌کند.

  • امروز فقط رشته اصلی را بررسی می‌کنیم.

  • چون Compose کارش را در اینجا انجام می‌دهد.

  • ما درباره Compose صحبت می‌کنیم، بنابراین منطقی است که فقط روی آن تمرکز کنیم.

  • و وقتی آن یک قاب را در رشته اصلی جدا می‌کنیم، درواقع اکنون شروع به دیدن برش‌های Compose می‌کنیم.

  • چند مورد را برایتان انتخاب کردم.

  • مثل ترکیب‌بندی، اندازه‌گیری، چیدمان، و ترسیم است.

  • اگر درباره فازهای Compose شنیده باشید، احتمالاً آن‌ها را در بررسی‌های قبلی‌تان از عملکرد دیده‌اید.

  • بنابراین، این‌ها برش‌های جداگانه‌ای در ردگیری Perfetto دارند و خیلی راحت می‌توان فهمید که هر فاز چقدر طول می‌کشد و کدام فازها در هر قاب خاص اجرا می‌شوند.

  • بنابراین، ما به‌طور مفصل درباره بهینه‌سازی برخی از این کارها صحبت کرده‌ایم و شما می‌خواهید مطمئن شوید که کار درست را در مرحله درست انجام می‌دهید.

  • برای مثال، اگر پویانمایی‌ای دارید که رنگ جعبه شما یا چیزی شبیه به آن را به‌روز می‌کند، فقط می‌خواهید فاز ترسیم را باطل کنید، نه ترکیب‌بندی، چیدمان، و چیزهایی از این قبیل.

  • این عملکرد ۱۰۱ است و امروز کمی عمیق‌تر صحبت می‌کنیم یا کمی عمیق‌تر در برخی چیزها غوطه‌ور می‌شویم و به‌طور خاص روی ترکیب‌بندی تمرکز می‌کنیم.

  • اکنون نوبت اندرو است تا درباره آن صحبت کند.

  • متشکرم، آندری.

  • همان‌طور که آندری اشاره کرد، ما قبلاً زمان زیادی را در زمینه عملکرد درباره بهینه‌سازی‌هایی مانند اجتناب از خواندن‌های معکوس، اطمینان از اینکه خواندن‌های حالت را به فاز مناسب در چرخه زندگی ترکیب‌بندی موکول می‌کنید، و بهینه‌سازی، و اطمینان از اینکه composable‌های شما همان‌طور که انتظار دارید باطل می‌شوند، صحبت کردیم تا در هر فریم کار زیادی انجام ندهید.

  • این نقطه شروع بسیار خوبی است، اما ما درحال‌حاضر مطالب زیادی درباره این موضوع داریم و می‌خواهیم این گفتگو را کمی پیش ببریم.

  • بنابراین، امروز می‌خواهیم به آنچه پس‌از انجام این بهینه‌سازی‌ها در ترکیب‌بندی باقی می‌ماند بپردازیم و به آنچه خودِ ترکیب‌شدنی‌های شما انجام می‌دهند رسیدگی کنیم.

  • واقعیت ناخوشایند درباره هر چیزی که در کد Compose شما وجود دارد، و درواقع هر چیزی که با برنامه‌تان ازنظر عملکرد ارائه می‌دهید، این است که همه آن‌ها هزینه‌ای به‌همراه دارند.

  • هیچ‌کدام از کدهایی که در برنامه‌تان ارسال می‌کنید رایگان نیستند و حتی چیزهای جزئی مانند خواندن وضعیت و ترکیب کردن چیزهایی مانند عوارض جانبی در ترکیب‌بندی شما، هزینه‌های ملموسی دارند و مهم است که به آن‌ها توجه داشته باشید، به‌ویژه زمانی که شروع به تجزیه و تحلیل آنچه که composable شما واقعاً انجام می‌دهند می‌کنید و شروع به افزایش عملکرد در داخل خود برنامه می‌کنید.

  • برای نشان دادن اینکه این موضوع در میکروسکوپی‌ترین مقیاس واقعاً چگونه به‌نظر می‌رسد، در اینجا برخی‌از کوچک‌ترین عملیاتی را که می‌توانید در هرجایی از کدتان، و به‌ویژه در داخل composableهایتان انجام دهید آورده‌ایم، و در این وضوح، ما درباره مقیاس نانوثانیه ازنظر هزینه تقریبی این چیزها به‌عنوان یک طرز فکر صحبت می‌کنیم.

  • اگر می‌خواهید در عملکردتان تأثیر بگذارید، دو گزینه دارید.

  • می‌توانید تعداد بسیار زیادی از عملیات‌های ارزان یا تعداد کمتری از عملیات‌های گران‌تر را حذف کنید.

  • به‌طور خاص در این نمودار، روال‌های همگام از نظر این عملیات‌ها در سمت گران‌تر قرار دارند و نمونه خوبی برای شروع جستجو هستند، زیرا اگر چند مورد از این نمونه‌ها را حذف کنیم، می‌توانیم مزایای ملموس‌تری را سریع‌تر در برنامه‌مان ببینیم.

  • نکته دیگری که باید به‌طور خاص درباره کوروتین‌ها درنظر داشت این است که همان‌طور که در اسلاید آورده‌ام، نه‌تنها راه‌اندازی کوروتین هزینه‌ای به‌همراه دارد، بلکه لغو کوروتین نیز هزینه دارد.

  • هزینه لغو به‌ویژه قابل‌توجه است زیرا این اتفاق عمدتاً زمانی رخ می‌دهد که شما محتوا را دور می‌اندازید و احتمالاً به مناطق دیگری در داخل برنامه خود منتقل می‌شوید.

  • Navigating away is an extremely expensive case in many applications where you need to squeeze the most out of the frame time, and make sure that you're using the most of what you have.

  • بنابراین مهم است که به هر دو این هزینه‌ها توجه داشته باشید.

  • همه این‌ها، صرف‌نظر از اینکه به چه چیزی نگاه می‌کنیم، در این مقیاس حدود ۱۶ میلیون نانوثانیه برای کار کردن جمع می‌شوند.

  • بنابراین ما در اینجا بودجه‌ای داریم که باید با آن کار کنیم، و کوروتین‌ها مثال خوبی برای شروع خواهند بود.

مدیریت عوارض جانبی

  • در Compose، روال‌های همگام بیشتر با میاناهای برنامه‌سازی کاربردی جلوه‌ها ظاهر می‌شوند.

  • طیفی از میاناهای برنامه‌سازی کاربردی در اینجا وجود دارد که می‌توانید با SideEffects از عملکرد بالا تا تأخیر کم استفاده کنید.

  • در کاربردی‌ترین بخش این طیف، LaunchedEffect را داریم که شامل همه چیز برای کار ناهمگام است، به‌ویژه چیزهایی مثل پویانمایی‌ها، هر چیزی که برای آن به محدوده هم‌روال نیاز دارید.

  • اما اگر به آن نیاز ندارید و می‌توانید کارتان را با میانای برنامه‌سازی کاربردی ارزان‌تری انجام دهید، شاید بهتر باشد از آن استفاده کنید، البته اگر با نیازهای مورد استفاده شما مطابقت داشته باشد.

  • برای نشان دادن اینکه این موضوع در عمل چگونه است، در اینجا یک پخش‌کننده رسانه قابل‌ساخت دارم تا همه SideEffects مختلف و زمان ظاهر شدن آن‌ها را نشان دهم.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Asynchronous work and animations
        LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
    }
  • می‌توانیم با LaunchedEffect شروع کنیم.

  • در این مثال، از این برای بار کردن ناهمگام داده‌ها در پخش‌کننده رسانه استفاده می‌شود.

  • اگر به دامنه کوروتین نیاز دارید، یا اگر به‌طور کلی به کوروتین نیاز دارید یا می‌خواهید تماس تعلیقی برقرار کنید، از LaunchedEffect استفاده کنید.

  • اگرچه این روش همچنان گران‌تر است و هزینه‌ای برای کوروتین‌ها دارد، اما تا زمانی که از آن بهره می‌برید و استفاده از آن را توجیه می‌کنید، از API مناسب برای کار استفاده کنید.

  • اما اگر در شرایطی قرار گرفتید که از 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 زمانی عالی است که فرایند راه‌اندازی و برچیدن مرتبط با جلوه خود داشته باشید.

  • در اینجا، در همان مثال پخش‌کننده رسانه، مثالی دارم که جهت فعالیت را به افقی قفل می‌کند وقتی که composable وجود دارد، و سپس آن قفل جهت را وقتی که مدتی بعد از بین می‌رود، آزاد می‌کند.

  • اگر می‌خواهید از اثر یک‌باره LaunchedEffect و DisposableEffect استفاده کنید، اما به کوروتین‌ها نیاز ندارید و اثر dispose ندارید، می‌توانید از SideEffect جدید با API کلیدها که در Compose نسخه ۱.۱۲ معرفی کردیم نیز استفاده کنید.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // One-shot events
        SideEffect(Unit) { mediaPlayer.play() }
    }
  • این گزینه به‌عنوان جایگزینی برای دو میانای برنامه‌سازی کاربردی دیگر عالی است، اگر به آن‌ها دسترسی پیدا کرده‌اید، چه LaunchedEffect برای راحتی، یا DisposableEffect از روی ضرورت برای یک اثر یک‌باره، پیمایش یا انتقال به SideEffect با میاناهای برنامه‌سازی کاربردی کلیدها می‌تواند همان کار را با سربار کمتر و عملکرد بهبودیافته انجام دهد، به‌ویژه اگر چندین بار در طول یک برنامه کامل منتقل شده باشد.

  • و درنهایت، ارزان‌ترین API همان API ساده SideEffect است.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Synchronize the outside world to the state of composition
        SideEffect { mediaPlayer.playbackRate = playbackRate }
    }
  • این حالت زمانی مناسب‌تر است که می‌خواهید وضعیت بیرونی جهان را با آنچه در ترکیب‌بندی اتفاق می‌افتد همگام‌سازی کنید.

  • بنابراین در اینجا، دوباره در مثال پخش‌کننده رسانه، مثال دیگری دارم که همگام‌سازی برخی‌از وضعیت‌های بازپخش را با برخی پارامترهایی که توسط ترکیب‌بندی دریافت می‌شود نشان می‌دهد.

  • یک نکته را باید درنظر داشته باشید، اگر تصمیم گرفتید استفاده از عوارض جانبی را ممیزی کنید و به عوارض جانبی کم‌هزینه‌تری مهاجرت کنید، به‌خاطر داشته باشید که علاوه‌بر سایر رفتارها و موارد استفاده‌ای که درباره آن‌ها صحبت کردیم، تفاوت‌هایی نیز در تضمین‌های ترتیب‌دهی با عوارض جانبی وجود دارد.

  • All of the disposable effects execute first, followed by the regular side effects, keyed or not, and then after the frame completes is when all the launched effects run because there's a dispatch involved.

  • اگر برای جلوه‌های راه‌اندازی‌شده‌تان به این زمان‌بندی وابسته هستید، درصورت تصمیم به انتقال، این موضوع را درنظر داشته باشید.

  • و برای هرگونه انتقال، حتماً کدتان را آزمایش کنید و مطمئن شوید که به هیچ‌یک از این تضمین‌های زمان‌بندی وابسته نیستید و کدتان پس‌از آن همچنان کار می‌کند.

  • اگر نمی‌توانید پیچیدگی خود عارضه جانبی را کاهش دهید، همچنان می‌توانید تعداد عوارض جانبی را با ادغام کردن آن‌ها با یکدیگر کاهش دهید.

    @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 اجرا کنید و از سنجه‌های دیگر استفاده کنید.

  • اما ردیابی ترکیب‌بندی زمانی واقعاً مفید است که بخواهید بدانید هر ترکیب‌شدنی چقدر طول می‌کشد.

  • Now, sometimes you also want to figure out why things are recomposing, and that's what we trying to solve in the next release of the Compose Tracing runtime tracing, rather, 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 نسخه ۱.۱۳ جدید است و اگرچه 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
  • I'm also then reading the state twice and we'll get the same value both times, but what goes on under the hood is different between the two of them.

  • برای ساده کردن ردیابی آنچه درواقع در اینجا در چیزی مانند Perfetto نشان داده می‌شود، می‌توانیم آنچه را که درواقع هنگام خواندن هر دوی این حالت‌ها اتفاق می‌افتد بررسی کنیم.

  • در اینجا، با فراخوانی computedState.value شروع می‌کنیم.

  • این تابع محاسبه lambda را اجرا می‌کند که سپس مقدار وضعیت را می‌خواند، و همچنین کمی نگهداری برای اطلاع‌رسانی درباره وقوع خواندن وجود دارد.

  • این موضوع برای همه حالت‌های عکس آنی در Compose صادق است.

  • دومین خواندن دقیقاً مانند اولین خواندن رفتار می‌کند و در سراسر آن ویژگی‌های عملکردی یکسانی دارد.

  • درمقایسه، اولین خواندن DerivedState کار مشابهی انجام می‌دهد، جایی که هنوز باید لامبدا محاسبه را فراخوانی کند و همچنان اعلان دیگر را انجام دهد، اما کمی سربار مدیریت حافظه پنهان دارد، که در اولین بار کمی اتصال کوتاه است زیرا هیچ حافظه پنهانی برای بررسی وجود ندارد.

  • بنابراین، کمی نگهداری برای خود حافظه پنهان انجام می‌دهد و سپس همان کار را در زمان کمی بیشتر انجام می‌دهد.

  • بااین‌حال، خواندن دوم برای 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 کار سختی در پیش دارد.

  • It goes through a very expensive path to see that the cache is invalid before it even updates and calls the calculation itself, and then it has to do dependency tracking and manage a lot of dependent a lot of the observation to write back the cache value and understand that all of these values actually changed and manage the upkeep itself.

  • همه این‌ها جمع می‌شوند و هزینه باطل کردن DerivedState را درمقایسه با ComputedState بسیار گران می‌کنند.

  • برای اینکه این ادعاها را با عدد نشان دهیم، ComputedState معمولاً در اولین خواندن حدود ۲۰۰ نانوثانیه سریع‌تر است.

  • ComputedState حدود ۳۵۰ نانوثانیه در Pixel 6، به‌طور خاص، تحت شرایط محک‌زنی خاص ازدست می‌دهد.

  • بنابراین، در خواندن دوم، ComputedState کمی نسبت‌به DerivedState عقب می‌افتد.

  • اما در موردی که واقعاً برای ما مهم است، یعنی موردی که درحال باطل کردن هستیم و احتمالاً موردی که واقعاً باید روی قاب خود کار کنید، ComputedState تقریباً و درواقع بیش‌از دو برابر سریع‌تر از DerivedState است.

  • بنابراین، بسته به اینکه از حالت‌هایتان برای چه چیزی استفاده می‌کنید، ComputedState می‌تواند اولین انتخاب بسیار خوبی برای شروع باشد.

  • به‌طور کلی، هنگام استفاده از DerivedState و ComputedState هنوز باید تصمیم‌گیری شود.

  • ما ComputedState را منسوخ نمی‌کنیم، هنوز برای هر دو کاربردهایی وجود دارد.

  • به‌طورکلی، اگر مطمئن نیستید کدام را انتخاب کنید، حالت محاسبه‌شده پیش‌فرض خوبی است.

  • Derived state is most useful when you actually take advantage of the caching, meaning that you do something non-trivial like a complicated algorithm, some large allocation, a lot of state reads inside that actually benefit from the caching here, or anything non-trivial or that depends on things like object equality and object identity equality, where you make up the difference and gain the benefits of the caching that derived state offers.

رد شدن و پایداری در Compose

  • موضوع مهم دیگری که می‌خواهم درباره آن صحبت کنم این است که هم حالت مشتق‌شده و هم حالت محاسبه‌شده تلاش می‌کنند از نامعتبر کردن خود composable جلوگیری کنند.

  • این با رد شدن از یک ترکیب‌پذیر متفاوت است، که معمولاً وقتی درباره باطل کردن مشروط یا انجام ندادن کار در داخل یک ترکیب‌پذیر فکر می‌کنیم، درباره آن صحبت می‌کنیم.

  • حقیقتی که می‌خواهم در اینجا فاش کنم این است که اگرچه صحبت‌های زیادی درباره پایداری و رد شدن در 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) { ... }
  • بنابراین در اینجا، مدل صفحه شامل چیزی مانند صفحه سرصفحه و سپس تمام فهرست مخاطبین است که سپس از چند ترکیب‌پذیر عبور می‌کند که در نهایت یک فهرست تنبل را فراخوانی می‌کنند و سپس آن‌ها را با چند سلول مورد روی صفحه نمایش می‌دهند.

  • سلسله‌مراتب ترکیب‌بندی برای این کد چیزی شبیه به این است، که در آن دوباره با صفحه مخاطب شروع می‌کنیم، کمی گسترش می‌دهیم، و سپس چند ترکیب‌شدنی دیگر داریم که فراخوانی می‌شوند و موارد ما را ارائه می‌دهند.

  • اگر به نحوه انتقال داده‌هایمان نگاه کنیم، با مدل بالا شروع می‌کنیم.

  • ما آن را به یک رشته و سپس یک فهرست تجزیه می‌کنیم، سپس آن فهرست به یک لامبدا موارد برای فهرست تنبل تبدیل می‌شود، و سپس فهرست تنبل تمام مواردی را که روی صفحه ظاهر می‌شوند ترکیب می‌کند.

  • بیایید ببینیم وقتی آخرین مخاطب این فهرست را اصلاح می‌کنیم چه اتفاقی می‌افتد.

  • چیزی که می‌خواهیم اتفاق بیفتد این است که فقط آخرین مورد در فهرست باطل شود، اما این چیزی نیست که قرار است اینجا اتفاق بیفتد.

  • اگر به رد ترکیب‌بندی برای این بازترکیب‌بندی نگاه کنیم، چیزی شبیه به این خواهد بود، جایی که از صفحه مخاطب شروع می‌کنیم و درنهایت به این مورد فهرست می‌رسیم.

  • و این روش نسبتاً گرانی برای پایین آمدن به اینجا است.

  • همچنین می‌توانیم ببینیم که همه این بخش‌ها وجود دارند که به‌نظر می‌رسد هیچ اتفاقی در داخل ردیابی رخ نمی‌دهد.

  • و اگرچه در اینجا مشخص نیست، اما درواقع اینجاست که ترکیب‌پذیرها تصمیم می‌گیرند که آیا می‌خواهند رد شوند یا نه.

  • در این مثال، به‌دلیل نحوه بازترکیب فهرست تنبل، فقط دو ترکیب‌پذیر نادیده گرفته می‌شوند.

  • وقتی به آن یک لامبدا محتوای جدید می‌دهید، تمام مواردی را که دارد باطل می‌کند.

  • بنابراین، دو مخاطبی را که تغییر نکرده‌اند، علاوه‌بر مخاطبی که تغییر کرده است، باطل می‌کند و آن دو تنها مواردی هستند که در اینجا نادیده گرفته می‌شوند.

  • همه چیزهای دیگر این مقایسه برابری را نادیده می‌گیرند تا متوجه شوند که درواقع باید اجرا شوند.

  • اگر اشیای شما پایدار باشند، این مقایسه می‌تواند درواقع نسبتاً پرهزینه باشد.

  • اگر ما را مجبور کنید که همه ساختارهای داده شما را بررسی کنیم، این کار می‌تواند واقعاً هزینه‌بر باشد و فقط برای اینکه بگوییم دو شیء برابر هستند، هزینه قابل‌توجهی داشته باشد.

  • و بدتر اینکه چون ساختارزدایی کرده‌ایم، درواقع باید همان مقایسه را دو بار در فهرست انجام دهیم.

  • اگر درعوض اجازه دهید کامپایلر با رفتار پیش‌فرض رفتار کند و از پرش قوی استفاده کند، همچنان می‌توانید پرش داشته باشید و همچنان می‌توانید بررسی‌هایی برای پرش مشروط از composable‌های خود دریافت کنید، اما آن‌ها مسیر بسیار سریع‌تری را برای استفاده از برابری هویت شی به‌جای برابری ساختاری طی می‌کنند.

  • محاسبه این روش برای ما بسیار سریع‌تر است و اگرچه همان موارد را در این قاب دوباره ترکیب می‌کنیم، اما در جایی که واقعاً مهم است سریع وارد شویم، زمان زیادی را برای فهمیدن اینکه واقعاً نمی‌توانستیم از این ترکیب‌پذیرها صرف‌نظر کنیم تلف نمی‌کنیم.

  • به همین دلیل است که ما بر استفاده از ابزارهایی مانند معیارها و ردیابی‌ها برای درک آنچه در داخل برنامه شما می‌گذرد تأکید می‌کنیم، نه مستقیماً بر تعداد بازترکیب‌ها.

  • این معیارها می‌توانند نماینده مفیدی باشند، اما چیزی نیستند که لزوماً درطول بهینه‌سازی برنامه‌هایتان بیش‌ازحد روی آن‌ها تمرکز کنید یا به آن‌ها اهمیت دهید.

  • به‌یاد داشته باشید که به‌جای تلاش برای رد شدن از ترکیب‌پذیر، از همان ابتدا از نامعتبر کردن آن اجتناب کنید.

تجزیه‌وتحلیل اجرا با گزارش ردیابی روش‌ها

  • در این مثال دیدیم که ردگیری‌های ترکیب‌بندی می‌توانند اطلاعات زیادی درباره آنچه اتفاق می‌افتد ارائه دهند، اما لزوماً کل تصویر را نشان نمی‌دهند.

  • برای این کار، آندری قرار است درباره گزارش ردیابی روش‌ها به ما بگوید.

  • بسیار خوب.

  • همان‌طور که اندرو گفت، ردیابی ترکیب‌بندی همیشه همه اتفاقاتی را که درحال رخ دادن است به شما نمی‌گوید، فقط به این دلیل که فقط توابع ترکیب‌پذیر را حاشیه‌نویسی می‌کند، و باید مواردی وجود داشته باشد که فقط فضای خالی را در ردیابی ترکیب‌بندی ببینید که ازقبل ردیابی Perfetto را تقویت می‌کند، اما هنوز همه اتفاقاتی را که در آنجا رخ می‌دهد نشان نمی‌دهد.

  • و گاهی اوقات، می‌تواند چیزی باشد که Compose انجام می‌دهد، گاهی اوقات ممکن است چیزی در کد شما باشد، واقعاً سخت است که فقط با نگاه کردن به ردیابی ترکیب‌بندی در اینجا متوجه شوید.

  • کاری که اکنون باید انجام دهید این است که گزارش ردیابی روش‌ها را بگیرید.

  • چیزی مثل یک مورد در نمایه‌ساز در Android Studio وجود دارد، یا می‌توانید از فرمان‌های ADB برای دریافت ردگیری استفاده کنید، و آنچه به شما می‌دهد هر فراخوانی تابع است که در دنیای Java اتفاق می‌افتد.

  • این شامل هر چیزی است که Compose Compiler در آنجا کدگذاری می‌کند، هر چیزی که Compose Runtime انجام می‌دهد، و همچنین هر چیزی که کد شما انجام می‌دهد، و برای ردیابی و بررسی دقیق آنچه درحال اجرا است و اینکه کدام توابع در آن فضا فراخوانی شده‌اند، بسیار مفید است.

  • یک چیز، می‌خواهم-- این مورد را در وسط ببینم.

  • می‌خواهم- می‌خواهم اشاره کنم که ردیابی روش‌ها سربار بسیار بالایی دارد.

  • شما ۱۰ تا ۲۰ برابر سربار بیشتری دارید، بسته به اینکه مثلاً یک درخت تابع چقدر پیچیده است، چون هر فراخوانی تابع کمی به آن اضافه می‌کند.

  • و برای نشان دادن آن، می‌توانیم به نماگرفت نگاه کنیم.

  • این معمولاً همان چیزی است که گزارش ردیابی روش‌ها برای شروع برنامه به‌نظر می‌رسد، و ممکن است بپرسید: «چگونه این را درک کنم؟»

  • خب، معمولاً ردیابی ترکیب‌بندی را شروع می‌کنید، به توابعی که اینجا دارید نگاه می‌کنید.

  • بنابراین، این مانند ردیابی ترکیب‌بندی ساده‌شده است، جایی که آن‌ها مانند ScreenContent، برخی دکمه‌ها در آنجا، و برخی توابع دیگر دارند.

  • فرض کنید می‌خواهیم بفهمیم در ScreenContent چه اتفاقی می‌افتد.

  • خب، Perfetto نوار جستجو دارد.

  • ردیابی روشتان را در نمایشگر Perfetto بار می‌کنید، همان تابع را که اینجا دارید، ScreenContent، جستجو می‌کنید، به چیزی شبیه به آن می‌رسید که ردیابی روش برای صفحه است.

  • بنابراین، می‌توانید در اینجا ببینید که مثلاً چند Composer.startGroup و تماس گروهی در اطراف وجود دارد.

  • این همان کاری است که زمان‌های اجرا و کامپایلر در پس‌زمینه برای شما انجام می‌دهند تا مطمئن شوند که ترکیب‌بندی شما واقعاً جادوی خود را انجام می‌دهد، اما فراخوانی جالب در اینجا Composer.changed است.

  • به این ترتیب بررسی می‌کنیم که آیا پارامترها بین فراخوانی‌های قبلی و فراخوانی فعلی composable تغییر کرده‌اند یا نه.

  • و اکثر این Composer.changed تحت سلطه این ScreenModel.equals است.

  • بنابراین، شاید ScreenModel نوعی کلاس کلان داده باشد، و شاید ما آن را پایدار کرده‌ایم تا به تعداد بازترکیب کمک کند، بنابراین شاید چیزی باشد که در گزارش کامپایلر نشان داده شده است.

  • و شاید برداشتن این حاشیه‌نویسی پایدار به عملکرد در این مورد خاص کمک کند.

  • بنابراین، باید به‌دنبال آن باشید و گزارش ردیابی روش‌های ترکیب‌بندی بهترین ابزار برای یافتن آن است.

  • بسیار خوب.

  • امروز چیزهای زیادی را بررسی کردیم، پس حالا نوبت Andrew است که جمع‌بندی کند.

ابزارهای عملکرد و طراحی ترکیب‌پذیر سریع

  • برای جمع‌بندی و ارائه خلاصه‌ای کلی از همه چیزهایی که امروز درباره آن‌ها صحبت کردیم، چند نکته وجود دارد که باید هنگام طراحی composable و سپس بهینه‌سازی برنامه‌هایی که درحال‌حاضر دارید، درنظر داشته باشید.

  • قبل‌از هر چیز، به‌یاد داشته باشید که ترکیب‌پذیرهای ارزان‌تر و ترکیب‌پذیرهایی که باطل نمی‌کنید بهتر از ترکیب‌پذیرهایی هستند که از آن‌ها رد می‌شوید.

  • به‌یاد داشته باشید که هنگام طراحی composable، بهتر است آرگومان‌هایی را درنظر بگیرید که کوچک‌تر هستند و مقایسه آن‌ها آسان‌تر است و احتمالاً مقایسه‌ها را دربرابر سایر فراخوانی‌های تابع مجدداً استفاده کنید تا از بهینه‌سازی بهره ببرید.

  • وقتی از میاناهای برنامه‌سازی کاربردی در داخل ترکیب‌پذیرهایتان استفاده می‌کنید، توجه داشته باشید که عملکرد یا زمان پردازش فریم حاصل هزینه خود میاناهای برنامه‌سازی کاربردی و تعداد دفعات استفاده شما از آن‌ها خواهد بود.

  • به نحوه جمع شدن این هزینه‌ها توجه داشته باشید و همچنین توجه داشته باشید که ممکن است هزینه‌های تخریب مرتبط با آن‌ها وجود داشته باشد که ممکن است در نامناسب‌ترین زمان برای برنامه شما رخ دهد.

  • همچنین، به‌یاد داشته باشید که هنگام بهینه‌سازی در داخل خود برنامه، مهم است که شهروند خوبی باشید.

  • با انجام محک‌زنی شروع کنید تا تصمیمات آگاهانه بگیرید، برنامه‌تان را نمایه کنید تا مطمئن شوید که بخش‌های درست کدتان را هدف قرار می‌دهید، و تأیید کنید که تغییرات را اعمال می‌کنید و نتایجی را که از اصلاحات انتظار دارید می‌بینید، اصلاحاتی که درنتیجه محک‌زنی و نمایه‌سازی انجام می‌دهید.

  • همچنین به‌خاطر داشته باشید که ممکن است بین قابلیت نگهداری و عملکرد در داخل برنامه شما موازنه‌ای وجود داشته باشد.

  • بنابراین، کاری را انجام دهید که برای شما و تیمتان بهترین است تا مطمئن شوید که بیش‌ازحد بهینه‌سازی نمی‌کنید و خودتان را در گوشه‌ای با قابلیت نگهداری قرار نمی‌دهید.

  • And also, for more help, there's one more tool at your disposal, and it's the AI agent that you already have.

  • ما به‌عنوان بخشی از Android CLI و به‌عنوان بخشی از مهارت‌هایی که در آنجا ارائه می‌دهیم، یک مهارت عملکرد Jetpack Compose را راه‌اندازی کرده‌ایم که اطلاعات زیادی را که امروز درباره آن‌ها صحبت کردیم دارد و می‌تواند برنامه شما را بررسی و ممیزی کند و پیشنهادهایی برای این نوع بهینه‌سازی‌ها ارائه دهد.

  • می‌توانید آن را بااستفاده از Android CLI در نماینده موجودتان نصب کنید.

    android skills add jetpack-compose-performance
  • برای کسب اطلاعات بیشتر درباره میانای خط فرمان Android، می‌توانید به developer.android.com مراجعه کنید.

  • همچنین می‌توانید در مخزن GitHub ما درباره خود مهارت‌های ما بیشتر بدانید.

  • با این اوصاف، محک‌زنی ایمن و ترکیب‌بندی خوبی داشته باشید.

کد

تکه‌کدها

قطعه Kotlin در ۰۹:۳۴ در «مدیریت عوارض جانبی»
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Asynchronous work and animations
    LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
}
قطعه Kotlin در دقیقه ۱۰:۱۵ در «مدیریت عوارض جانبی»
@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 در دقیقه ۱۰:۳۶ در «مدیریت عوارض جانبی»
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // One-shot events
    SideEffect(Unit) { mediaPlayer.play() }
}
قطعه Kotlin در ۱۱:۱۲ در «مدیریت عوارض جانبی»
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Synchronize the outside world to the state of composition
    SideEffect { mediaPlayer.playbackRate = playbackRate }
}
قطعه Kotlin در ۱۲:۱۲ در «مدیریت عوارض جانبی»
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) { viewModel.loadCart() }
    LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
    LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }

    CartContent(...)
}
قطعه Kotlin در ۱۲:۳۶ در «مدیریت عوارض جانبی»
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) {
        launch { viewModel.loadCart() }
        launch { viewModel.loadPaymentMethods() }
        launch { viewModel.loadDeliveryAddresses() }
    }

    CartContent(...)
}
قطعه Kotlin در ۱۵:۱۲ در «بهینه‌سازی وضعیت و باطل‌سازی‌ها»
@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 در ۱۵:۵۱ در «بهینه‌سازی وضعیت و باطل‌سازی‌ها»
@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 در ۱۶:۴۹ در «بهینه‌سازی وضعیت و باطل‌سازی‌ها»
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 در ۱۸:۳۵ در «بهینه‌سازی وضعیت و باطل‌سازی‌ها»
var a by mutableStateOf(0)

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

...

// Read-after-write
a++
derivedState.value
computedState.value
قطعه Kotlin در ۲۱:۰۷ در «رد شدن و پایداری در 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 در ۲۹:۴۰ در «ابزارهای عملکرد و طراحی ترکیب‌پذیر سریع»
android skills add jetpack-compose-performance