چگونه Performance اپلیکیشن اندرویدی را افزایش دهیم؟ / بخش اول: CPU

چقدر به مشکلات performance برنامه ای که نوشتید آگاه هستید؟ آیا می دانید کدام متد بیش از حد زمان بر است؟ inflate شدن کدام UI بیش از حد انتظاراتان طول می کشد؟ چه قسمت هایی از برنامه Memory leak دارد؟ کجا از ساختار داده مناسب استفاده نکردید؟ کجا با مشکل overdraw روبرو هستید و می توانید آن را بهبود دهید؟ آیا می دانید چرا برنامه لَگ دارد؟ چرا زمانی که برنامه شما اجرا می شود، کلا عملکرد سیستم افت می کند؟ همانطور که مشخص است performance برای برنامه اندروید جنبه های مختلفی دارد. توانایی سخت افزاری گوشی های موبایل نسبت به لپ تاپ یا کامپیوتر بسیار کمتر است و سیستم resource کمتری در اختیار دارد در نتیجه استفاده درست از آن resource محدود، برای داشتن performance مناسب، بسیار مساله ی مهمی است. با اینکه performance جنبه های مختلفی دارد ولی شاید به شکل کلی بتوان آن را به:سی پی یو (CPU)حافظه (Memory)گرافیک (GPU)تقسیم کرد. در این مقاله تمرکزمان بر روی CPU است و در مقالات بعد، به دو مورد دیگر خواهم پرداخت. اندروید پروفایلر (Android Profiler)ما برای اینکه جواب سوالاتمان راجع به performance را پیدا کنیم نیاز به داده داریم. Android Profiler این داده ها را جمع آوری کرده و در اختیار ما قرار می دهد. در ادامه با اینکه چطور می توانیم از داده هایی که در اختیارمان قرار گرفته به مشکلات پی ببریم آشنا می شویم.پیش از آنکه جلوتر رویم به این سه نکته توجه کنید:اگر فکر می کنید در ادامه قرار است یک راه حل مشخص و جادویی برای بهبود performance برنامه تان معرفی شود سخت در اشتباهید. در ادامه با یک مثال سعی می کنیم با استفاده از ابزارهایی که در اختیار داریم مشکلات را پیدا کنیم. برای اینکه performance برنامه تان را بهبود ببخشید باید با این ابزارها سر و کله بزنید و مشکلات مخصوص خودتان را پیدا کنید.مثال هایی که در ادامه می آید صرفا به هدف یادگیری است. پس فکر نکنید که فلان بخش که واضح بود و برای تشخیص مشکل اصلا نیاز به Android Profiler نبود. این صرفا یک مثال برای بیان روش است و می توانید در پروژه شما خیلی پیچیده تر باشد.برای بدست آوردن اطلاعات بهتر و دقیق تر پیشنهاد می کنم حتما برنامه را روی موبایل واقعی اجرا کنید. گاهی اطلاعاتی که توسط emulator ارائه می شود آن دقت را ندارد (در ادامه گاهی برای ضبط راحت تر از genymotion استفاده شده است) اندروید پروفایلر از طریق منو پایین صفحه در دسترس است.برای اینکه به جزییات اطلاعات CPU دسترسی داشته باشید باید یک بازه زمانی را Record کنید. حال می توانید تنظیم کنید که بلافاصله بعد از باز شدن اپ Record شروع شود یا اینکه هر موقعی که خودتان خواستید Record را شروع کنید. در هر دو مورد مشخص کردن پایان Record با شماست. توجه داشته باشید که معمولا طول Record نباید خیلی زیاد باشد چرا که حجم اطلاعات دریافتی بسیار زیاد خواهد بود و تحلیل آن ها سخت تر می شود ضمن اینکه اندروید استدیو هم گاهی بسیار کند می شود. ما از روش اول (شروع Record به محض باز شدن برنامه) استفاده می کنیم. فارغ از اینکه از چه روشی استفاده کنید می توان یکی از ۴ متد زیر را انتخاب کرد. متد اول(Sample Java Methods)متد دوم(Trace Java Methods)متد سوم(Sample C/C++ Functions)متد چهارم(Trace System Calls)توضیحات هر متد در اینجا موجود است. به شکل کلی برای اینکه با CPU Profiler بیشتر آشنا شوید خواندن این داکیومنت می تواند مفید باشد.برای پیدا کردن مشکل، استفاده از متد آخر پیشنهاد شده است زیرا تاثیر کمی بر روی Performance دارد و از طرفی اطلاعات کم ولی مکفی برای حدس زدن مشکل در اختیار ما قرار می دهد. زمانی که به بخش خاصی مشکوک شدیم برای بررسی جزییات دقیق تر به سراغ متد سوم می رویم. چالش اول: Startupهمانطور که در گیف بالا مشخص است بین زمانی که من روی اپ کلیک می کنم تا زمانی که صفحه اول برنامه را می بینم یک صفحه سفید نمایش داده می شود. این احتمالا به این معنی است که پروسه startup برنامه دچار مشکل است و جایی سبب کندی شده است. در این مرحله به کمک CPU Profiler به دنبال مشکل خواهیم گشت. در ابتدا تنظیمات مربوطه را انجام می دهیم که با شروع برنامه، Record هم شروع شود و از متد Trace System Calls استفاده می کنیم. مراحل در گیف بالا مشخص است ولی به منظور سهولت در پیگیری به ترتیب آن ها را در زیر می نویسم:از بخش app بالا (Run/Debug Configuration) گزینه Edit Configuration را انتخاب می کنیم.از منو Profiling گزینه start this recording on startup را فعال می کنیم.گزینه cpu activity را انتخاب کرده و سپس از drop down گزینه Trace system calls را انتخاب می کنیم.با انتخاب Ok تغییرات را ذخیره می کنیم.گزینه Profile را انتخاب می کنیم تا برنامه اجرا شود.پس از چند ثانیه زمانی که گزینه stop نمایش داده شد و صفحه اول برنامه نمایش داده شده بود گزینه stop را انتخاب می کنیم.بعد از چند ثانیه شما به خروجی record دسترسی دارید. قسمتی که ما بیشتر با آن کار داریم بخش threads و زیر مجموعه ای است که پکیج برنامه خودمان را نمایش می دهد. این دیتا در این شرایط قابل استفاده نیست برای همین باید به کمک گزینه های زوم بالا سمت راست یا کلید های a w s d زوم را بیشتر کنیم تا دیتا خواناتر باشد. بعد از کمی زوم کردن و اسکرول به سمت چپ من به این بخش رسیدم.همانطور که مشخص است پروسه bindApplication حدود ۱.۲۴ ثانیه زمان نیاز داشته است. این عدد شک برانگیز است. به طور عادی نباید این میزان طول بکشد. حالت Trace system calls اطلاعات بیشتری در اختیار ما قرار نمی دهد که بتوانیم دقیق تر بررسی کنیم به همین منظور متد را به Sample C/C++ Functions تغییر می دهیم و مجدد برنامه را اجرا می کنیم تا با اطلاعاتی که بدست آوردیم به بررسی دقیق تر بپردازیم.پس از اجرای برنامه در همان نگاه اول متوجه تفاوت حجم اطلاعات می شوید. از منو سمت راست گزینه flame chart را انتخاب کنید و سپس bindApplication را در آن سرچ کنید.همانطور که مشخص شده آن قسمتی که مربوط به سرچ شما می شود bold شده است. اگر کمی بیشتر زوم کنیم متوجه می شویم که متد com.shkbhbb.performancetest.MyApp.OnCreate حدود ۹۰۰.۶۳ میلی ثانیه زمان به خود اختصاص داده است یعنی حدود ۷۰ درصد زمان bindApplication !! وقتی به سراغ متد onCreate در کلاس MyApp می رویم با کد زیر مواجه می شویم.public class MyApp extends Application { public static String ID = &quot&quot @Override public void onCreate() { super.onCreate(); for (int i = 0; i < 20000; i++) { ID = ID.concat(String.valueOf(i)); } } }مشخصا این loop زمانبر در کلاس application سبب کند شدن startup برنامه ما شده است. با قرار دادن این loop در یک thread مجزا مشکل startup مرتفع می شود. پس از اعمال این تغییر زمان bindApplication به ۲۹۷ میلی ثانیه کاهش پیدا کرد. (همیشه برای بدست آوردن زمان از Trace system calls استفاده کنید)چالش دوم: بهینه بودن پیاده سازی UIشمایل صفحه اول برنامه و کد آن به شرح زیر است.<?xml version=&quot1.0&quot encoding=&quotutf-8&quot?> <LinearLayout xmlns:android=&quothttp://schemas.android.com/apk/res/android&quot android:layout_width=&quotmatch_parent&quot android:layout_height=&quotmatch_parent&quot android:orientation=&quotvertical&quot> <LinearLayout android:layout_width=&quotmatch_parent&quot android:layout_height=&quot0dp&quot android:layout_weight=&quot1&quot android:orientation=&quothorizontal&quot> <View android:layout_width=&quot0dp&quot android:layout_height=&quotwrap_content&quot android:layout_weight=&quot1&quot android:background=&quot@color/green_4c&quot /> <LinearLayout android:layout_width=&quot0dp&quot android:layout_height=&quotmatch_parent&quot android:layout_weight=&quot1&quot android:orientation=&quotvertical&quot> <View android:layout_width=&quotmatch_parent&quot android:layout_height=&quot0dp&quot android:layout_weight=&quot1&quot android:background=&quot@color/red&quot /> <View android:layout_width=&quotmatch_parent&quot android:layout_height=&quot0dp&quot android:layout_weight=&quot2&quot android:background=&quot@color/colorPrimaryDark&quot /> <View android:layout_width=&quotmatch_parent&quot android:layout_height=&quot0dp&quot android:layout_weight=&quot3&quot android:background=&quot@color/black&quot /> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width=&quotmatch_parent&quot android:layout_height=&quot0dp&quot android:layout_weight=&quot1&quot android:orientation=&quotvertical&quot> <LinearLayout android:layout_width=&quotmatch_parent&quot android:layout_height=&quot0dp&quot android:layout_weight=&quot1&quot> <View android:layout_width=&quot0dp&quot android:layout_height=&quotmatch_parent&quot android:layout_weight=&quot1&quot android:background=&quot@color/colorPrimaryDark&quot /> <View android:layout_width=&quot0dp&quot android:layout_height=&quotmatch_parent&quot android:layout_weight=&quot1&quot android:background=&quot@color/blue_03&quot /> <View android:layout_width=&quot0dp&quot android:layout_height=&quotmatch_parent&quot android:layout_weight=&quot1&quot android:background=&quot@color/yellow&quot /> </LinearLayout> <LinearLayout android:layout_width=&quotmatch_parent&quot android:layout_height=&quot0dp&quot android:layout_weight=&quot1&quot android:gravity=&quotcenter&quot> <View android:layout_width=&quot100dp&quot android:layout_height=&quot100dp&quot android:background=&quot@color/colorAccent&quot /> </LinearLayout> </LinearLayout> </LinearLayout>همانطور که مشخص است این صفحه به کمک linear layout پیاده سازی شده است. طبیعتا به علت محدودیت های این layout مجبور به استفاده تو در توی آن شده ام. من حدس می زنم احتمالا اگر به کمک constraint layout این صفحه را بازنویسی کنم performance بهتری خواهد داشت چرا که می توانم آن را flat پیاده سازی کنم. برای این کار اول به کمک profiler میزان زمانی که برای inflate شدن پیاده سازی فعلی نیاز است را پیدا می کنم. همانطور که در تصویر زیر مشخص است این پروسه حدود ۱۳ میلی ثانیه زمان به خود اختصاص داده است.حال پیاده سازی را به شکل زیر تغییر می دهم.<?xml version=&quot1.0&quot encoding=&quotutf-8&quot?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android=&quothttp://schemas.android.com/apk/res/android&quot xmlns:app=&quothttp://schemas.android.com/apk/res-auto&quot android:layout_width=&quotmatch_parent&quot android:layout_height=&quotmatch_parent&quot> <View android:layout_width=&quot0dp&quot android:layout_height=&quot0dp&quot android:background=&quot@color/green_4c&quot app:layout_constraintBottom_toTopOf=&quot@id/half_horizontal_guide&quot app:layout_constraintLeft_toLeftOf=&quotparent&quot app:layout_constraintRight_toLeftOf=&quot@id/half_vertical_guide&quot app:layout_constraintTop_toTopOf=&quotparent&quot /> <View android:id=&quot@+id/red_v&quot android:layout_width=&quot0dp&quot android:layout_height=&quot0dp&quot android:background=&quot@color/red&quot app:layout_constraintBottom_toTopOf=&quot@id/primary_dark_v&quot app:layout_constraintLeft_toRightOf=&quot@id/half_vertical_guide&quot app:layout_constraintRight_toRightOf=&quotparent&quot app:layout_constraintTop_toTopOf=&quotparent&quot app:layout_constraintVertical_weight=&quot1&quot /> <View android:id=&quot@+id/primary_dark_v&quot android:layout_width=&quot0dp&quot android:layout_height=&quot0dp&quot android:background=&quot@color/colorPrimaryDark&quot app:layout_constraintBottom_toTopOf=&quot@id/black_v&quot app:layout_constraintLeft_toRightOf=&quot@id/half_vertical_guide&quot app:layout_constraintRight_toRightOf=&quotparent&quot app:layout_constraintTop_toBottomOf=&quot@id/red_v&quot app:layout_constraintVertical_weight=&quot2&quot /> <View android:id=&quot@+id/black_v&quot android:layout_width=&quot0dp&quot android:layout_height=&quot0dp&quot android:background=&quot@color/black&quot app:layout_constraintBottom_toTopOf=&quot@id/half_horizontal_guide&quot app:layout_constraintLeft_toRightOf=&quot@id/half_vertical_guide&quot app:layout_constraintRight_toRightOf=&quotparent&quot app:layout_constraintTop_toBottomOf=&quot@id/primary_dark_v&quot app:layout_constraintVertical_weight=&quot3&quot /> <View android:id=&quot@+id/primary_v&quot android:layout_width=&quot0dp&quot android:layout_height=&quot0dp&quot android:background=&quot@color/colorPrimary&quot app:layout_constraintBottom_toTopOf=&quot@id/quarter_horizontal_guide&quot app:layout_constraintLeft_toLeftOf=&quotparent&quot app:layout_constraintRight_toLeftOf=&quot@id/blue_v&quot app:layout_constraintTop_toBottomOf=&quot@id/half_horizontal_guide&quot /> <View android:id=&quot@+id/blue_v&quot android:layout_width=&quot0dp&quot android:layout_height=&quot0dp&quot android:background=&quot@color/blue_03&quot app:layout_constraintBottom_toTopOf=&quot@id/quarter_horizontal_guide&quot app:layout_constraintLeft_toRightOf=&quot@id/primary_v&quot app:layout_constraintRight_toLeftOf=&quot@id/yellow_v&quot app:layout_constraintTop_toBottomOf=&quot@id/half_horizontal_guide&quot /> <View android:id=&quot@+id/yellow_v&quot android:layout_width=&quot0dp&quot android:layout_height=&quot0dp&quot android:background=&quot@color/yellow&quot app:layout_constraintBottom_toTopOf=&quot@id/quarter_horizontal_guide&quot app:layout_constraintLeft_toRightOf=&quot@id/blue_v&quot app:layout_constraintRight_toRightOf=&quotparent&quot app:layout_constraintTop_toBottomOf=&quot@id/half_horizontal_guide&quot /> <View android:layout_width=&quot100dp&quot android:layout_height=&quot100dp&quot android:background=&quot@color/colorAccent&quot app:layout_constraintBottom_toBottomOf=&quotparent&quot app:layout_constraintLeft_toLeftOf=&quotparent&quot app:layout_constraintRight_toRightOf=&quotparent&quot app:layout_constraintTop_toBottomOf=&quot@id/quarter_horizontal_guide&quot /> <androidx.constraintlayout.widget.Guideline android:id=&quot@+id/half_vertical_guide&quot android:layout_width=&quotwrap_content&quot android:layout_height=&quotwrap_content&quot android:orientation=&quotvertical&quot app:layout_constraintGuide_percent=&quot.5&quot /> <androidx.constraintlayout.widget.Guideline android:id=&quot@+id/half_horizontal_guide&quot android:layout_width=&quotwrap_content&quot android:layout_height=&quotwrap_content&quot android:orientation=&quothorizontal&quot app:layout_constraintGuide_percent=&quot.5&quot /> <androidx.constraintlayout.widget.Guideline android:id=&quot@+id/quarter_horizontal_guide&quot android:layout_width=&quotwrap_content&quot android:layout_height=&quotwrap_content&quot android:orientation=&quothorizontal&quot app:layout_constraintGuide_percent=&quot.75&quot /> </androidx.constraintlayout.widget.ConstraintLayout>پس از تغییر دوباره به کمک profiler زمان inflate شدن را بررسی می کنم. در کمال تعجب زمان به حدود ۱۹ میلی ثانیه افزایش پیدا کرد !!پس به کمک profiler می توانیم پیاده سازی های مختلف را امتحان کنیم (در صورت نیاز) تا به بهترین پیاده سازی برای نیاز خودمان برسیم. این فقط مربوط به xml نیست و در مورد کد java/kotlin هم قطعا صادق است. جمع بندیبرای حل مشکل performance راه حل ثابتی وجود ندارد بلکه باید به کمک ابزار های موجود سعی کنیم مشکلات را پیدا کنیم و مطمئن شویم راه حل جایگزینی که اجرا می کنیم واقعا مفیدتر و بهینه تر می باشد. CPU profiler در این مسیر بسیار به کمک ما می آید. به این نکته توجه داشته باشید که لزوما هر پروسه ی طولانی ای مشکل performance ندارد. دنبال پروسه هایی باشید که بیشتر از انتظار شما زمان بر بوده اند. همیشه هم دنبال تغییرات performance عجیب و غریب نباشید. اگر هر متد چند میلی ثانیه بهینه تر شود می تواند تاثیر بزرگی در بهبود کیفیت داشته باشد و برعکس عدم بهینه بودن های کوچک زمانی که در کنار هم قرار می گیرند می توانند تاثیر مشهودی بر عملکرد برنامه داشته باشند.امیدوارم این مطلب برای شما مفید بوده باشد