در فلاتر هر تغییری در UI (مثل انیمیشن، transition و ..) بوسیله تولید فریم های جدید و دوباره ساخته شدن widget ها انجام می شود. برای این که این تغییرات به شکل هموار نمایش داده شوند، فلاتر سعی می کند فریم ها را با فرکانس 60Hz ساخته و نمایش دهد. یعنی در هر ثانیه ۶۰ فریم ساخته شود و این یعنی ساخت هر فریم باید حداکثر 16ms زمان نیاز داشته باشد. اگر این زمان رعایت نشود شاهد ناهمواری UI در تغییرات خواهیم بود (Jank UI). 

  • مسئله
    • شرح مسئله
    • نیازمندی ها
  • profile کردن برنامه
  • دیباگ کردن مشکلات عملکردی
    • Flutter Threads
      • UI Thread
      • Raster Thread
    • طولانی بودن زمان اجرا در UI Thread
    • طولانی بودن زمان اجرا در Raster Thread
  • نتیجه
  • منابع

مسئله

شرح مسئله

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

نیازمندی ها

در این متن از android studio و flutter DevTools استفاده می کنیم.

profile کردن برنامه

برای profile کردن عملکرد یک برنامه، ابتدا باید آن را در حالت profile برروی یک دستگاه فیزیکی ( و نه emulator ) اجرا کنیم. علت این کار این است که وقتی برنامه در حالت debug اجرا می شود، تعداد زیادی assertion در کد چک می شوند که بر عملکرد برنامه تاثیر گذار خواهند بود. همچنین در شبیه ساز (emulator)، شرایط سخت افزاری با حالت عادی تفاوت عمده دارند و در این حالت، عملکرد برنامه با حالت release برروی دستگاه واقعی متفاوت است.

برای اجرای برنامه در حالت profile، می توان از دستور زیر در ترمینال استفاده کرد:

flutter run --profile

همچنین می توان از طریق android studio در زبانه run گزینه run flutter in profile mode را انتخاب کرد.

برای استفاده از flutter devTools در android studio و در بخش flutter performance مطابق شکل open devtools را کلیک میکنیم:

Flutter App Performance Profiling
  • DevTools یک ابزار برای دیباگ کردن و ارزیابی عملکرد برنامه‌های فلاتر و همچنین Dart است. هم اکنون در نسخه بتا منتشر شده است.

پس از اجرای DevTools در بخش TimeLine گزینه Performance Overlay را انتخاب می کنیم. با فعال شدن این گزینه، دو گراف در بالای صفحه برنامه در حال اجرا رسم می شود.

profile کردن برنامه
profile کردن برنامه

گراف اول، مربوط به زمان سپری شدن ساخت هر فریم در Raster Thread و گراف دوم (پایین)، بیانگر زمان سپری شده در UI Thread است.

یک خط افقی، حد 16ms را برای محور عمودی (که زمان را مشخص می کند) تعیین می کند. اگر زمان ساخت یک فریم (خطوط عمودی و آبی) از این حد عبور کنند، نمایانگر این است که بیش از 16ms برای ساخت آن فریم زمان صرف شده است در تنیجه UIای ناهموار خواهیم داشت. (طبق صحبت ابتدایی). در این صورت خط عمودی سبز رنگ که زمان ساخته شدن فریم فعلی را نشان می دهد، قرمز می شود.

دیباگ کردن مشکلات عملکردی

Flutter Threads

در یک برنامه فلاتر، از thread های مختلفی با وظایف متفاوت استفاده می شود. دو مورد از این thread ها که در عملکرد برنامه تاثیر بیشتری دارند UI Thread و Raster Thread هستند.

UI Thread

این thread در واقع همان thread ای است که کد dart ای که ما می زنیم را اجرا می کند. همچنین بخشی از کد های dart مربوط به خود فلاتر نیز در این thread اجرا می شوند (flutter framework). وظیفه‌ی این thread ساخت layer tree است. layer tree یک داده ساختار نحوی ( semantic‌) برای widget ها در یک scene است و شامل تعدادی painting command برای قرار گیری آن ها نسبت به هم است.

Raster Thread

این thread وظیفه تبدیل layer tree به پیکسل های واقعی روی صفحه را دارد و کار renderکردن را انجام می دهد. برای این کار از GPU و کتاب خانه هایی نظیر Skia استفاده می کند.

  • برای مطالعه بیشتر در مورد thread ها و ساختار flutter framework به این لینک مراجعه کنید.

طبق بخش قبلی با فعال کردن Performance Overlay مدت زمان صرف شده در thread های Raster و UI را برای هر فریم توانستیم مشاهده کنیم.

طولانی بودن زمان اجرا در UI Thread

اگر زمان اجرا در UI Thread طولانی بود. (یعنی در اکثر مواقع شاهد قرمز بودن خط عمودی در این گراف بودیم). باید کد Dart زده شده را دیباگ کنیم. این امر نشان دهنده این است که کد Dart بهینه زده نشده است و عمل ساختن widget ها زمان بر و پر هزینه است.

طولانی بودن زمان اجرا در Raster Thread

این امر نشان دهنده این است که renderکردن Widget ها زمان بر است. که ممکن است علت های متفاوتی داشته باشد که به بیان برخی از آن ها می پردازیم.

  • به طور کلی در اکثر مواقع زمان اجرا در این thread نسبت به UI Thread بیشتر است. زیرا عمل render کردن در این thread انجام می شود.
  • ممکن است علت آن استفاده از انیمیشن ها باشد. برای پی بردن به صحت این ادعا بوسیله‌ی گزینه Slow Animation در بخش Flutter Inspector در Android Studio سرعت اجرای انیمیشن ها را کم می کنیم و مجددا به زمان ساخت فریم ها نگاه می کنیم.
    • این کار بوسیله ترمینال و با گزینه های بیشتر نیز قابل انجام است.
profile کردن برنامه در فلاتر
  • یکی از متد های پرهزینه در فریم ورک flutter متد saveLayer است. این متد ممکن است به صورت غیرمستقیم برای دفعات متعدد صدا زده شود و باعث کند شدن rendering شود. استفاده از Clip و Opacity  و Shadow ممکن است باعث ایجاد این اتفاق شود.
    • استفاده از این موارد برای یک گروه از Widget ها می تواند برنامه را کُند کند. 
    • بهتر است از این widget ها در element های مختلف و کوچک به شکل جدا استفاده کنیم. نسبت به حالتی که برای یک Widget پدر، در راس یک درخت استفاده کنیم.
  • علت دیگر برای بروز این اتفاق Image IO است. وقتی از یک فایل تصویری در برنامه استفاده می کنیم:
    • تصویر ابتدا باید از فایل fetchشود.
    • سپس باید decompress شود و به مموری GPU منتقل شود.
    • سپس به RAM منتقل شود.
  • این عمل زمان بر است و به جای این کار فقط در موارد ضروری می توان از image caching استفاده کرد. (البته این کار نیز منابع قابل توجهی از GPU را مصرف می کند.).
  • در آخر برای پیدا کردن علت این مشکل باید فرایند دوباره سازی ویجت ها ( widget rebuild ) را profileکنیم.
    • ممکن است بعضی از widget ها در فریم، بارها rebuild شوند اما نیازی به این کار نباشد و ساخت فریم کند شود.
    • برای profile کردن
      • برنامه را در حالت debug اجرا می کنیم.
      • در بخش Flutter performanceدر Android Studio ، گزینه Track Widget Rebuilds را انتخاب می کنیم.
      • با این کار، ویجت هایی که در هر صفحه ساخته می شوند نمایش داده می شود. همچنین تعداد دفعاتی که یک ویجت  از زمان ورود به یک صفحه ساخته شده است نیز نمایش داده میشود.
    • استفاده از یک stateful widget برای کل یک صفحه باعث می شود که متد build پرهزینه شود و تعداد دفعات زیادی صدا زده شود. برای حل این مشکل بهتر است از تعداد بیشتری stateful widgetبا build کم هزینه استفاده کنیم.
    • همچنین بعضی از ویجت ها offscreen ساخته می شوند و redraw می شوند که ممکن است باعث این اتفاق شوند. مثلا استفاده از ListView برای یک Column که طولی بیشتر از طول صفحه دارد.
      • برای حل این مشکلات می توان از ListView.builder و RepaintBoundary استفاده کرد.
    • استفاده از AnimatedBuilder برای یک subtree که نیازی به ساخت مجدد آن در طول انیمیشن نیست نیز از دلایل دیگر این اتفاق است.
profile کردن برنامه در فلاتر

نتیجه

در این متن روش های ارزیابی یک برنامه فلاتر و علت های مختلف آن بررسی شد. یکی از کارهایی که می تواند در این زمینه کمک کند، استفاده از integration test ها و benchmarking است. با استفاده از integration test می توان محدودیت ها و متریک هایی برای 

  • زمان شروع برنامه (startup time)
  • مصرف باطری
  • زمان ساخته شدن فریم و هموار یا ناهموار بودن UI

تعیین کرد.

منابع

درباره نیلوا

ایران،تهران

ناحیه نوآوری شریف

بلوار جواد اکبری

برج فناوری بنتک

T: 09150773830
E: admin[@]nilva.ir