رندر آهسته

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

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

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

شناسایی جنک

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

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

بازرسی بصری

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

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

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

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

سیستریس

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

هنگام انجام مورد استفاده‌ی jank در دستگاه خود، با Systrace یک ردیابی ثبت کنید. برای دستورالعمل‌های نحوه‌ی استفاده از Systrace، به Capture a system trace on the command line مراجعه کنید. Systrace به فرآیندها و نخ‌ها تقسیم می‌شود. در Systrace به دنبال فرآیند برنامه‌ی خود بگردید که چیزی شبیه شکل ۱ است.

مثال سیستم
شکل ۱. مثال Systrace.

مثال Systrace در شکل 1 شامل اطلاعات زیر برای شناسایی jank است:

  1. Systrace زمان ترسیم هر فریم را نشان می‌دهد و برای برجسته کردن زمان‌های رندر کند، هر فریم را با رنگ‌های مختلف کدگذاری می‌کند. این به شما کمک می‌کند فریم‌های مشکل‌دار را با دقت بیشتری نسبت به بازرسی بصری پیدا کنید. برای اطلاعات بیشتر، به Inspect UI frames and alerts مراجعه کنید.
  2. Systrace مشکلات برنامه شما را تشخیص می‌دهد و هشدارها را هم در فریم‌های جداگانه و هم در پنل هشدارها نمایش می‌دهد. بهتر است دستورالعمل‌های موجود در هشدار را دنبال کنید.
  3. بخش‌هایی از چارچوب اندروید و کتابخانه‌ها، مانند RecyclerView ، حاوی نشانگرهای ردیابی هستند. بنابراین، جدول زمانی systrace نشان می‌دهد که این متدها چه زمانی در نخ UI اجرا می‌شوند و چقدر طول می‌کشد تا اجرا شوند.

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

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

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

نظارت بر عملکرد سفارشی

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

برای انجام این کار، زمان رندر فریم را از قسمت‌های خاصی از برنامه خود با FrameMetricsAggregator جمع‌آوری کنید و داده‌ها را با استفاده از Firebase Performance Monitoring ثبت و تجزیه و تحلیل کنید.

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

قاب‌های یخ‌زده

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

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

ردیابی بی‌فایده

FrameTimeline در Perfeto می‌تواند در ردیابی فریم‌های کند یا فریز شده کمک کند.

رابطه بین فریم‌های کند، فریم‌های ثابت و ANRها

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

فریم‌های کند قاب‌های یخ‌زده ANR ها
زمان رندر بین ۱۶ میلی‌ثانیه تا ۷۰۰ میلی‌ثانیه بین ۷۰۰ میلی‌ثانیه تا ۵ ثانیه بزرگتر از ۵ ثانیه
ناحیه تأثیر کاربر قابل مشاهده
  • اسکرول RecyclerView به طور ناگهانی رفتار می‌کند
  • در صفحاتی که انیمیشن‌های پیچیده به درستی نمایش داده نمی‌شوند
  • در طول راه‌اندازی برنامه
  • حرکت از یک صفحه به صفحه دیگر - برای مثال، انتقال صفحه
  • در حالی که activity شما در پیش‌زمینه است، برنامه شما ظرف پنج ثانیه به یک رویداد ورودی یا BroadcastReceiver - مانند رویدادهای فشردن کلید یا لمس صفحه - پاسخی نداده است.
  • در حالی که شما هیچ فعالیتی در پیش‌زمینه ندارید، BroadcastReceiver شما در مدت زمان قابل توجهی اجرای خود را به پایان نرسانده است.

فریم‌های کند و فریم‌های یخ‌زده را جداگانه ردیابی کنید

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

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

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

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

رفع مشکل

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

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

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

منابع رایج مواد مخدر

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

لیست‌های قابل اسکرول

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

RecyclerView: notifyDataSetChanged()

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

برای مثال، برنامه‌ای را در نظر بگیرید که نسخه جدیدی از فهرستی از محتوای خبری را از سرور دریافت می‌کند. وقتی این اطلاعات را به Adapter ارسال می‌کنید، می‌توانید تابع 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: RecyclerViewهای تو در تو

معمولاً چندین نمونه از RecyclerView به صورت تو در تو ساخته می‌شوند، مخصوصاً با یک لیست عمودی از لیست‌هایی که به صورت افقی اسکرول می‌شوند. نمونه‌ای از این مورد، جدول‌بندی برنامه‌ها در صفحه اصلی Play Store است. این روش می‌تواند عالی عمل کند، اما در عین حال تعداد زیادی view نیز در حال حرکت هستند.

اگر هنگام اولین اسکرول کردن به پایین صفحه، تعداد زیادی آیتم داخلی را در حال افزایش حجم می‌بینید، شاید بهتر باشد بررسی کنید که آیا RecyclerView.RecycledViewPool را بین نمونه‌های داخلی (افقی) RecyclerView به اشتراک می‌گذارید یا خیر. به طور پیش‌فرض، هر RecyclerView مجموعه آیتم‌های خود را دارد. با این حال، در حالتی که دوازده itemViews به طور همزمان روی صفحه نمایش داده می‌شوند، اگر همه ردیف‌ها انواع مشابهی از viewها را نشان دهند، وقتی 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: تورم بیش از حد یا ایجاد خیلی طول می‌کشد

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

اگر مرتباً شاهد ایجاد وقفه در نمایش آیتم‌های جدید روی صفحه هستید، بررسی کنید که تعداد انواع نماها (view types) بیش از نیاز شما نباشد. هرچه تعداد انواع نماها در محتوای RecyclerView کمتر باشد، هنگام نمایش انواع آیتم‌های جدید روی صفحه، نیاز به ایجاد وقفه کمتری وجود دارد. در صورت امکان، در صورت لزوم، انواع نماها را با هم ادغام کنید. اگر فقط یک آیکون، رنگ یا متن بین انواع تغییر می‌کند، می‌توانید آن تغییر را در زمان اتصال (bind time) اعمال کنید و از وقفه در نمایش (inflammation) جلوگیری کنید، که این امر باعث کاهش مصرف حافظه برنامه شما در همان زمان می‌شود.

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

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

RecyclerView: اتصال خیلی طول می‌کشد

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

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

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

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

نمای لیست: تورم

اگر مراقب نباشید، ممکن است به‌طور تصادفی قابلیت بازیافت را در 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 نشان دهد که بخش Layout از Choreographer#doFrame بیش از حد یا اغلب اوقات کار می‌کند، این بدان معناست که شما با مشکلات عملکرد Layout مواجه هستید. عملکرد Layout برنامه شما بستگی به این دارد که کدام بخش از سلسله مراتب view دارای پارامترها یا ورودی‌های Layout در حال تغییر است.

عملکرد طرح بندی: هزینه

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

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

  • دیدگاه‌های ساختاری خود را مجدداً سازماندهی کنید.
  • منطق طرح‌بندی سفارشی را تعریف کنید. برای مثال خاص به Optimize layout hierarchies مراجعه کنید. می‌توانید تبدیل به ConstraintLayout را امتحان کنید که ویژگی‌های مشابهی را بدون مشکلات عملکردی ارائه می‌دهد.

عملکرد طرح‌بندی: فرکانس

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

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

شما می‌توانید همه این موارد را بسیار ارزان‌تر از ویژگی‌های طرح‌بندی، مانند padding یا margin، تغییر دهید. به‌طورکلی، تغییر ویژگی‌های ترسیم یک نما با فراخوانی یک setter که باعث اجرای invalidate() و به دنبال آن draw(Canvas) در فریم بعدی می‌شود، بسیار ارزان‌تر نیز هست. این کار عملیات ترسیم را برای نمایی که نامعتبر شده است، دوباره ثبت می‌کند و به‌طورکلی بسیار ارزان‌تر از طرح‌بندی است.

عملکرد رندرینگ

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

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

عملکرد رندرینگ: UI Thread

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

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

کاتلین

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 ارسال می‌شود، فراخوانی می‌شود، اندروید ابتدا این مسیرها را روی CPU رسم کرده و آنها را به GPU آپلود می‌کند. اگر مسیرهای بزرگی دارید، از ویرایش آنها از فریمی به فریم دیگر خودداری کنید تا بتوان آنها را به طور کارآمد ذخیره و رسم کرد. drawPoints() ، drawLines() و drawRect/Circle/Oval/RoundRect() کارآمدتر هستند و استفاده از آنها حتی اگر از فراخوانی‌های ترسیم بیشتری استفاده کنید، بهتر است.

Canvas.clipPath

clipPath(Path) رفتار برش پرهزینه‌ای را ایجاد می‌کند و عموماً باید از آن اجتناب شود. در صورت امکان، به جای برش به مستطیل‌های غیرمستطیلی، رسم شکل‌ها را انتخاب کنید. این روش عملکرد بهتری دارد و از anti-aliasing پشتیبانی می‌کند. برای مثال، فراخوانی 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);
آپلودهای بیت‌مپ

اندروید بیت‌مپ‌ها را به صورت بافت‌های OpenGL نمایش می‌دهد و اولین باری که یک بیت‌مپ در یک فریم نمایش داده می‌شود، به GPU آپلود می‌شود. می‌توانید این را در Systrace به صورت Texture upload(id) width x height مشاهده کنید. این کار می‌تواند چندین میلی‌ثانیه طول بکشد، همانطور که در شکل 2 نشان داده شده است، اما برای نمایش تصویر با GPU ضروری است.

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

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

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

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

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

گاهی اوقات، jank به این دلیل رخ می‌دهد که UI Thread برنامه شما مسدود شده یا در حال اجرا نیست. Systrace از رنگ‌های مختلفی، همانطور که در شکل 3 نشان داده شده است، برای نشان دادن زمانی که یک thread در حالت خواب (خاکستری)، قابل اجرا (آبی: می‌تواند اجرا شود، اما هنوز توسط زمانبند برای اجرا انتخاب نشده است)، فعال در حال اجرا (سبز) یا در حالت خواب بدون وقفه (قرمز یا نارنجی) است، استفاده می‌کند. این برای اشکال‌زدایی مشکلات jank که ناشی از تأخیر در زمان‌بندی thread هستند، بسیار مفید است.

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

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

اگر تراکنش‌های binder دارید، می‌توانید call stack های آنها را با دستورات 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 در حالت خواب است. منطق bind خود را متمرکز نگه دارید و از trace-ipc برای ردیابی و حذف فراخوانی‌های binder استفاده کنید.
شکل ۴. نخ رابط کاربری به دلیل تراکنش‌های binder در یک RV fling در حالت خواب است. منطق bind خود را ساده نگه دارید و trace-ipc برای ردیابی و حذف فراخوانی‌های binder استفاده کنید.

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

تخصیص شیء و جمع‌آوری زباله

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

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

زمان اجرای GC در HeapTaskDaemon، ۹۴ میلی‌ثانیه است.
شکل ۵. یک GC با زمان ۹۴ میلی‌ثانیه روی نخ HeapTaskDaemon.

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

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}