پرداز آهسته

پرداز UI عمل تولید قاب از برنامه شما و نمایش آن در صفحه است. برای کمک به اطمینان از اینکه تعامل کاربر با برنامه‌تان روان است، برنامه‌تان باید فریم‌ها را در کمتر از ۱۶ میلی‌ثانیه ارائه دهد تا به ۶۰ فریم در ثانیه (fps) برسد. برای درک اینکه چرا ۶۰ فریم در ثانیه ترجیح داده می‌شود، الگوهای عملکرد Android: چرا ۶۰ فریم در ثانیه؟ را ببینید. اگر می‌خواهید به ۹۰ فریم در ثانیه برسید، این پنجره به ۱۱ میلی‌ثانیه کاهش می‌یابد و برای ۱۲۰ فریم در ثانیه، ۸ میلی‌ثانیه است.

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

اگر بازی‌هایی توسعه می‌دهید که از سیستم View استفاده نمی‌کنند، Choreographer را دور می‌زنید. در این حالت، کتابخانه Frame Pacing به بازی‌های OpenGL و Vulkan کمک می‌کند تا به پرداز هموار و سرعت قاب صحیح در Android دست یابند.

شناسایی کردن پرش

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

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

بازرسی دیداری

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

در اینجا چند نکته برای انجام بازرسی‌های بصری آورده شده است:

  • نسخه انتشار—یا حداقل غیرقابل‌اشکال‌زدایی—برنامه‌تان را اجرا کنید. زمان اجرای ART چندین بهینه‌سازی مهم را برای پشتیبانی از ویژگی‌های اشکال‌زدایی غیرفعال می‌کند، بنابراین مطمئن شوید که چیزی شبیه به آنچه کاربر می‌بیند را مشاهده می‌کنید.
  • نمایه پردازنده گرافیکی پرداخت را فعال کنید. «پرداز GPU نمایه» نوارهایی را روی صفحه نمایش می‌دهد که نمایشی تصویری از مدت زمان لازم برای پرداز قاب‌های پنجره واسط کاربر نسبت به معیار ۱۶ میلی‌ثانیه برای هر قاب ارائه می‌دهد. هر نوار دارای مؤلفه‌های رنگی است که به مرحله‌ای در خط لوله پردازشی نگاشت می‌شود، بنابراین می‌توانید ببینید کدام بخش بیشترین زمان را می‌برد. برای مثال، اگر قاب زمان زیادی را صرف رسیدگی به ورودی می‌کند، به کد برنامه‌تان که ورودی کاربر را مدیریت می‌کند نگاه کنید.
  • اجرای مؤلفه‌هایی که منابع رایج پرش هستند، مانند RecyclerView.
  • برنامه را از راه‌اندازی سرد اجرا کنید.
  • برنامه‌تان را در دستگاهی کندتر اجرا کنید تا مشکل تشدید شود.

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

Systrace

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

هنگام اجرای مورد استفاده با تأخیر در دستگاهتان، ردگیری را با Systrace ضبط کنید. برای دستورالعمل‌های نحوه استفاده از Systrace، به ضبط ردیابی سیستم در خط فرمان مراجعه کنید. Systrace براساس فرایندها و رشته‌ها تقسیم می‌شود. فرایند برنامه‌تان را در Systrace جستجو کنید که چیزی شبیه به شکل ۱ است.

نمونه Systrace
شکل ۱. نمونه Systrace.

نمونه Systrace در شکل ۱ حاوی اطلاعات زیر برای شناسایی لنگش است:

  1. Systrace نشان می‌دهد که هر فریم چه زمانی ترسیم می‌شود و هر فریم را با کد رنگی مشخص می‌کند تا زمان‌های پرداز آهسته را برجسته کند. این کار به شما کمک می‌کند فریم‌های نامنظم را دقیق‌تر از بازرسی بصری پیدا کنید. برای اطلاعات بیشتر، بازرسی قاب‌ها و هشدارهای رابط کاربری را ببینید.
  2. Systrace مشکلات موجود در برنامه شما را شناسایی می‌کند و هشدارها را هم در فریم‌های جداگانه و هم در پانل هشدارها نمایش می‌دهد. بهتر است دستورالعمل‌های موجود در هشدار را دنبال کنید.
  3. بخش‌هایی از چارچوب و کتابخانه‌های Android، مانند RecyclerView، حاوی نشانگرهای ردیابی هستند. بنابراین، خط زمان systrace نشان می‌دهد که این روش‌ها چه زمانی در رشته میانای کاربر اجرا می‌شوند و اجرای آن‌ها چقدر طول می‌کشد.

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

اگر Systrace جزئیات مربوط به دلیل طولانی شدن کار رشته میانای کاربر را نشان نمی‌دهد، از تحلیل‌گر واحد پردازش مرکزی Android برای ضبط کردن رد روش نمونه‌برداری‌شده یا ابزاردار استفاده کنید. به‌طور کلی، گزارش ردیابی روش‌ها برای شناسایی کندی مناسب نیستند زیرا به‌دلیل سربار سنگین، کندی‌های مثبت کاذب ایجاد می‌کنند و نمی‌توانند تشخیص دهند که رشته‌ها درحال اجرا هستند یا مسدود شده‌اند. اما، ردیابی روش‌ها می‌تواند به شما کمک کند روش‌هایی را در برنامه‌تان شناسایی کنید که بیشترین زمان را می‌برند. پس‌از شناسایی این روش‌ها، نشانگرهای Trace را اضافه کنید و Systrace را دوباره اجرا کنید تا ببینید آیا این روش‌ها باعث لرزش می‌شوند یا نه.

برای اطلاعات بیشتر، به آشنایی با Systrace مراجعه کنید.

پایش عملکرد سفارشی

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

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

برای کسب اطلاعات بیشتر، شروع به کار با «پایش عملکرد» برای Android را ببینید.

قاب‌های ثابت‌شده

قاب‌های ثابت قاب‌های واسط کاربری هستند که پرداز آن‌ها بیشتر از ۷۰۰ میلی‌ثانیه طول می‌کشد. این یک مشکل است زیرا به‌نظر می‌رسد برنامه شما گیر کرده است و تقریباً برای یک ثانیه کامل درحالی‌که قاب درحال رندر شدن است به ورودی کاربر پاسخ نمی‌دهد. توصیه می‌کنیم برنامه‌ها را بهینه‌سازی کنید تا قاب را ظرف ۱۶ میلی‌ثانیه ارائه دهند تا از روان بودن واسط کاربر اطمینان حاصل شود. بااین‌حال، درطول راه‌اندازی برنامه یا هنگام انتقال به صفحه‌ای دیگر، طبیعی است که ترسیم قاب اولیه بیشتر از ۱۶ میلی‌ثانیه طول بکشد زیرا برنامه شما باید نماها را گسترش دهد، صفحه را چیدمان کند، و ترسیم اولیه را از ابتدا انجام دهد. به‌همین دلیل است که Android فریم‌های ثابت را جدا از پرداز آهسته ردیابی می‌کند. هیچ قابی در برنامه‌تان نباید بیشتر از ۷۰۰ میلی‌ثانیه برای پرداز زمان ببرد.

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

ردیابی کردن پرش تصویر

FrameTimeline در Perfetto می‌تواند به ردیابی فریم‌های کند یا ثابت کمک کند.

رابطه بین قاب‌های آهسته، قاب‌های ثابت، و خطاهای ANR

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

قاب‌های آهسته قاب‌های ثابت‌شده تعداد خطاهای «برنامه پاسخ نمی‌دهد»
زمان پرداز زدن بین ۱۶ میلی‌ثانیه و ۷۰۰ میلی‌ثانیه بین ۷۰۰ میلی‌ثانیه و ۵ ثانیه بیشتر از ۵ ثانیه
ناحیه تأثیر قابل‌مشاهده بر کاربر
  • پیمایش RecyclerView به‌طور ناگهانی رفتار می‌کند
  • در صفحه‌هایی با پویانمایی‌های پیچیده که به‌درستی پویانمایی نمی‌شوند
  • هنگام راه‌اندازی برنامه
  • انتقال از یک صفحه به صفحه دیگر—برای مثال، انتقال صفحه
  • درحالی‌که فعالیت شما در پیش‌زمینه است، برنامه‌تان به رویداد ورودی یا BroadcastReceiver (مثلاً رویدادهای فشار دادن کلید یا ضربه زدن روی صفحه) ظرف پنج ثانیه پاسخ نداده است.
  • درحالی‌که فعالیتی در پیش‌زمینه ندارید، BroadcastReceiver شما در مدت زمان قابل‌توجهی به پایان نرسیده است.

ردیابی فریم‌های آهسته و فریم‌های ثابت به‌صورت جداگانه

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

روال‌های مطلوب برای اولویت‌بندی و حل کردن پرش

هنگام تلاش برای حل مشکل لرزش در برنامه‌تان، روال‌های مطلوب زیر را درنظر داشته باشید:

  • نمونه‌های پرش تصویر را که به‌راحتی قابل‌بازآفرینی هستند شناسایی و برطرف کنید.
  • خطاهای «برنامه پاسخ نمی‌دهد» را اولویت‌بندی کنید. درحالی‌که فریم‌های کند یا ثابت ممکن است باعث شود برنامه کند به‌نظر برسد، ANR باعث می‌شود برنامه پاسخ ندهد.
  • بازتولید پرداز آهسته دشوار است، اما می‌توانید با ازبین بردن فریم‌های ثابت‌شده ۷۰۰ میلی‌ثانیه‌ای شروع کنید. این مشکل معمولاً هنگام راه‌اندازی برنامه یا تغییر صفحه‌ها رخ می‌دهد.

رفع اشکال قطع اتصال

برای رفع لرزش، بررسی کنید کدام فریم‌ها در ۱۶ میلی‌ثانیه تکمیل نمی‌شوند و ببینید مشکل چیست. بررسی کنید که آیا Record View#draw یا Layout در برخی‌از قاب‌ها به‌طور غیرعادی طولانی است یا نه. برای این مشکلات و مشکلات دیگر، منابع رایج لرزش را ببینید.

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

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

منابع رایج پرش تصویر

بخش‌های زیر منابع رایج پرش در برنامه‌هایی را که از سیستم View استفاده می‌کنند و روال‌های مطلوب برای رفع آن‌ها را توضیح می‌دهند. برای کسب اطلاعات درباره رفع مشکلات عملکرد با Jetpack Compose، به عملکرد Jetpack Compose مراجعه کنید.

فهرست‌های قابل‌پیمایش

ListView—و به‌ویژه RecyclerView—معمولاً برای فهرست‌های پیمایشی پیچیده که بیشتر مستعد پرش هستند استفاده می‌شوند. هر دو حاوی نشانگرهای Systrace هستند، بنابراین می‌توانید از Systrace استفاده کنید تا ببینید آیا آن‌ها در ایجاد لرزش در برنامه‌تان نقش دارند یا نه. آرگومان خط فرمان -a <your-package-name> را ارسال کنید تا بخش‌های ردیابی در RecyclerView—و همچنین هر نشانگر ردیابی که اضافه کرده‌اید—نمایش داده شوند. درصورت دردسترس بودن، از راهنمایی هشدارهای تولیدشده در خروجی Systrace پیروی کنید. در Systrace، می‌توانید روی بخش‌های ردیابی‌شده RecyclerView کلیک کنید تا توضیحی درباره کاری که RecyclerView انجام می‌دهد ببینید.

RecyclerView: notifyDataSetChanged()

اگر می‌بینید که هر مورد در RecyclerView شما درحال اتصال مجدد است—و بنابراین دوباره چیدمان می‌شود و در یک فریم دوباره ترسیم می‌شود—مطمئن شوید که برای به‌روزرسانی‌های کوچک notifyDataSetChanged()، setAdapter(Adapter)، یا swapAdapter(Adapter, boolean) را فراخوانی نمی‌کنید. این روش‌ها نشان می‌دهند که تغییراتی در کل محتوای فهرست وجود دارد و در Systrace به‌صورت RV FullInvalidate نشان داده می‌شوند. درعوض، از SortedList یا DiffUtil برای تولید به‌روزرسانی‌های حداقلی هنگام تغییر یا افزودن محتوا استفاده کنید.

برای مثال، برنامه‌ای را درنظر بگیرید که نسخه جدیدی از فهرست محتوای خبری را از سرور دریافت می‌کند. وقتی این اطلاعات را در «آداپتور» پست می‌کنید، می‌توانید notifyDataSetChanged() را فراخوانی کنید، همان‌طور که در مثال زیر نشان داده شده است:

کاتلین

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

جاوا

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

نکته منفی این است که اگر تغییر جزئی وجود داشته باشد، مثلاً یک مورد به بالا اضافه شده باشد، RecyclerView متوجه نمی‌شود. بنابراین، به آن گفته می‌شود که کل وضعیت مورد ذخیره‌شده در حافظه نهان را رها کند و درنتیجه باید همه چیز را دوباره متصل کند.

توصیه می‌کنیم از DiffUtil استفاده کنید که حداقل به‌روزرسانی‌ها را برای شما محاسبه و ارسال می‌کند:

کاتلین

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

جاوا

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

برای اطلاع دادن به DiffUtil درباره نحوه بازرسی فهرست‌هایتان، MyCallback خود را به‌عنوان پیاده‌سازی Callback تعریف کنید.

RecyclerView: RecyclerViews تودرتو

تودرتو کردن چند نمونه از RecyclerView رایج است، به‌ویژه با فهرست عمودی از فهرست‌های پیمایش افقی. نمونه‌ای از این مورد، شبکه‌های برنامه‌ها در صفحه اصلی «فروشگاه Play» است. این کار می‌تواند عالی باشد، اما همچنین تعداد زیادی بازدید درحال جابه‌جایی است.

اگر وقتی برای اولین‌بار صفحه را به‌پایین پیمایش می‌کنید، موارد داخلی زیادی را می‌بینید که درحال افزایش هستند، شاید بهتر باشد بررسی کنید که RecyclerView.RecycledViewPool را بین نمونه‌های داخلی (افقی) RecyclerView هم‌رسانی می‌کنید. به‌طور پیش‌فرض، هر RecyclerView مجموعه موارد خودش را دارد. بااین‌حال، در مواردی که ده‌ها itemViews به‌طور هم‌زمان روی صفحه وجود دارد، اگر همه ردیف‌ها انواع مشابهی از نماها را نشان دهند، مشکل‌ساز است که itemViews نتواند توسط فهرست‌های افقی مختلف هم‌رسانی شود.

کاتلین

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

جاوا

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

اگر می‌خواهید بیشتر بهینه‌سازی کنید، می‌توانید setInitialPrefetchItemCount(int) را در LinearLayoutManager RecyclerView داخلی نیز فراخوانی کنید. برای مثال، اگر همیشه ۳.۵ مورد در یک ردیف قابل‌مشاهده است، innerLLM.setInitialItemPrefetchCount(4) را فراخوانی کنید. این به RecyclerView علامت می‌دهد که وقتی ردیف افقی درحال آمدن روی صفحه‌نمایش است، باید تلاش کند موارد داخل آن را پیش‌واکشی کند، اگر زمان اضافی در رشته میانای کاربر وجود داشته باشد.

RecyclerView: تورم بیش‌ازحد یا ایجاد بیش‌ازحد طول می‌کشد

در اکثر موارد، ویژگی واکشی پیشاپیش در RecyclerView می‌تواند با انجام کار پیش‌از موعد درحالی‌که رشته میانای کاربر غیرفعال است، به حل مشکل هزینه تورم کمک کند. اگر درطول یک قاب و نه در بخشی با برچسب RV Prefetch شاهد تورم هستید، مطمئن شوید که در دستگاه پشتیبانی‌شده‌ای آزمایش می‌کنید و از نسخه جدیدی از کتابخانه پشتیبانی استفاده می‌کنید. پیش‌واکشی فقط در Android 5.0 سطح API‏ ۲۱ و نسخه‌های جدیدتر پشتیبانی می‌شود.

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

اگر انواع نمای شما خوب به‌نظر می‌رسد، هزینه تورم خود را کاهش دهید. کاهش نماهای ساختاری و محفظه‌ای غیرضروری می‌تواند کمک کند. ساختن itemViews با ConstraintLayout را درنظر بگیرید، که می‌تواند به کاهش نماهای ساختاری کمک کند.

اگر می‌خواهید عملکرد را بیشتر بهینه‌سازی کنید و سلسله‌مراتب موارد شما ساده است و به ویژگی‌های پیچیده زمینه‌سازی و سبک نیاز ندارید، می‌توانید خودتان سازنده‌ها را فراخوانی کنید. بااین‌حال، اغلب ارزش ازدست دادن سادگی و ویژگی‌های XML را ندارد.

RecyclerView: Bind taking too long

اتصال—یعنی onBindViewHolder(VH, int)— باید ساده باشد و برای همه موارد به‌جز پیچیده‌ترین آن‌ها، بسیار کمتر از یک میلی‌ثانیه طول بکشد. این متد باید موارد شیء قدیمی ساده جاوا (POJO) را از داده‌های داخلی آداپتور شما بگیرد و متدهای تنظیم‌کننده را روی نماها در ViewHolder فراخوانی کند. اگر RV OnBindView زمان زیادی می‌برد، بررسی کنید که در کد bind خود حداقل کار را انجام می‌دهید.

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

RecyclerView یا ListView: چیدمان یا رسم خیلی طول می‌کشد

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

ListView: Inflation

اگر دقت نکنید، ممکن است به‌طور تصادفی بازیافت را در ListView غیرفعال کنید. اگر هر بار که موردی روی صفحه‌نمایش ظاهر می‌شود، تورم می‌بینید، بررسی کنید که پیاده‌سازی شما از Adapter.getView() در حال استفاده، اتصال مجدد، و برگرداندن پارامتر convertView باشد. اگر پیاده‌سازی getView() شما همیشه متورم شود، برنامه‌تان از مزایای بازیافت در ListView بهره‌مند نمی‌شود. ساختار getView() شما تقریباً همیشه باید مشابه پیاده‌سازی زیر باشد:

کاتلین

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

جاوا

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

عملکرد چیدمان

اگر Systrace نشان دهد که بخش چیدمان Choreographer#doFrame بیش‌ازحد یا به‌طور مکرر کار می‌کند، این یعنی با مشکلات عملکرد چیدمان مواجه هستید. عملکرد چیدمان برنامه شما به این بستگی دارد که کدام بخش از سلسله‌مراتب نما دارای پارامترها یا ورودی‌های چیدمان درحال تغییر است.

عملکرد چیدمان: هزینه

اگر بخش‌ها طولانی‌تر از چند میلی‌ثانیه باشند، ممکن است به بدترین عملکرد تودرتو برای RelativeLayouts یا weighted-LinearLayouts رسیده باشید. هرکدام از این چیدمان‌ها می‌توانند چندین مرحله اندازه‌گیری و چیدمان را برای فرزندانشان راه‌اندازی کنند، بنابراین تودرتو کردن آن‌ها می‌تواند منجر به رفتار O(n^2) در عمق تودرتو شود.

سعی کنید از RelativeLayout یا ویژگی وزنLinearLayout در همه گره‌ها به‌جز پایین‌ترین گره‌های برگ سلسله‌مراتب استفاده نکنید. از راه‌های زیر می‌توانید این کار را انجام دهید:

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

عملکرد چیدمان: بسامد

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

به‌طورکلی، پویانمایی‌ها باید روی ویژگی‌های طراحی View اجرا شوند، مانند موارد زیر:

می‌توانید همه این موارد را با هزینه بسیار کمتری نسبت‌به ویژگی‌های چیدمان، مانند حاشیه داخلی یا حاشیه خارجی، تغییر دهید. به‌طورکلی، تغییر دادن ویژگی‌های طراحی نما با فراخوانی یک تنظیم‌کننده که invalidate() را راه‌اندازی می‌کند و سپس draw(Canvas) در قاب بعدی بسیار ارزان‌تر است. این کار عملیات ترسیم را برای نمایی که نامعتبر شده است دوباره ضبط می‌کند و معمولاً بسیار ارزان‌تر از چیدمان است.

عملکرد پرداز زدن

واسط کاربر Android در دو مرحله کار می‌کند:

  • Record View#draw در رشته میانای کاربر، که draw(Canvas) را در هر نمای نامعتبر اجرا می‌کند و می‌تواند تماس‌ها را در نماهای سفارشی یا در کد شما فراخوانی کند.
  • DrawFrame در RenderThread، که در RenderThread بومی اجرا می‌شود اما براساس کار تولیدشده توسط فاز Record View#draw عمل می‌کند.

عملکرد پرداز زدن: رشته میانای کاربر

اگر Record View#draw زمان زیادی می‌برد، معمولاً به این دلیل است که یک نقشه بیتی در رشته میانای کاربر درحال رنگ‌آمیزی است. نقاشی روی نقشه بیتی از پردازش واحد پردازش مرکزی استفاده می‌کند، بنابراین به‌طور کلی درصورت امکان از این کار اجتناب کنید. می‌توانید از ردیابی روش‌ها با تحلیل‌گر واحد پردازش مرکزی Android استفاده کنید تا ببینید آیا مشکل همین است یا نه.

نقاشی روی نقشه بیتی اغلب زمانی انجام می‌شود که برنامه‌ای بخواهد نقشه بیتی را قبل‌از نمایش آن تزئین کند—گاهی‌اوقات تزئینی مانند افزودن گوشه‌های گرد:

کاتلین

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

جاوا

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

اگر این نوع کاری است که در رشته میانای کاربر انجام می‌دهید، می‌توانید به‌جای آن این کار را در رشته رمزگشایی در پس‌زمینه انجام دهید. در برخی موارد، مانند مثال قبلی، حتی می‌توانید کار را در زمان ترسیم انجام دهید. بنابراین، اگر کد Drawable یا View شما چیزی شبیه به این است:

کاتلین

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

جاوا

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

می‌توانید آن را با این جایگزین کنید:

کاتلین

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

جاوا

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

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

اگر به دلیل دیگری درحال ترسیم روی نقشه بیتی هستید (احتمالاً از آن به‌عنوان حافظه نهان استفاده می‌کنید)، سعی کنید مستقیماً روی Canvas با شتاب سخت‌افزاری که به View یا Drawable شما منتقل شده است ترسیم کنید. درصورت لزوم، فراخوانی setLayerType() با LAYER_TYPE_HARDWARE را نیز درنظر بگیرید تا خروجی پردازش پیچیده را در حافظه نهان ذخیره کنید و همچنان از پردازش GPU بهره ببرید.

عملکرد پرداز زدن: RenderThread

برخی‌از عملیات Canvas برای ضبط ارزان هستند اما محاسبات گران‌قیمتی را در RenderThread راه‌اندازی می‌کنند. Systrace معمولاً این موارد را با هشدار نشان می‌دهد.

پویانمایی «مسیرهای» بزرگ

وقتی Canvas.drawPath() در Canvas با شتاب سخت‌افزاری که به View منتقل شده است فراخوانی می‌شود، Android ابتدا این مسیرها را در CPU ترسیم می‌کند و آن‌ها را در GPU بارگذاری می‌کند. اگر مسیرهای بزرگی دارید، از ویرایش آن‌ها از قابی به قاب دیگر خودداری کنید تا بتوان آن‌ها را به‌طور مؤثر ذخیره و ترسیم کرد. ‫drawPoints()، drawLines()، و drawRect/Circle/Oval/RoundRect() کارآمدتر هستند و حتی اگر از فراخوانی‌های ترسیم بیشتری استفاده کنید، بهتر است از آن‌ها استفاده کنید.

Canvas.clipPath

clipPath(Path) رفتار برش پرهزینه‌ای را راه‌اندازی می‌کند و معمولاً باید از آن اجتناب شود. درصورت امکان، به‌جای برش دادن به شکل‌های غیرمستطیلی، از کشیدن شکل استفاده کنید. این موتور عملکرد بهتری دارد و از ضدپلگی پشتیبانی می‌کند. برای مثال، فراخوانی clipPath زیر را می‌توان به‌صورت متفاوتی بیان کرد:

کاتلین

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

جاوا

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

درعوض، مثال قبلی را به‌صورت زیر بیان کنید:

کاتلین

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

جاوا

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
بارگذاری‌های بیت‌مپ

Android بیت‌مپ‌ها را به‌عنوان بافت‌های OpenGL نمایش می‌دهد و اولین باری که بیت‌مپ در یک قاب نمایش داده می‌شود، به GPU بارگذاری می‌شود. می‌توانید این را در Systrace به‌صورت بارگذاری بافت(شناسه) عرض x ارتفاع ببینید. این کار ممکن است چند میلی‌ثانیه طول بکشد، همان‌طور که در شکل ۲ نشان داده شده است، اما برای نمایش تصویر با GPU ضروری است.

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

در Android 7.0، کد بارگیری نقشه بیتی (که معمولاً توسط کتابخانه‌ها انجام می‌شود) می‌تواند prepareToDraw() را فراخوانی کند تا بارگذاری زودهنگام را قبل‌از نیاز به آن راه‌اندازی کند. به این ترتیب، بارگذاری زودتر انجام می‌شود درحالی‌که RenderThread در حالت آزاد است. می‌توانید این کار را پس‌از رمزگشایی یا هنگام اتصال نقشه بیتی به نما انجام دهید، به‌شرطی که نقشه بیتی را بشناسید. در حالت ایده‌آل، کتابخانه بار کردن نقشه بیتی شما این کار را برایتان انجام می‌دهد، اما اگر خودتان آن را مدیریت می‌کنید یا می‌خواهید مطمئن شوید که در دستگاه‌های جدیدتر با بارگذاری مواجه نمی‌شوید، می‌توانید prepareToDraw() را در کد خودتان فراخوانی کنید.

برنامه زمان قابل‌توجهی را در یک
  قاب صرف بارگذاری بیت‌مپ بزرگ می‌کند
شکل ۲. برنامه زمان قابل‌توجهی را در یک فریم صرف بارگذاری یک بیت‌مپ بزرگ می‌کند. یا اندازه آن را کاهش دهید یا وقتی آن را با prepareToDraw() رمزگشایی می‌کنید، آن را زودتر راه‌اندازی کنید.

تأخیرهای زمان‌بندی رشته

زمان‌بند رشته بخشی از سیستم‌عامل Android است که مسئولیت تصمیم‌گیری درباره اینکه کدام رشته‌ها در سیستم باید اجرا شوند، چه زمانی اجرا شوند، و برای چه مدت اجرا شوند را برعهده دارد.

گاهی اوقات، لرزش به‌دلیل مسدود شدن یا اجرا نشدن «رشته میانای کاربر» برنامه شما رخ می‌دهد. Systrace از رنگ‌های مختلفی استفاده می‌کند، همان‌طور که در شکل ۳ نشان داده شده است، تا نشان دهد که یک رشته چه زمانی خوابیده (خاکستری)، قابل اجرا (آبی: می‌تواند اجرا شود، اما هنوز توسط زمان‌بند برای اجرا انتخاب نشده است)، به‌طور فعال درحال اجرا (سبز)، یا در خواب غیرقابل وقفه (قرمز یا نارنجی) است. این ویژگی برای اشکال‌زدایی مشکلات پرش که به‌دلیل تأخیر در زمان‌بندی رشته ایجاد می‌شوند بسیار مفید است.

دوره‌ای را که رشته میانای کاربر درحال خواب است برجسته می‌کند
شکل ۳. برجسته کردن دوره‌ای که رشته میانای کاربر در حالت خواب است.

اغلب، فراخوانی‌های binder (سازوکار ارتباط بین‌فرایندی (IPC) در Android) باعث مکث‌های طولانی در اجرای برنامه شما می‌شوند. در نسخه‌های جدیدتر Android، این یکی از رایج‌ترین دلایل توقف اجرای رشته میانای کاربر است. به‌طور کلی، راه‌حل این است که از فراخوانی توابعی که تماس‌های binder برقرار می‌کنند اجتناب شود. اگر اجتناب‌ناپذیر است، مقدار را در حافظه نهان ذخیره کنید یا کار را به رشته‌های پس‌زمینه منتقل کنید. با بزرگ‌تر شدن پایگاه‌های کد، اگر دقت نکنید ممکن است با فراخوانی برخی روش‌های سطح پایین، به‌طور تصادفی یک فراخوانی binder اضافه کنید. اما می‌توانید آن‌ها را با ردیابی پیدا و برطرف کنید.

اگر تراکنش‌های binder دارید، می‌توانید پشته‌های فراخوانی آن‌ها را با فرمان‌های adb زیر ضبط کنید:

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

گاهی‌اوقات تماس‌هایی که بی‌ضرر به‌نظر می‌رسند، مثل getRefreshRate()، می‌توانند تراکنش‌های binder را راه‌اندازی کنند و وقتی به‌طور مکرر فراخوانده می‌شوند، مشکلات بزرگی ایجاد کنند. ردیابی دوره‌ای می‌تواند به شما کمک کند این مشکلات را هنگام بروز پیدا و برطرف کنید.

رشته میانای کاربر را نشان می‌دهد که به‌دلیل تراکنش‌های
  binder در RV fling در حالت خواب است. منطق اتصال را متمرکز نگه دارید و از trace-ipc برای ردیابی و حذف تماس‌های اتصال‌دهنده استفاده کنید.
شکل ۴. رشته میانای کاربر به‌دلیل تراکنش‌های صحافی در پرتاب RV در حالت خواب است. منطق اتصال را ساده نگه دارید و از trace-ipc برای ردیابی و حذف تماس‌های اتصال‌دهنده استفاده کنید.

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

تخصیص شیء و زباله‌روبی

از زمانی که ART به‌عنوان زمان اجرای پیش‌فرض در Android 5.0 معرفی شد، تخصیص شیء و جمع‌آوری زباله (GC) به‌طور قابل‌توجهی کمتر مشکل‌ساز هستند، اما هنوز هم ممکن است با این کار اضافی، رشته‌های شما سنگین شوند. اختصاص دادن در پاسخ به رویدادی نادر که چندین بار در ثانیه اتفاق نمی‌افتد (مثلاً ضربه زدن کاربر روی دکمه) مشکلی ندارد، اما به‌خاطر داشته باشید که هر اختصاص دادن هزینه‌ای دارد. اگر در حلقه تنگی است که مکرراً فراخوانی می‌شود، برای سبک کردن بار GC، از تخصیص اجتناب کنید.

Systrace به شما نشان می‌دهد که آیا GC به‌طور مکرر اجرا می‌شود یا نه، و نمایه‌ساز حافظه Android می‌تواند به شما نشان دهد که تخصیص‌ها از کجا می‌آیند. اگر درصورت امکان از تخصیص‌ها اجتناب کنید، به‌ویژه در حلقه‌های تنگ، احتمال بروز مشکل کمتر خواهد بود.

یک GC‏ ۹۴ میلی‌ثانیه‌ای را در HeapTaskDaemon نشان می‌دهد
شکل ۵. ‫۹۴ میلی‌ثانیه GC در رشته HeapTaskDaemon.

در نسخه‌های اخیر Android، «جمع‌آوری زباله» معمولاً در رشته پس‌زمینه‌ای به‌نام HeapTaskDaemon اجرا می‌شود. مقادیر قابل‌توجه تخصیص می‌تواند به معنای صرف منابع CPU بیشتر برای GC باشد، همان‌طور که در شکل ۵ نشان داده شده است.