پروفایل کردن اپلیکیشن ها با اندروید استودیو — به زبان ساده
یکی از سؤالاتی که توسعهدهندگان باید از خود بپرسند این است که اگر کاربران در زمان استفاده از اپلیکیشن حس کند بودن بکنند چه کار میتوان کرد؟ پاسخ این سؤال همواره بیدرنگ روشن نیست؛ اما در اغلب موارد با وظایفی که استفاده سنگینی از CPU دارند و نخ اصلی را مسدود میکنند مرتبط است. همچنین کلاسهایی وجود دارند که این نوع از مشکلات عملکردی در آنها با استفاده از حافظه مرتبط است. در این نوشته با روش پروفایل کردن اپلیکیشن اندروید آشنا میشویم.
پروفایل کردن به چه معنا است و چه کمکی میکند؟
شما میتوانید برخی لاگها را نمایش دهید تا به عیبیابی اپلیکیشن کمک کنید؛ اما باید با کدبیس اپلیکیشن آشنا باشید تا بتوانید لاگها را در مکانهای مناسب قرار دهید.
اگر میخواهید گزینه دیگری را بررسی کنید که صرفاً مبتنی بر لاگ نیست، بهتر است از ابزار Android Profiler استفاده کنید که در نسخه 3.0 اندروید استودیو معرفی شده است. توسعهدهندگان میتوانند از این ابزار برای نظارت بر استفاده از CPU و همچنین مصرف حافظه، تفسیر پاسخهای شبکه و حتی مشاهده مصرف انرژی بهره بگیرند.
بر اساس دادههای معیاری که اندروید پروفایلر ارائه میکند، میتوانیم درک بهتری از شیوه استفاده از CPU و دیگر منابع حافظه از سوی اپلیکیشن داشته باشیم. اینها مواردی هستند که در نهایت ما را به یافتن منشأ مشکل راهنمایی میکنند.
مشکلات حافظه
در این بخش به بررسی مدیریت حافظه در اندروید میپردازیم و بدین ترتیب میتوانیم درک کنیم که چرا استفاده نامناسب از حافظه میتواند موجب مشکلات عملکردی شود. اپلیکیشن اندروید نیز مانند هر اپلیکیشن نرمافزاری کلاینت دیگر در محیط مقید به حافظه اجرا میشود. سیستم عامل اندروید بخش محدود اما انعطافپذیری از هیپ حافظه را برای هر اپلیکیشن در حال اجرا اختصاص میدهد.
سیستم عامل اندروید در طی دوره حیات اپلیکیشن حافظهای به آن اختصاص میدهد تا دستورالعملها و دادههای برنامه را در آن ذخیره کند. مقدار حافظه مورد نیاز به نوع اپلیکیشن وابسته است. برای نمونه یک اپلیکیشن برای نمایش تصویر تمام صفحه bitmap ممکن است به حافظه بیشتری نسبت به متن تمام صفحه نیاز داشته باشد.
زمانی که یک بخش از حافظه دیگر لازم نباشد، سیستم عامل اندروید به طور خودکار این منبع حافظه را باز پس میگیرد تا بتواند برای درخواستهای تخصیص حافظه جدید مورد استفاده قرار دهد. این فرایند به نام Garbage Collection شناخته میشود.
Garbage Collection معمولاً تأثیری روی عملکرد اپلیکیشنها ندارد، چون زمان مکث اپلیکیشن در نتیجه فرایند Garbage Collection ناچیز است. با این حال اگر رویدادهای Garbage Collection زیاد شوند و در مدت کوتاهی رخ دهند، کاربران به تدریج تجربه کاربری توأم با کند شدن در اپلیکیشن خواهند داشت.
پروفایل کردن حافظه با اندروید پروفایلر
پیشنیازهای پروفایل کردن اندروید این است که اندروید نسخه 3.0 یا بالاتر را نصب کنید و یک دستگاه تست یا شبیهساز به آن وصل کنید که دستکم SDK سطح 26 یا بالاتر را اجرا کند. زمانی که این نرمافزارها آماده شدند میتوانید روی برگه profiler در پنل انتهایی کلیک کنید تا پروفایلر اندروید باز شود.
اپلیکیشن خود را در حالت دیباگ آغاز کنید تا ببینید که اندروید پروفایلر معیارهای آنی را برای CPU، Memory، Network و Energy نمایش میدهد.
روی بخش Memory کلیک کنید تا جزییات معیارهای مصرف حافظه را ببینید. اندروید پروفایلر یک مرور بصری از مصرف حافظه در طی زمان ارائه میکند.
چنان که در نمودار فوق میبینید، یک اوج مصرف ابتدایی وجود دارد که در زمان اجرای اولیه اپلیکیشن ایجاد شده است و سپس یک افت داریم و در نهایت به یک خط صاف رسیده است. این رفتار معمول یک اپلیکیشن hello world ساده است.
یک گراف صاف حافظه به این معنی است که مصرف حافظه پایدار است و این موقعیت ایدهآل مصرف حافظه است که همواره میخواهیم به دست بیاوریم. خواندن یک گراف حافظه مانند تحلیل کردن یک نمودار سهام است اما این تفاوت را دارد که سقوطها ترجیح بیشتری نسبت به اوجها دارند.
استفاده عملی از اندروید پروفایلر
در این بخش برخی الگوهای گراف را که نشاندهنده مشکلات مرتبط با حافظه هستند، بررسی میکنیم و شما میتوانید از این کد منبع (+) برای بازتولید مشکلات استفاده کنید.
یک گراف در حال رشد
اگر یک خط روند مشاهده میکنید که مداوماً و با شیب ملایم در حال رشد است، میتواند ناشی از نشت حافظه باشد، یعنی بخشی از حافظه نرمافزار نمیتواند آزاد شود. همچنین این وضعیت میتواند ناشی از نبود حافظه کافی برای اپلیکیشن باشد. زمانی که اپلیکیشن به محدودیت حافظه میرسد و سیستم عامل اندروید حافظه دیگری به اپلیکیشن تخصیص نمیدهد، خطای OutOfMemoryError ایجاد خواهد شد.
این مشکل را میتوانید در مثال کاربر حافظه بالا در اپلیکیشن دمو بازتولید کنید. این مثال اساساً تعداد بالایی از ردیفها (100 هزار) ایجاد میکند و این ردیفها را به یک LinearLayout اضافه میکند. این کار معمولی است اما ما میخواستیم به شما یک حالت حدی را نشان دهیم که ایجاد نماهای زیاد چنان که در کد زیر مشاهده میکنید، میتواند موجب بروز مشکل شود:
1/***
2 * In order to stress the memory usage,
3 * this activity creates 100000 rows of TextView when user clicks on the start button
4 */
5class HighMemoryUsageActivity : AppCompatActivity() {
6
7 val NO_OF_TEXTVIEWS_ADDED = 100000
8
9 override fun onCreate(savedInstanceState: Bundle?) {
10 super.onCreate(savedInstanceState)
11 setContentView(R.layout.activity_high_memory_usage)
12
13 supportActionBar?.setDisplayHomeAsUpEnabled(true)
14 supportActionBar?.setTitle(R.string.activity_name_high_memory_usage)
15 btn_start.setOnClickListener {
16 addRowsOfTextView()
17 }
18 }
19
20 override fun onSupportNavigateUp(): Boolean {
21 onBackPressed()
22 return true
23 }
24
25 /**
26 * Add rows of text views to the root LinearLayout
27 */
28 private fun addRowsOfTextView() {
29 val linearLayout = findViewById<LinearLayout>(R.id.linear_layout)
30
31 val textViewParams = LinearLayout.LayoutParams(
32 LinearLayout.LayoutParams.MATCH_PARENT,
33 LinearLayout.LayoutParams.WRAP_CONTENT
34 )
35
36 val textViews = arrayOfNulls<TextView>(NO_OF_TEXTVIEWS_ADDED)
37
38 for (i in 0 until NO_OF_TEXTVIEWS_ADDED) {
39 textViews[i] = TextView(this)
40 textViews[i]?.layoutParams = textViewParams
41 textViews[i]?.text = i.toString()
42 textViews[i]?.setBackgroundColor(getRandomColor())
43 linearLayout.addView(textViews[i])
44 linearLayout.invalidate()
45 }
46 }
47
48 /**
49 * Creates a random color for background color of the text view.
50 */
51 private fun getRandomColor(): Int {
52 val r = Random()
53 val red = r.nextInt(255)
54 val green = r.nextInt(255)
55 val blue = r.nextInt(255)
56
57 return Color.rgb(red, green, blue)
58 }
59}
این اکتیویتی از هیچ AdapterView یا RecyclerView برای بازیافت نماهای آیتم استفاده نمیکند. از این رو ایجاد 100 هزار نما برای تکمیل اجرای ()addRowsOfTextView لازم است.
اکنون روی دکمه Start کلیک کنید تا مصرف حافظه را در اندروید استودیو مورد نظارت قرار دهید. این مصرف حافظه رو به رشد است و در نهایت اپلیکیشن از کار میافتد. این همان رفتار مورد انتظار است.
راهحل اصلاح این مشکل سرراست است. کافی است از RecyclerView استفاده کنید تا نماهای آیتم را مورد استفاده مجدد قرار دهد و بدین ترتیب تعداد ایجاد نما را کاهش دهد. بدین ترتیب مصرف حافظه نیز کاهش مییابد. نمودار زیر مصرف حافظه را در زمان وجود همان 100 هزار نمای آیتم نشان میدهد و میبینید که بهبود زیادی در کاهش مصرف حافظه در مثال مربوط به کاربرد RecyclerView به دست آمده است.
آشفتگی مصرف حافظه در بازه زمانی کوتاه
آشفتگی نشاندهنده وجود ناپایداری است و این موضوع در مورد مصرف حافظه در اندروید نیز صدق میکند. زمانی که این نوع از الگو را ملاحظه میکنیم، معمولاً تعداد زیادی اشیای پرهزینه ایجاد شدهاند و در طی چرخه عمر کوتاهشان کنار گذاشته میشوند.
برای بازتولید این مشکل، مثال Numerous GCs را در اپلیکیشن دمو اجرا کنید. در این مثال از RecyclerView برای نمایش دو تصویر bitmap به صورت متناوب استفاده شده است که تصویر بزرگتر دارای وضوح تصویر 1000 در 1000 پیکسل و تصویر کوچکتر دارای وضوح 256 در 256 پیکسل است. RecyclerView را اسکرول کنید تا آشفتگی آشکار را در پروفایلر حافظه ببینید. همچنین تجربه کاربری در اپلیکیشن موبایل کند میشوند.
1class NumerousGCActivity: AppCompatActivity() {
2
3 val NO_OF_VIEWS = 100000
4
5
6 override fun onCreate(savedInstanceState: Bundle?) {
7 super.onCreate(savedInstanceState)
8 setContentView(R.layout.activity_numerous_gc)
9 btn_start.setOnClickListener {
10 setupRecyclerView()
11 }
12 }
13
14 private fun setupRecyclerView() {
15 val numbers = arrayOfNulls<Int>(NO_OF_VIEWS).mapIndexed { index, _ -> index }
16 recyclerView.layoutManager = LinearLayoutManager(this)
17 recyclerView.adapter = NumerousGCRecyclerViewAdapter(numbers)
18 }
19}
20
21class NumerousGCRecyclerViewAdapter(private val numbers: List<Int>): RecyclerView.Adapter<NumerousGCViewHolder>() {
22 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NumerousGCViewHolder {
23 val view = LayoutInflater.from(parent.context)
24 .inflate(R.layout.item_numerous_gc, parent, false)
25 return NumerousGCViewHolder(view)
26 }
27
28 override fun getItemCount(): Int {
29 return numbers.size
30 }
31
32 override fun onBindViewHolder(vh: NumerousGCViewHolder, position: Int) {
33 vh.textView.text = position.toString()
34
35 //Create bitmap from resource
36 val bitmap = if(position % 2 == 0)
37 BitmapFactory.decodeResource(vh.imageView.context.resources, R.drawable.big_bitmap)
38 else
39 BitmapFactory.decodeResource(vh.imageView.context.resources, R.drawable.small_bitmap)
40 vh.imageView.setImageBitmap(bitmap)
41 }
42}
43
44class NumerousGCViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
45 var textView: TextView = itemView.findViewById(R.id.text_view)
46 var imageView: ImageView = itemView.findViewById(R.id.image_view)
47}
در این حالت کد مثال برای پیادهسازی RecyclerView صحیح استفاده میشود و با این حال با مشکلات حافظه مواجه هستیم. حتی با این که RecyclerView راهحل مشکل حافظه قبلی است، اما یک راهحل همهکاره برای همه مشکلات حافظه به حساب نمیآید. برای یافتن ریشه مشکل باید اطلاعات بیشتری را مورد تحلیل قرار دهیم.
روی دکمه Record در پروفایلر حافظه کلیک کنید و برای مدتی به RecyclerView بروید سپس روی دکمه stop کلیک کنید. بدین ترتیب پروفایلر لیستی تفصیلی از مصرف حافظه از سوی انواع شیءهای مختلف در اختیار شما قرار میدهد.
فهرست را بر اساس اندازه سطحی مرتبسازی کنید تا ببینید که آیتم فوقانی یک آرایه بایت است و از این رو میدانیم که بخش بیشتر تخصیص حافظه به ایجاد آرایه بایت اختصاص دارد. تنها 32 تخصیص برای آرایه بایت وجود دارد و اندازه کلی بیتها 577,871,888 است که معادل 72.23 مگابایت است.
برای این که اطلاعات بیشتری به دست آوریم روی یکی از وهلههای نما کلیک میکنیم تا تخصیص پشته فراخوانی را نیز ببینیم. متد هایلایت شده ()onBindViewHolder از NumerousGCRecyclerViewAdapter است، اما ما هیچ آرایه بایتی را به صورت صریح با این متد نساختهایم
اگر به متد بعدی ()onBindViewHolder در پشته فراخوانی نگاه کنیم، میبینیم که این متد فراخوانیهایی در مسیر زیر اجرا میکند:
decodeResource() -> decodeResourceStream() -> decodeStream() -> nativeDecodeAsset()
و در نهایت به ()newNonMovableArray میرسد. مستندات این متد چنین اعلام میکنند که:
این متد یک آرایه تخصیصیافته در یک نتیجه از هیپ جاوا بازگشت میدهد که هرگز جابجا نخواهد شد. متد مورد اشاره برای پیادهسازی تخصیصهای نیتیو روی هیپ جاوا مانند DirectByteBuffers و Bitmaps استفاده میشود.
از این رو میتوانیم نتیجه بگیریم که مصرف بالای حافظه آرایههای بایت ناشی از استفاده از متد ()nativeDecodeAsset است.
در واقع هر بار که ما ()BitmapFactory.decodeResource را فراخوانی میکنیم، یک وهله جدید از یک شیء بیتمپ ایجاد میشود و از این رو دادههای آرایه بایت جدیدی اضافه میشوند. اگر بتوانیم فراوانی احضار ()BitmapFactory.decodeResource را کاهش دهیم میتوانیم از نیاز به تخصیص حافظه بیشتر جلوگیری کنیم و از این رو از رخداد Garbage Collection بکاهیم.
1class LessNumerousGCRecyclerViewAdapter(private val context: Context,
2 private val numbers: List<Int>): RecyclerView.Adapter<NumerousGCViewHolder>() {
3
4 val bitBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.big_bitmap)
5 val smallBitmap = BitmapFactory.decodeResource(context.resources, R.drawable.small_bitmap)
6
7 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NumerousGCViewHolder {
8 val view = LayoutInflater.from(parent.context)
9 .inflate(R.layout.item_numerous_gc, parent, false)
10 return NumerousGCViewHolder(view)
11 }
12
13 override fun getItemCount(): Int {
14 return numbers.size
15 }
16
17 override fun onBindViewHolder(vh: NumerousGCViewHolder, position: Int) {
18 vh.textView.text = position.toString()
19
20 //Reuse bitmap
21 val bitmap = if(position % 2 == 0) bitBitmap else smallBitmap
22 vh.imageView.setImageBitmap(bitmap)
23 }
24}
کد فوق نسخه بهبودیافتهای از RecyclerViewAdapter است که وهلههای بیتمپ را تنها یک بار ایجاد میکند، آنها را کش میکند و از این بیتمپها برای imageView در متد ()onBindViewHolder استفاده مجدد میکند. بدین ترتیب از هر تخصیص حافظه غیرضروری دیگری اجتناب میشود. در ادامه به گراف حافظه پس از این بهبود نگاهی میاندازیم.
چنان که میبینید گراف حافظه صاف است و تنها یک تخصیص برای آرایه بایت وجود دارد که اندازه آن ناچیز است. ضمناً اپلیکیشن اینک میتواند به طور روانی اسکرول شود و هیچ نوع گیر کردن رخ نمیدهد. بدین ترتیب مشکل حافظه را حل کردهایم.
جمع بندی
امیدواریم با مطالعه این راهنما ایدهای در مورد شیوه استفاده از ابزار پروفایل کردن برای تحلیل مشکلات عملکردی اپلیکیشنهای اندروید به دست آورده باشید. همواره به مصرف حافظه در اپلیکیشن خود توجه داشته باشید و از تخصیص منابع حافظه غیرضروری اجتناب کنید. کافی است به خاطر داشته باشید که هدف ما این است که گراف مصرف حافظه تا حد امکان مسطح باشد.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- گنجینه برنامه نویسی اندروید (Android)
- برنامه نویسی موبایل با اندروید استودیو
- ۵ گام ضروری برای یادگیری برنامهنویسی اندروید — راهنمای جامع
==
بسیار بسیار عالی بود.اگر امکانش هست مدیریت شبکه و cpu هم ادامه این پست بذارین.