Android性能優(yōu)化之卡頓監(jiān)測(cè)及問(wèn)題分析

前言

?卡頓是在用戶使用過(guò)程中很直觀的不良感受,主要分為由代碼、內(nèi)存不足等問(wèn)題引起的常規(guī)卡頓和ANR異常,我們可以利用線上和線下相結(jié)合的方式全覆蓋監(jiān)測(cè)卡頓點(diǎn),還要特別針對(duì)一些不易監(jiān)測(cè)到的難點(diǎn)進(jìn)行優(yōu)化。



1 手動(dòng)監(jiān)測(cè)

?手動(dòng)監(jiān)測(cè)方案可以查看Android性能優(yōu)化之啟動(dòng)優(yōu)化工具(TraceView、Systrace、Profiler),核心思想就是通過(guò)計(jì)算方法耗時(shí)、查看CPU的使用情況等去找到卡頓的地方并解決問(wèn)題。


2 自動(dòng)監(jiān)測(cè)

2.1 StrictMode

?如果開發(fā)者在UI線程中進(jìn)行了網(wǎng)絡(luò)操作或者IO操作,而這些操作可能會(huì)影響到App的性能,甚至出現(xiàn)ANR對(duì)話框。Android提供了一種運(yùn)行時(shí)檢測(cè)機(jī)制,主要用于檢測(cè)兩大問(wèn)題:
一、線程策略
自定義的耗時(shí)調(diào)用,detectCustomSlowCalls()
磁盤讀取操作,detectDiskReads
網(wǎng)絡(luò)請(qǐng)求,detectNetwork
二、虛擬機(jī)策略
Activity泄露,detectActivityLeaks()
Sqlite對(duì)象泄露。detectLeakedSqliteObjects
檢測(cè)實(shí)例數(shù)量,setClassIntancceLimit()

2.2 StrictMode實(shí)戰(zhàn)

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        initStrictMode(true)
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        writeToExternalStorageInMainThread()//模擬Closable對(duì)象未關(guān)閉
    }
    fun writeToExternalStorageInMainThread() {
        val externalStorage: File = Environment.getExternalStorageDirectory()
        val destFile = File(externalStorage, "hello.txt")
        try {
            val output: OutputStream = FileOutputStream(destFile, true)
            output.write("I am testing io".toByteArray())
            output.flush()
            output.close()
        } catch (e: FileNotFoundException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

    private fun initStrictMode(isDebug: Boolean) {
        if (isDebug) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectCustomSlowCalls()//API 11
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork() //or .detectAll()
                    .penaltyLog()//在Logcat 中發(fā)音違規(guī)異常信息  or .penalDialog()
                    .build()
            )
            StrictMode.setVmPolicy(
                StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .setClassInstanceLimit(ExplainAction::class.java, 1)
                    .detectActivityLeaks()
                    .detectLeakedClosableObjects()//監(jiān)測(cè)Closable對(duì)象
                    .penaltyLog()
                    .build()
            )
        }
    }
}
#

?我們可以通過(guò)使用StrictMode進(jìn)行實(shí)例監(jiān)測(cè)、或者內(nèi)存泄露監(jiān)測(cè)等等,這些問(wèn)題都是可能引起卡頓的問(wèn)題點(diǎn)。上面運(yùn)行代碼之后可以在Logcat里看到錯(cuò)誤的堆棧信息:


錯(cuò)誤日志

3 Looper

?利用UI線程中,事件發(fā)生時(shí)Looper.loop會(huì)執(zhí)行dispatchMessage的原理,如果UI線程發(fā)生卡頓,即dispatchMessage發(fā)生了卡頓。因此我們可在分發(fā)消息前和分發(fā)消息后時(shí)間間隔與我們?cè)O(shè)置的閾值對(duì)比,實(shí)現(xiàn)卡頓自動(dòng)化監(jiān)測(cè)。

public static void loop() {
    final Looper me = myLooper();
    final MessageQueue queue = me.mQueue;
    for (;;) {
        Message msg = queue.next(); 
        //可替換成自己的Printer
        Printer logging = me.mLogging;
        //1.消息分發(fā)前打印日志
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        //2.消息分發(fā)
        msg.target.dispatchMessage(msg);
        //3.消息分發(fā)后打印日志
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
        }
        msg.recycleUnchecked();
    }
}

?記錄事件發(fā)生前后的事件,并與閾值進(jìn)行對(duì)比。

package com.example.myapplication

import android.os.Looper
import android.util.Log

class LogMonitor{
    companion object{
        var thresholdTime = 100;
        private var isStart = true
        private var mStartTime = 0L;
        private var mEndTime = 0L;
        fun recordTime(){
            if (isStart){
                mStartTime = System.currentTimeMillis();
                isStart = false
            }else{
                mEndTime = System.currentTimeMillis()
                isStart = true
                if (isBlock(mEndTime)){
                    Log.d("LogMonotor","發(fā)生卡頓了。。。")
                }
            }
        }

        private fun isBlock(endTime:Long):Boolean{
            return endTime- mStartTime> thresholdTime
        }
    }
}

?在Application的Oncreate中實(shí)現(xiàn)對(duì)Printer的替換。

class MyApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        mainLooper.setMessageLogging {
            LogMonitor.recordTime()
        }
    }
}

?我們還可以使用第三方庫(kù)AndroidPerformanceMonitor進(jìn)行卡頓監(jiān)測(cè)。AndroidPerformanceMonitor就是利用了以上所述相同的原理。
?我們可以利用此方法頻繁采集堆棧信息,并將超過(guò)閾值的堆棧存入文件中上傳給服務(wù)器,在上傳前對(duì)文件進(jìn)行Hash排重處理。文件過(guò)大是首先可將文件拆分,然后使用Hash除余將相同的的堆棧放入一個(gè)文件進(jìn)行排重,最后合并文件上傳。


總結(jié)

?造成卡頓的主要原因有以下幾點(diǎn):
1.過(guò)于復(fù)雜的布局
原因:UI布局層次太深, 或是自定義控件的onDraw中有復(fù)雜運(yùn)算, CPU的相關(guān)運(yùn)算就可能大于16ms, 導(dǎo)致卡頓。
解決方案:可通過(guò)Android Studio的Layout Inspector去查看層級(jí),并改善層級(jí)深度,在開發(fā)中建議使用ConstrainLayout改善減少層級(jí)。


2.過(guò)度繪制
原因:像素被多次繪制。
解決方案:可在開發(fā)者模式中,打開顯示邊界布局,查看繪制顏色。

1.原色 – 沒(méi)有被過(guò)度繪制 – 這部分的像素點(diǎn)只在屏幕上沒(méi)有繪制。
2.藍(lán)色 – 1次過(guò)度繪制– 這部分的像素點(diǎn)只在屏幕上繪制了一次。
3.綠色 – 2次過(guò)度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了二次。
4.粉色 – 3次過(guò)度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了三次。
5.紅色 – 4次過(guò)度繪制 – 這部分的像素點(diǎn)只在屏幕上繪制了四次及以上。

2.1clipRect、clipRect
?使用canvas.clipRect后,繪制區(qū)域之外的繪制指令都不會(huì)被執(zhí)行,那些部分內(nèi)容在矩形區(qū)域內(nèi)的組件,仍然會(huì)得到繪制。
?canvas.quickreject()來(lái)判斷是否沒(méi)和某個(gè)矩形相交,從而跳過(guò)那些非矩形區(qū)域內(nèi)的繪制操作
3. 耗時(shí)事件
原因:在UI線程中執(zhí)行耗時(shí)事件,會(huì)導(dǎo)致UI線程loop卡頓。
解決方案:如果UI線程發(fā)生卡頓,即dispatchMessage發(fā)生了卡頓。我們可在dispatchMessage的原理分發(fā)消息前和分發(fā)消息后時(shí)間間隔與我們?cè)O(shè)置的閾值對(duì)比。
4. 頻繁GC
原因:短時(shí)間內(nèi)創(chuàng)建大量對(duì)象進(jìn)入新生區(qū),導(dǎo)致頻繁的GC。gc會(huì)大量占用ui線程和cpu資源,會(huì)導(dǎo)致app整體卡頓
解決方案:使用Profiler查看CPU抖動(dòng)位置,跟蹤內(nèi)存分配情況找到對(duì)象重復(fù)創(chuàng)建的對(duì)象。
5. 內(nèi)存不足
原因:低內(nèi)存會(huì)導(dǎo)致磁盤 IO 變多, 如果頻繁進(jìn)行磁盤 IO , 由于磁盤IO 很慢, 那么主線程會(huì)有很多進(jìn)程處于等 IO 的狀態(tài)。
解決方案:使用 SharedPerforence 時(shí)用 Apply而不是commit等等。
6 幀率與刷新率不匹配
原因:屏幕幀率和系統(tǒng)的 fps 不相符 , 那么有可能會(huì)導(dǎo)致畫面不是那么順暢. 比如使用 90 Hz 的屏幕搭配 60 fps 的動(dòng)畫
解決方案:使用以下代碼獲取屏幕刷新率,根據(jù)屏幕刷新率進(jìn)行動(dòng)畫計(jì)算。

Display display = getWindowManager().getDefaultDisplay();
float refreshRate = display.getRefreshRate();

?除以上問(wèn)題外,造成卡頓的原因還有GPU頻繁渲染、頻繁調(diào)用 buildDrawingCache、使用CPU渲染而不是使用GPU、WebView 性能不足等等問(wèn)題都會(huì)造成卡頓。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 界面是 Android 應(yīng)用中直接影響用戶體驗(yàn)最關(guān)鍵的部分。如果代碼實(shí)現(xiàn)得不好,界面容易發(fā)生卡頓且導(dǎo)致應(yīng)用占用...
    passiontim閱讀 1,822評(píng)論 0 8
  • 界面是 Android 應(yīng)用中直接影響用戶體驗(yàn)最關(guān)鍵的部分。如果代碼實(shí)現(xiàn)得不好,界面容易發(fā)生卡頓且導(dǎo)致應(yīng)用占用大量...
    Ten_Minutes閱讀 693評(píng)論 0 9
  • 注:本文是我在 Android 界面性能調(diào)優(yōu)知識(shí)的系統(tǒng)性總結(jié),純屬個(gè)人碎碎念。秉持開源分享的原則發(fā)布本文出來(lái),各位...
    東經(jīng)315度閱讀 737評(píng)論 0 8
  • (一)海邊觀日出 遠(yuǎn)望寬寬無(wú)際天, 長(zhǎng)波卷卷水相連。 倉(cāng)忙誰(shuí)料烏云布, 唯見(jiàn)滔滔點(diǎn)點(diǎn)帆。
    開宗明義閱讀 1,018評(píng)論 4 1
  • 職業(yè)化問(wèn)題背后的思維方式:永遠(yuǎn)要站在對(duì)方舒不舒服的角度考慮問(wèn)題。 不要用微信問(wèn)對(duì)方“親,在嗎?”而是直接簡(jiǎn)單扼要的...
    w小郭閱讀 355評(píng)論 0 0