android性能優化(二)之卡頓優化

閱讀本文大概需要 5 分鐘。

 

 

相對於其他類型的性能指標,卡頓是能直接讓用戶產生視覺反饋的現象,比如App反應滯後於用戶的操作,在嚴重的情況下會出現ANR。關乎用戶體驗的大事,是很容易遭到用戶吐槽的。因此,開發人員平時寫代碼時必須要時刻提醒自己不要落入卡頓的陷阱之中。

 

一. 卡頓原因

 

在羅列卡頓可能會發生的幾個點之前,先簡單介紹一下發生卡頓的原因。

 

在之前《handler系列》聊過,UI線程是基於queue中的message事件驅動的,事件 -> 執行 -> 下一個事件...,另一方面由於Android的幀率是60fps,也就是每16ms就會觸發一次UI刷新,如果某個message的處理時間 > 16ms,就會導致接收到VSYNC信號的時候無法完成本次刷新操作,產生掉幀現象。

 

因此,從本質上來講,我們必須讓UI線程的任何事件在16ms之內解決戰鬥。

 

基於此,可能會導致卡頓的原因有三大類:

1)事件本身太耗時。

2)事件本身並不耗時,但需要等待別的地方返回耗時。

3)UI線程本身已經拿不到CPU資源來執行事件。

 

下面根據這三大類來分別具體細聊。

 

二. 耗時事件

 

這個很容易理解,就是把一些耗時業務邏輯直接寫在了UI線程中,比如計算密集型的複雜計算,龐大的MD5計算,非對稱RSA解密等。一般情況下,開發人員都不會犯這種錯誤,因爲能夠直接意識到計算量很大,本身就有警醒的作用。

 

三.耗時等待

 

1)網絡I/O 同步請求

這種如果是在用以前比較老的網絡庫,比如URLConnection這種就需要開發人員自己來開啓新的線程。開發者可能忘記開啓子線程,又同時做了同步請求等待,導致卡頓的發生。但是現代網絡庫比如Okhttp,Retrofit已經幫我們準備好了線程池,一般不會再遇到。 

 

2)磁盤I/O 文件,數據庫

一般的文件和數據庫操作,大家可能都會自覺的在子線程中操作。但是值得一提的是SharedPreference的存儲和讀取,根據sp的設計,創建的時候會開啓子線程把整個文件全部加載進內存,加載完畢再通知主線程,如果讀取尚未結束,此時想獲取某個key的值,主線程就必須等待加載完畢爲止。

 

因此,如果你的sp文件比較大,那麼會帶來幾個嚴重問題:

a)第一次從sp中獲取值的時候,有可能阻塞主線程,使界面卡頓、掉幀。

b)解析sp的時候會產生大量的臨時對象,導致頻繁GC,引起界面卡頓。

c)這些key和value會永遠存在於內存之中,不會被釋放,佔用大量內存。

所以千萬不要把龐大的key/value存在sp中,比如把複雜的json當value。

 

另外對於sp的存儲,commit是同步操作,要在子線程中使用。而apply雖然是在子線程執行的,但是無節制地apply也會造成卡頓,原因是每次有系統消息發生的時候(handleStopActivity,handlePauseActivity)都會去檢查已經提交的apply寫操作是否完成,如果沒有完成則阻塞主線程。

 

3)跨進程Binder同步等待返回數據

 

四.CPU時間片

 

1)其他應用發生搶佔CPU資源的情況,導致本應用無法獲得CPU執行時間片。

2)線程間發生死鎖,UI線程無法獲取鎖,導致無法繼續執行。

3)頻繁GC,內存抖動。GC的次數越多,消耗在GC上的時間就越長,CPU花在界面繪製上的時間相應就越短。

 

五. 分析

 

對於卡頓的分析手段,有很多工具可以使用,下面介紹幾種。

 

1)TraceView

相比之下,TraceView是分析卡頓的神兵利器,它不僅能看出每個方法消耗的時間、發生次數,並且可以進行排序,直接從最耗時的方法開始處優化。

 

2)ANR-WatchDog

其原理簡單來說就是開啓一個子線程,設置tick = interval,然後每隔一個interval(可設置)就往UI線程queue中扔一個runnable,若UI線程沒卡頓,則interval時間內會取出此runnable執行,即重置tick,那麼下一個interval循環時根據檢測此tick是否被重置來判斷是否有卡頓發生。如果有,則打印此時的各個線程運行時的stack trace(可設置只打印主線程),以幫助定位。

 

3)AndroidPerformanceMonitor

AndroidPerformanceMonitor 是國人開發的一個檢測卡頓的開源庫,原名是BlockCanary,可以設置卡頓檢測時間,debug模式下檢測到的卡頓可以通知展示(基本和LeakCanary一樣),這個在開發自測時很有用。

 

其基本原理稍有不同,它並沒有採用新開線程自己往UI線程裏扔runnable的這種普通思想。而是利用系統在loop()方法裏取出message前後進行了log打印這一特點,來重寫Printer的println(String)方法,根據message處理前後的時間差,來判斷是否發生了卡頓。

 

public static void loop() {
    ...

    for (;;) {
        ...

        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        msg.target.dispatchMessage(msg);

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        ...
    }
}

 

而且這個工具在卡頓發生時,收集的信息還比較豐富,包括基本信息,耗時信息,CPU信息,堆棧信息等。

 

4)ANR trace.txt

而對於ANR,每當測試跑monkey一晚下來,ANR必是log的重點關注對象,若存在ANR,測試肯定會開jira貼上log給開發解決。對於trace.txt的分析,有幾個基本的點是需要重點關注的:

a)具體的call stack指向的具體代碼,是否是卡頓發生的原因。

b)是否有lock相關的關鍵字,代表可能發生死鎖。

c)是否有iowait字樣,是否在UI線程發生了網絡或者磁盤I/O。

d)CPU使用率是否很高,很高表示要麼自身有計算密集型任務發生,要麼在其他地方有搶佔CPU資源的任務。很低說明非耗時計算導致,可懷疑死鎖和I/O耗時等待。

 

六. 解決

 

只要通過log分析能夠找到發生卡頓的代碼,基本上可以宣告問題很容易解決了,因爲無論是對於耗時事件還是耗時等待,都可以採取異步的方式搞定。

 

而對於被搶佔時間片的場景:

1)如果是死鎖,則需要fix發生死鎖的漏洞;

2)如果排除了以上所有可能後,就可以懷疑卡頓是由於被其他應用搶佔CPU或者GC抖動導致,這需要通過log中的CPU使用率,和memory相關的回收信息,或者通過在debug模式下場景復現,綜合profiler來觀察和確定。

 

對於無法找到定位但是能夠復現的場景,還可以根據業務場景來log打印時間,逐步縮小可疑代碼的範圍,從而排查和定位原因。

 

七. 總結

 

總之,關於卡頓的分析,並不是所有卡頓發生了都能找到原因,相反,大量ANR發生後通過log分析來解決是非常棘手的,甚至根本無從下手。所以我的觀點是,對於卡頓一定要在開發寫代碼時做好警惕,養成良好習慣纔是正道,防範爲主,解決問題爲輔。

 

 

一切從android的handler說起(一)之message

一切從android的handler說起(二)之threadLocal

一切從android的handler說起(三)之UI線程不卡頓

一切從android的handler說起(四)之postDelay原理

一切從android的handler說起(五)之觸摸事件模型

一切從android的handler說起(六)之生命週期來源

一切從android的handler說起(七)之Handler內存泄露


 

進入公衆號,回覆“程序員“可以領取一份計算機技術電子書福利合集

歡迎轉發,關注公衆號 肖暉

每天幾分鐘,掌握一個硬核面試知識點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章