چقدر به مشکلات performance برنامه ای که نوشتید آگاه هستید؟ آیا می دانید کدام متد بیش از حد زمان بر است؟ inflate شدن کدام UI بیش از حد انتظاراتان طول می کشد؟ چه قسمت هایی از برنامه Memory leak دارد؟ کجا از ساختار داده مناسب استفاده نکردید؟ کجا با مشکل overdraw روبرو هستید و می توانید آن را بهبود دهید؟ آیا می دانید چرا برنامه لَگ دارد؟ چرا زمانی که برنامه شما اجرا می شود، کلا عملکرد سیستم افت می کند؟
همانطور که مشخص است performance برای برنامه اندروید جنبه های مختلفی دارد. توانایی سخت افزاری گوشی های موبایل نسبت به لپ تاپ یا کامپیوتر بسیار کمتر است و سیستم resource کمتری در اختیار دارد در نتیجه استفاده درست از آن resource محدود، برای داشتن performance مناسب، بسیار مساله ی مهمی است.
با اینکه performance جنبه های مختلفی دارد ولی شاید به شکل کلی بتوان آن را به:
تقسیم کرد. در این مقاله تمرکزمان بر روی CPU است و در مقالات بعد، به دو مورد دیگر خواهم پرداخت.
ما برای اینکه جواب سوالاتمان راجع به performance را پیدا کنیم نیاز به داده داریم. Android Profiler این داده ها را جمع آوری کرده و در اختیار ما قرار می دهد. در ادامه با اینکه چطور می توانیم از داده هایی که در اختیارمان قرار گرفته به مشکلات پی ببریم آشنا می شویم.
پیش از آنکه جلوتر رویم به این سه نکته توجه کنید:
اندروید پروفایلر از طریق منو پایین صفحه در دسترس است.
برای اینکه به جزییات اطلاعات CPU دسترسی داشته باشید باید یک بازه زمانی را Record کنید. حال می توانید تنظیم کنید که بلافاصله بعد از باز شدن اپ Record شروع شود یا اینکه هر موقعی که خودتان خواستید Record را شروع کنید. در هر دو مورد مشخص کردن پایان Record با شماست. توجه داشته باشید که معمولا طول Record نباید خیلی زیاد باشد چرا که حجم اطلاعات دریافتی بسیار زیاد خواهد بود و تحلیل آن ها سخت تر می شود ضمن اینکه اندروید استدیو هم گاهی بسیار کند می شود.
ما از روش اول (شروع Record به محض باز شدن برنامه) استفاده می کنیم. فارغ از اینکه از چه روشی استفاده کنید می توان یکی از ۴ متد زیر را انتخاب کرد.
توضیحات هر متد در اینجا موجود است. به شکل کلی برای اینکه با CPU Profiler بیشتر آشنا شوید خواندن این داکیومنت می تواند مفید باشد.
برای پیدا کردن مشکل، استفاده از متد آخر پیشنهاد شده است زیرا تاثیر کمی بر روی Performance دارد و از طرفی اطلاعات کم ولی مکفی برای حدس زدن مشکل در اختیار ما قرار می دهد. زمانی که به بخش خاصی مشکوک شدیم برای بررسی جزییات دقیق تر به سراغ متد سوم می رویم.
همانطور که در گیف بالا مشخص است بین زمانی که من روی اپ کلیک می کنم تا زمانی که صفحه اول برنامه را می بینم یک صفحه سفید نمایش داده می شود. این احتمالا به این معنی است که پروسه startup برنامه دچار مشکل است و جایی سبب کندی شده است. در این مرحله به کمک CPU Profiler به دنبال مشکل خواهیم گشت. در ابتدا تنظیمات مربوطه را انجام می دهیم که با شروع برنامه، Record هم شروع شود و از متد Trace System Calls استفاده می کنیم.
مراحل در گیف بالا مشخص است ولی به منظور سهولت در پیگیری به ترتیب آن ها را در زیر می نویسم:
بعد از چند ثانیه شما به خروجی 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 = "" @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 استفاده کنید)
شمایل صفحه اول برنامه و کد آن به شرح زیر است.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <View android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:background="@color/green_4c" /> <LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:orientation="vertical"> <View android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="@color/red" /> <View android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2" android:background="@color/colorPrimaryDark" /> <View android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="3" android:background="@color/black" /> </LinearLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <View android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="@color/colorPrimaryDark" /> <View android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="@color/blue_03" /> <View android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="@color/yellow" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center"> <View android:layout_width="100dp" android:layout_height="100dp" android:background="@color/colorAccent" /> </LinearLayout> </LinearLayout> </LinearLayout>
همانطور که مشخص است این صفحه به کمک linear layout پیاده سازی شده است. طبیعتا به علت محدودیت های این layout مجبور به استفاده تو در توی آن شده ام. من حدس می زنم احتمالا اگر به کمک constraint layout این صفحه را بازنویسی کنم performance بهتری خواهد داشت چرا که می توانم آن را flat پیاده سازی کنم. برای این کار اول به کمک profiler میزان زمانی که برای inflate شدن پیاده سازی فعلی نیاز است را پیدا می کنم. همانطور که در تصویر زیر مشخص است این پروسه حدود ۱۳ میلی ثانیه زمان به خود اختصاص داده است.
حال پیاده سازی را به شکل زیر تغییر می دهم.
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:layout_width="0dp" android:layout_height="0dp" android:background="@color/green_4c" app:layout_constraintBottom_toTopOf="@id/half_horizontal_guide" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@id/half_vertical_guide" app:layout_constraintTop_toTopOf="parent" /> <View android:id="@+id/red_v" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/red" app:layout_constraintBottom_toTopOf="@id/primary_dark_v" app:layout_constraintLeft_toRightOf="@id/half_vertical_guide" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_weight="1" /> <View android:id="@+id/primary_dark_v" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorPrimaryDark" app:layout_constraintBottom_toTopOf="@id/black_v" app:layout_constraintLeft_toRightOf="@id/half_vertical_guide" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/red_v" app:layout_constraintVertical_weight="2" /> <View android:id="@+id/black_v" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/black" app:layout_constraintBottom_toTopOf="@id/half_horizontal_guide" app:layout_constraintLeft_toRightOf="@id/half_vertical_guide" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/primary_dark_v" app:layout_constraintVertical_weight="3" /> <View android:id="@+id/primary_v" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/colorPrimary" app:layout_constraintBottom_toTopOf="@id/quarter_horizontal_guide" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@id/blue_v" app:layout_constraintTop_toBottomOf="@id/half_horizontal_guide" /> <View android:id="@+id/blue_v" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/blue_03" app:layout_constraintBottom_toTopOf="@id/quarter_horizontal_guide" app:layout_constraintLeft_toRightOf="@id/primary_v" app:layout_constraintRight_toLeftOf="@id/yellow_v" app:layout_constraintTop_toBottomOf="@id/half_horizontal_guide" /> <View android:id="@+id/yellow_v" android:layout_width="0dp" android:layout_height="0dp" android:background="@color/yellow" app:layout_constraintBottom_toTopOf="@id/quarter_horizontal_guide" app:layout_constraintLeft_toRightOf="@id/blue_v" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/half_horizontal_guide" /> <View android:layout_width="100dp" android:layout_height="100dp" android:background="@color/colorAccent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/quarter_horizontal_guide" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/half_vertical_guide" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_percent=".5" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/half_horizontal_guide" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent=".5" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/quarter_horizontal_guide" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent=".75" /> </androidx.constraintlayout.widget.ConstraintLayout>
پس از تغییر دوباره به کمک profiler زمان inflate شدن را بررسی می کنم. در کمال تعجب زمان به حدود ۱۹ میلی ثانیه افزایش پیدا کرد !!
پس به کمک profiler می توانیم پیاده سازی های مختلف را امتحان کنیم (در صورت نیاز) تا به بهترین پیاده سازی برای نیاز خودمان برسیم. این فقط مربوط به xml نیست و در مورد کد java/kotlin هم قطعا صادق است.
برای حل مشکل performance راه حل ثابتی وجود ندارد بلکه باید به کمک ابزار های موجود سعی کنیم مشکلات را پیدا کنیم و مطمئن شویم راه حل جایگزینی که اجرا می کنیم واقعا مفیدتر و بهینه تر می باشد. CPU profiler در این مسیر بسیار به کمک ما می آید.
به این نکته توجه داشته باشید که لزوما هر پروسه ی طولانی ای مشکل performance ندارد. دنبال پروسه هایی باشید که بیشتر از انتظار شما زمان بر بوده اند. همیشه هم دنبال تغییرات performance عجیب و غریب نباشید. اگر هر متد چند میلی ثانیه بهینه تر شود می تواند تاثیر بزرگی در بهبود کیفیت داشته باشد و برعکس عدم بهینه بودن های کوچک زمانی که در کنار هم قرار می گیرند می توانند تاثیر مشهودی بر عملکرد برنامه داشته باشند.
امیدوارم این مطلب برای شما مفید بوده باشد