閱讀本文大概需要 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内存洩露
進入公衆号,回複“程序員“可以領取一份計算機技術電子書福利合集
歡迎轉發,關注公衆号 肖晖
每天幾分鐘,掌握一個硬核面試知識點
,