رندر آهسته

رندر UI عمل تولید یک فریم از برنامه شما و نمایش آن بر روی صفحه است. برای اطمینان از اینکه تعامل کاربر با برنامه شما روان است، برنامه شما باید فریم ها را در کمتر از 16 میلی ثانیه ارائه کند تا به 60 فریم در ثانیه (فریم بر ثانیه) برسد. برای درک اینکه چرا 60 فریم در ثانیه ترجیح داده می شود، به الگوهای عملکرد Android: چرا 60 فریم در ثانیه مراجعه کنید؟ . اگر می‌خواهید به سرعت 90 فریم در ثانیه برسید، این پنجره به 11 میلی‌ثانیه کاهش می‌یابد و برای 120 فریم در ثانیه 8 میلی‌ثانیه است.

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

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

برای کمک به بهبود کیفیت برنامه، Android به طور خودکار برنامه شما را از نظر jank نظارت می کند و اطلاعات را در داشبورد Android vitals نمایش می دهد. برای اطلاعات در مورد نحوه جمع‌آوری داده‌ها، به نظارت بر کیفیت فنی برنامه خود با Android vitals مراجعه کنید.

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

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

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

بازرسی بصری

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

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

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

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

سیستراس

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

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

مثال Systrace
شکل 1. مثال Systrace.

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

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

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

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

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

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

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

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

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

قاب های یخ زده

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

برای کمک به بهبود کیفیت برنامه، Android به طور خودکار برنامه شما را از نظر فریم های ثابت نظارت می کند و اطلاعات را در داشبورد Android Vitals نمایش می دهد. برای اطلاعات در مورد نحوه جمع‌آوری داده‌ها، به نظارت بر کیفیت فنی برنامه خود با Android vitals مراجعه کنید.

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

جک ردیابی

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

رابطه بین فریم های آهسته، فریم های ثابت و ANR

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

فریم های آهسته قاب های یخ زده ANR ها
زمان رندر بین 16 تا 700 میلی‌ثانیه بین 700 میلی‌ثانیه تا 5 ثانیه بزرگتر از 5 ثانیه
منطقه تاثیر کاربر قابل مشاهده
  • اسکرول RecyclerView به طور ناگهانی رفتار می کند
  • در صفحه نمایش هایی با انیمیشن های پیچیده که به درستی متحرک نمی شوند
  • در حین راه اندازی اپلیکیشن
  • حرکت از یک صفحه به صفحه دیگر - برای مثال، انتقال صفحه
  • در حالی که فعالیت شما در پیش زمینه است، برنامه شما به یک رویداد ورودی یا BroadcastReceiver - مانند فشار دادن کلید یا رویدادهای ضربه زدن روی صفحه - در عرض پنج ثانیه پاسخ نداده است.
  • در حالی که فعالیتی در پیش زمینه ندارید، BroadcastReceiver شما در مدت زمان قابل توجهی اجرا را به پایان نرسانده است.

فریم های آهسته و فریم های ثابت را به طور جداگانه دنبال کنید

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

بهترین روش‌ها برای اولویت‌بندی و رفع jank

هنگامی که به دنبال رفع jank در برنامه خود هستید، بهترین روش های زیر را در نظر داشته باشید:

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

تعمیر جنک

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

برای جلوگیری از jank، کارهای طولانی مدت را به صورت ناهمزمان در خارج از رشته UI اجرا کنید. همیشه از اینکه کد شما روی چه رشته ای اجرا می شود آگاه باشید و هنگام ارسال کارهای غیر ضروری به موضوع اصلی احتیاط کنید.

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

منابع رایج jank

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

لیست های قابل پیمایش

ListView - و به ویژه RecyclerView - معمولاً برای لیست‌های پیمایشی پیچیده استفاده می‌شوند که بیشتر در معرض خطر jank هستند. هر دو حاوی نشانگرهای Systrace هستند، بنابراین می توانید از Systrace استفاده کنید تا ببینید آیا آنها در ایجاد jank در برنامه شما نقش دارند یا خیر. آرگومان خط فرمان -a <your-package-name> برای دریافت بخش‌های ردیابی در RecyclerView - و همچنین هر نشانگر ردیابی که اضافه کرده‌اید - ارسال کنید تا نمایش داده شود. در صورت موجود بودن، از راهنمایی های هشدارهای تولید شده در خروجی Systrace پیروی کنید. در داخل Systrace، می‌توانید روی بخش‌های RecyclerView -traced کلیک کنید تا توضیحی درباره کاری که 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: Nested 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 داخلی نیز فراخوانی کنید. برای مثال، اگر همیشه 3.5 مورد در یک ردیف قابل مشاهده است، innerLLM.setInitialItemPrefetchCount(4) را فراخوانی کنید. این به RecyclerView سیگنال می دهد که وقتی یک ردیف افقی می خواهد روی صفحه نمایش داده شود، اگر وقت خالی در رشته UI وجود دارد، باید سعی کند موارد داخل آن را از قبل واکشی کند.

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

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

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

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

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

RecyclerView: اتصال بیش از حد طولانی است

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

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

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

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

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

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

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

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

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

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

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

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

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

اجرای رندر

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

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

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

اگر Record View#draw زمان زیادی می برد، معمولاً یک بیت مپ روی رشته UI نقاشی می شود. نقاشی با بیت مپ از رندر CPU استفاده می کند، بنابراین به طور کلی در صورت امکان از این کار اجتناب کنید. می توانید از روش ردیابی با نمایه CPU 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) رفتار برش گران قیمت را تحریک می کند، و به طور کلی باید از آن اجتناب شود. در صورت امکان، به جای برش دادن به شکل های غیر مستطیلی، طراحی اشکال را انتخاب کنید. عملکرد بهتری دارد و از 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 به صورت بافت (id) آپلود عرض x ارتفاع ببینید. همانطور که در شکل 2 نشان داده شده است، ممکن است چندین میلی ثانیه طول بکشد، اما نمایش تصویر با GPU ضروری است.

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

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

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

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

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

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

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

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

اگر تراکنش‌های بایندر دارید، می‌توانید پشته‌های تماس آن‌ها را با دستورات 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() می توانند تراکنش های بایندر را راه اندازی کنند و در صورت تماس مکرر باعث ایجاد مشکلات بزرگ شوند. ردیابی دوره‌ای می‌تواند به شما در یافتن و رفع این مشکلات در هنگام نمایش کمک کند.

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

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

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

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

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

GC 94 میلی‌ثانیه را در HeapTaskDaemon نشان می‌دهد
شکل 5. یک GC 94 میلی‌ثانیه در رشته HeapTaskDaemon.

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

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}