卡頓分析
造成卡頓的原因可能是多種多樣的,但是最終都會(huì)反映在CPU時(shí)間上。Android系統(tǒng)是基于Linux的奋隶,可以CPU時(shí)間分為兩種:
- 用戶時(shí)間——執(zhí)行用戶態(tài)應(yīng)用程序代碼消耗的時(shí)間苛让。
- 系統(tǒng)時(shí)間——執(zhí)行內(nèi)核態(tài)系統(tǒng)調(diào)用的時(shí)間火惊。包括I/O、鎖累贤、中斷以及其他系統(tǒng)調(diào)用的執(zhí)行時(shí)間叠穆。
CPU的相關(guān)問(wèn)題可以分為三類:
- CPU資源冗余使用
算法效率太低——主要出現(xiàn)在數(shù)據(jù)的查找、排序臼膏、刪除等環(huán)節(jié)
沒(méi)有使用緩存——比如bitmap的復(fù)用硼被,圖片的緩存等,圖片的讀取會(huì)涉及文件I/O或者網(wǎng)絡(luò)I/O渗磅,Bitmap的創(chuàng)建涉及解碼祷嘶。這些操作對(duì)于CPU來(lái)說(shuō)都是耗時(shí)操作屎媳,應(yīng)該盡量避免。
計(jì)算時(shí)使用的數(shù)據(jù)結(jié)構(gòu)不對(duì)——可以用int類型處理的數(shù)據(jù)運(yùn)算论巍,卻使用long或者double烛谊,這會(huì)導(dǎo)致CPU的運(yùn)算負(fù)載多出4倍。 - CPU資源搶占
主線程的CPU資源被搶占——這是最常見(jiàn)的問(wèn)題嘉汰,在Android6.0之前沒(méi)有RenderThread的時(shí)候丹禀,頁(yè)面繪制渲染的工作都是在主線程中完成,主線程處理這些工作需要的CPU資源被子線程搶占鞋怀,就會(huì)導(dǎo)致頁(yè)面繪制渲染工作無(wú)法及時(shí)完成双泪。
音視頻播放的資源被搶占——音視頻編解碼本身會(huì)消耗大量的CPU資源,并且流暢的視頻播放會(huì)解碼速度是有硬性要求的密似,如果達(dá)不到就可能導(dǎo)致音視頻播放效果不流暢焙矛。
線程過(guò)多——如果同時(shí)競(jìng)爭(zhēng)CPU資源的線程過(guò)多,就會(huì)導(dǎo)致同一段時(shí)間內(nèi)残腌,每個(gè)線程分配到的CPU資源偏少村斟,從而導(dǎo)致線程執(zhí)行任務(wù)的耗時(shí)增加。所以在使用線程池時(shí)抛猫,要結(jié)合運(yùn)行設(shè)備的CPU的核心數(shù)蟆盹,來(lái)合理設(shè)置線程數(shù)。 - CPU使用率低
當(dāng)系統(tǒng)處理磁盤或者網(wǎng)絡(luò)IO闺金、同步鎖的競(jìng)爭(zhēng)和線程切換逾滥、線程的休眠等操作時(shí),會(huì)降低CPU的使用率败匹。
通過(guò)shell命令寨昙,了解CPU的性能
查看CPU的相關(guān)信息
//在Android studio的Terminal窗口
//先輸入adb shell進(jìn)入當(dāng)前已連接手機(jī)的shell環(huán)境
adb shell
//獲取手機(jī)CPU的核心數(shù),如下圖所示掀亩,當(dāng)前連接的手機(jī)CPU為8核
cat /sys/devices/system/cpu/possible
//獲取第一個(gè)CPU的最大頻率
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
//獲取第二個(gè)CPU的最小頻率
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
執(zhí)行結(jié)果如下圖所示:通過(guò)/proc/stat命令舔哪,查看CPU耗時(shí)
//查看整個(gè)系統(tǒng)的CPU使用情況
cat /proc/stat
//查看當(dāng)前正在調(diào)試的app所在進(jìn)程的CPU使用情況
cat /proc/self/stat
//查看指定的某個(gè)進(jìn)程的CPU使用情況
cat /proc/[pid]/stat
/proc/[pid]/stat // 進(jìn)程CPU使用情況
/proc/[pid]/task/[tid]/stat // 進(jìn)程下面各個(gè)線程的CPU使用情況
/proc/[pid]/sched // 進(jìn)程CPU調(diào)度相關(guān)
/proc/loadavg // 系統(tǒng)平均負(fù)載,uptime命令對(duì)應(yīng)文件
執(zhí)行結(jié)果如下:使用top命令查看各進(jìn)程的CPU使用情況
// 直接使用top命令會(huì)定時(shí)不斷地輸出進(jìn)程的相關(guān)信息
top
// 排除0%的進(jìn)程信息
top | grep -v '0% S'
// 獲取指定進(jìn)程的CPU归榕、內(nèi)存消耗尸红,并設(shè)置刷新間隔
top -d 1 | grep pers.jay.wanandroid
使用ps命令查看進(jìn)程消耗CPU的時(shí)間占比
// 查看指定進(jìn)程的狀態(tài)信息
ps -p 6440
上述指令中的6440是進(jìn)程ID,執(zhí)行結(jié)果如下上圖中各輸出參數(shù)的含義如下
- USER:用戶名
- PID:進(jìn)程ID
- PPID:父進(jìn)程ID
- VSZ:虛擬內(nèi)存大小刹泄,以K為單位
- RSS:常駐內(nèi)存大型饫铩(正在使用的頁(yè))
- WCHAN:進(jìn)程在內(nèi)核態(tài)的運(yùn)行時(shí)間
- nstruction pointer:指令指針
- NAME:進(jìn)程名字
- S:當(dāng)前進(jìn)程的狀態(tài),總共有10種可能的狀態(tài):R (running) S (sleeping) D (device I/O) T (stopped) t (traced) Z (zombie) X (deader) x (dead) K (wakekill) W (waking)特石。
查看指定進(jìn)程已經(jīng)消耗的CPU時(shí)間占系統(tǒng)總時(shí)間的百分比
// 查看指定進(jìn)程已經(jīng)消耗的CPU時(shí)間占系統(tǒng)總時(shí)間的百分比
ps -o PCPU -p 6440
使用dumpsys cpuinfo命令查看CPU使用情況
使用dumpsys cpuinfo命令獲得的信息比起top命令得到的信息要更加精煉盅蝗,可以看到一段時(shí)間內(nèi),系統(tǒng)內(nèi)正在運(yùn)行的所有進(jìn)程姆蘸,以及各進(jìn)程的CPU使用情況墩莫。卡頓優(yōu)化的工具
StrictMode
StrictMode芙委,是Android提供一種運(yùn)行時(shí)檢測(cè)機(jī)制,可以幫助開發(fā)人員檢測(cè)代碼中一些不規(guī)范的問(wèn)題狂秦。對(duì)于規(guī)模較大的項(xiàng)目灌侣,代碼量也很大,如果只是肉眼去review代碼裂问,不僅效率非常低侧啼,而且也比較容易出問(wèn)題。使用StrictMode之后堪簿,系統(tǒng)會(huì)自動(dòng)檢測(cè)出主線程中的一些異常情況痊乾。并且按照自定義的配置給出相應(yīng)的反應(yīng)。
StrictMode主要用來(lái)檢測(cè)兩方面的問(wèn)題:
- 線程策略
線程策略的檢測(cè)內(nèi)容椭更,是一些自定義的耗時(shí)操作哪审,磁盤讀取以及網(wǎng)絡(luò)請(qǐng)求等。主要用于檢測(cè)主線程中的耗時(shí)操作虑瀑。 - 虛擬機(jī)策略
虛擬機(jī)策略的檢測(cè)內(nèi)容主要是Activity泄漏湿滓、Sqlite對(duì)象泄漏和檢測(cè)實(shí)例數(shù)量。
StrictMode使用如下:
//設(shè)置線程策略
StrictMode.ThreadPolicy threadPolicy = new StrictMode.ThreadPolicy.Builder()
// .detectCustomSlowCalls() //API等級(jí)11缴川,使用StrictMode.noteSlowCode
// .detectDiskReads()
// .detectDiskWrites()
// .detectNetwork()
.detectAll() //detectAll() for all detectable problems
// .penaltyDialog() //可以直接彈出警報(bào)Dialog
// .penaltyDeath() //或者直接崩潰
.penaltyLog() //在Logcat 中打印違規(guī)異常信息
.build();
StrictMode.setThreadPolicy(threadPolicy);
//虛擬機(jī)策略
StrictMode.VmPolicy vmPolicy = new StrictMode.VmPolicy.Builder()
.detectActivityLeaks() //檢測(cè)Activity對(duì)象的泄漏
.detectLeakedSqlLiteObjects() //檢測(cè)Sqlite對(duì)象的泄漏
.setClassInstanceLimit(Danmakus.class,1)
.detectAll()
.penaltyLog()
.build();
StrictMode.setVmPolicy(vmPolicy);
在上述代碼中茉稠,我通過(guò)設(shè)置setClassInstanceLimit(Danmakus.class,1)來(lái)限制在程序運(yùn)行時(shí)Danmakus類的對(duì)象只能有一個(gè)描馅,當(dāng)StrictMode檢測(cè)到有多個(gè)Danmakus類的對(duì)象時(shí)把夸,就會(huì)報(bào)出如下提示:
2020-07-24 14:16:06.105 19773-19773/com.zacky.bulletchattest D/StrictMode: StrictMode policy violation: android.os.strictmode.InstanceCountViolation: class master.flame.danmaku.danmaku.model.android.Danmakus; instances=11; limit=1
at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
2020-07-24 14:16:29.579 19773-19773/com.zacky.bulletchattest D/StrictMode: StrictMode policy violation; ~duration=56 ms: android.os.strictmode.DiskWriteViolation
at android.os.StrictMode$AndroidBlockGuardPolicy.onWriteToDisk(StrictMode.java:1460)
at java.io.FileOutputStream.<init>(FileOutputStream.java:236)
at java.io.FileOutputStream.<init>(FileOutputStream.java:119)
at java.io.FileWriter.<init>(FileWriter.java:63)
at com.android.server.am.ActivityManagerServiceInjector.writeToNode(ActivityManagerServiceInjector.java:1726)
at com.android.server.am.ActivityManagerServiceInjector.setTopAppUIThread(ActivityManagerServiceInjector.java:1716)
at com.android.server.am.ActivityManagerService.applyOomAdjLocked(ActivityManagerService.java:25240)
at com.android.server.am.ActivityManagerService.updateOomAdjLocked(ActivityManagerService.java:25943)
at com.android.server.am.ActivityStack.resumeTopActivityInnerLocked(ActivityStack.java:2871)
at com.android.server.am.ActivityStack.resumeTopActivityUncheckedLocked(ActivityStack.java:2474)
at com.android.server.am.ActivityStackSupervisor.resumeFocusedStackTopActivityLocked(ActivityStackSupervisor.java:2352)
at com.android.server.am.ActivityStack.completePauseLocked(ActivityStack.java:1726)
at com.android.server.am.ActivityStack.activityPausedLocked(ActivityStack.java:1646)
at com.android.server.am.ActivityManagerService.activityPaused(ActivityManagerService.java:8523)
at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:225)
at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3374)
at android.os.Binder.execTransact(Binder.java:726)
BlockCanary
BlockCanary對(duì)主線程操作進(jìn)行了完全透明的監(jiān)控,并能輸出有效的信息铭污,幫助開發(fā)分析恋日、定位到問(wèn)題所在,迅速優(yōu)化應(yīng)用嘹狞。其特點(diǎn)有:
- 非侵入式岂膳,簡(jiǎn)單的兩行就打開監(jiān)控,不需要到處打點(diǎn)磅网,破壞代碼優(yōu)雅性谈截。
- 精準(zhǔn),輸出的信息可以幫助定位到問(wèn)題所在(精確到行)涧偷,不需要像Logcat一樣簸喂,慢慢去找。
BlockCanary的原理:基于Android的消息處理機(jī)制燎潮。熟悉消息處理機(jī)制的同學(xué)都知道喻鳄,一個(gè)線程最多只有一個(gè)Looper對(duì)象與之關(guān)聯(lián)。應(yīng)用程序的主線程也是如此确封,在ActivityThread的main方法中除呵,會(huì)調(diào)用Looper.prepareMainLooper()方法再菊,來(lái)為主線程創(chuàng)建關(guān)聯(lián)的Looper對(duì)象。
private static Looper sMainLooper; // guarded by Looper.class
...
/**
* Initialize the current thread as a looper, marking it as an
* application's main looper. The main looper for your application
* is created by the Android environment, so you should never need
* to call this function yourself. See also: {@link #prepare()}
*/
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
/** Returns the application's main looper, which lives in the main thread of the application.
*/
public static Looper getMainLooper() {
synchronized (Looper.class) {
return sMainLooper;
}
}
從上面的代碼也可以看出颜曾,prepareMainLooper()方法保證了主線程的Looper對(duì)象只會(huì)創(chuàng)建一次纠拔。這樣不管在主線程中,創(chuàng)建了多少個(gè)Handler對(duì)象來(lái)發(fā)送和處理各種消息泛豪,最終都會(huì)通過(guò)這個(gè)Looper對(duì)象來(lái)進(jìn)行消息的分發(fā)绿语,即調(diào)用Handler的dispatchMessage方法
在Looper.loop方法中有這樣一段代碼:
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);
}
...
}
}
可以看到在dispatchMessage方法的前后,都有一個(gè)Printer類型的對(duì)象logging在打印信息候址。也就是說(shuō)吕粹,只需要比較開始和結(jié)束信息的打印時(shí)間,就可以得到dispatchMessage方法的耗時(shí)岗仑。如果主線程中存在耗時(shí)操作匹耕,那肯定會(huì)體現(xiàn)在dispatchMessage的執(zhí)行時(shí)間上。那么我們可以給主線程的Looper對(duì)象設(shè)置一個(gè)自定義的Printer荠雕,來(lái)實(shí)現(xiàn)對(duì)每一次dispatchMessage方法執(zhí)行耗時(shí)的監(jiān)控稳其。
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
并在mainLooperPrinter中判斷start和end,來(lái)獲取主線程dispatch該message的開始和結(jié)束時(shí)間炸卑,并判定該時(shí)間超過(guò)閾值(如2000毫秒)為主線程卡慢發(fā)生既鞠,此時(shí)就通過(guò)子線程dump出卡慢發(fā)生時(shí)的各種信息,提供開發(fā)者分析性能瓶頸盖文。
...
@Override
public void println(String x) {
if (!mStartedPrinting) {
mStartTimeMillis = System.currentTimeMillis();
mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
mStartedPrinting = true;
} else {
final long endTime = System.currentTimeMillis();
mStartedPrinting = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
}
}
private boolean isBlock(long endTime) {
return endTime - mStartTimeMillis > mBlockThresholdMillis;
}
...
核心流程圖如下:
具體的監(jiān)控流程可以歸納為如下步驟:
- 首先通過(guò)Looper.getMainLooper().setMessageLogging()為主線程的Looper對(duì)象設(shè)置自定義的Printer實(shí)現(xiàn)類來(lái)打印輸出logging嘱蛋。這樣在每次執(zhí)行dispatchMessage方法前后都會(huì)調(diào)用我們自定義的Printer類。
- 在自定義的Printer類的println方法中五续,通過(guò)匹配字符串洒敏,如果匹配到">>>>> Dispatching to ",則開始在子線程去執(zhí)行獲取當(dāng)前主線程堆棧信息的任務(wù)疙驾,這個(gè)任務(wù)同時(shí)會(huì)獲取當(dāng)前的一些場(chǎng)景信息凶伙,比如內(nèi)存、CPU和網(wǎng)絡(luò)狀態(tài)等信息它碎。
- 如果在指定的時(shí)間閾值內(nèi)函荣,再次調(diào)用println方法匹配到了“<<<<< Finished to ”,就說(shuō)明dispatchMessage方法的耗時(shí)正常扳肛,沒(méi)有發(fā)生卡頓傻挂,那我們就可以將子線程的任務(wù)取消掉。
應(yīng)用發(fā)生卡頓時(shí)敞峭,BlockCanary除了能在開發(fā)調(diào)試時(shí)提供信息界面讓開發(fā)和測(cè)試人員直接看到卡頓原因之外踊谋,其最大的作用就是在App發(fā)布到線上后,也可以進(jìn)行大范圍的Log采集和分析旋讹,主要是從兩個(gè)維度進(jìn)行分析:一是卡頓時(shí)間殖蚕,二是根據(jù)相同堆棧出現(xiàn)的次數(shù)來(lái)對(duì)卡頓原因進(jìn)行排序和歸類轿衔。下圖演示了實(shí)際開發(fā)調(diào)試時(shí),BlockCanary的卡頓信息提示頁(yè)面
BlockCanary的優(yōu)點(diǎn):
非侵入式
方便精準(zhǔn)睦疫,能定位到具體的某一行代碼
BlockCanary的缺陷:
還有一個(gè)問(wèn)題是瞧掺,調(diào)用Printer的println方法時(shí)耕餐,會(huì)涉及到字符串拼接,所以在短時(shí)間內(nèi)處理大量任務(wù)辟狈,類似快速滑動(dòng)recyclerView導(dǎo)致頁(yè)面刷新等操作時(shí)肠缔,會(huì)增加內(nèi)存消耗,甚至觸發(fā)GC等哼转。
AspectJ
AspectJ是一個(gè)實(shí)現(xiàn)AOP的框架明未。在Android平臺(tái)上,常用的是Hujiang團(tuán)隊(duì)開源的AspectJ插件壹蔓。它的工作原理是:通過(guò)Gradle Transform趟妥,在編譯期間class文件生成后至dex文件生成前,遍歷并匹配所有符合AspectJ定義的切點(diǎn)處佣蓉,插入定義好的代碼披摄,從而處理相應(yīng)的邏輯亲雪。AspectJ常用于打印方法的耗時(shí),權(quán)限檢查疚膊,統(tǒng)計(jì)按鈕事件的點(diǎn)擊次數(shù)等义辕。在Android上的應(yīng)用主要是做性能監(jiān)控,基于注解的數(shù)據(jù)埋點(diǎn)寓盗。具體的使用教程可以參考Android AspectJ詳解和AspectJ AOP教程:實(shí)現(xiàn)Android基于注解無(wú)侵入埋點(diǎn)灌砖、性能監(jiān)控。
另外傀蚌,常見(jiàn)的基于AspectJ實(shí)現(xiàn)的有大神JakeWharton的Hugo框架基显。
AspectJ的弊端:由于其基于規(guī)則,所以切入點(diǎn)相對(duì)固定善炫,對(duì)于字節(jié)碼文件的操作自由度以及開發(fā)的掌握度都要打一定的折扣续镇。,并且它會(huì)額外生成一些包裝代碼销部,對(duì)性能以及包大小都有一定的負(fù)擔(dān)摸航。
雖然AspectJ非常強(qiáng)大,但是它也只能實(shí)現(xiàn)50%的字節(jié)碼操作場(chǎng)景舅桩,如果要實(shí)現(xiàn)100%的字節(jié)碼操作場(chǎng)景酱虎,就需要使用ASM。
ASM
ASM基本上可以實(shí)現(xiàn)任何對(duì)字節(jié)碼的操作擂涛,也就是自由度和開發(fā)的掌握度很高读串。它提供了訪問(wèn)者模式來(lái)訪問(wèn)字節(jié)碼文件,并且只注入我們想要注入的代碼撒妈。ASM是諸多JVM語(yǔ)言欽定的字節(jié)碼生成庫(kù)恢暖,它在效率和性能方面的優(yōu)勢(shì)要遠(yuǎn)超其他的字節(jié)碼操作庫(kù)如AspectJ。
ASM的優(yōu)點(diǎn):
- 適宜處理簡(jiǎn)單類的修改
- 學(xué)習(xí)成本較低
- 代碼量較少
ASM的缺點(diǎn): - 處理大量信息會(huì)使代碼變得復(fù)雜
- 代碼難以復(fù)用
Lancet
Lancet是一個(gè)輕量級(jí)的Android AOP框架狰右。由eleme團(tuán)隊(duì)開源分享杰捂。它的特點(diǎn)是:
- 編譯速度快,并且支持增量編譯棋蚌。
- 簡(jiǎn)潔的API嫁佳,幾行Java代碼就能完成注入需求。
- 沒(méi)有任何多余代碼插入APK.
- 支持用于SDK谷暮,可以在SDK編寫注入代碼來(lái)修改依賴SDK的App蒿往。
DroidAssist
DroidAssist由滴滴團(tuán)隊(duì)開源,是一個(gè)輕量級(jí)的Android字節(jié)碼編輯插件湿弦,基于Javassist對(duì)字節(jié)碼操作瓤漏,根據(jù)xml配置class文件,以達(dá)到對(duì)class文件進(jìn)行動(dòng)態(tài)修改的效果。與其他AOP方案不同蔬充。DroidAssist提供了一種更加輕量俯在,簡(jiǎn)單易用,無(wú)侵入娃惯,可配置化的字節(jié)碼操作方式跷乐。你不需要Java字節(jié)碼的相關(guān)知識(shí),只需要在xml插件配置中添加簡(jiǎn)單的Java代碼即可實(shí)現(xiàn)類似AOP的功能趾浅,同時(shí)不需要引入其他額外的依賴愕提。
DroidAssist的特點(diǎn)
- 靈活的配置化方式,使得一個(gè)配置就可以處理項(xiàng)目中所有的 class 文件皿哨。
- 豐富的字節(jié)碼處理功能浅侨,針對(duì) Android 移動(dòng)端的特點(diǎn)提供了例如代碼替換,添加try catch证膨,方法耗時(shí)等功能如输。
- 簡(jiǎn)單易用,只需要依賴一個(gè)插件央勒,處理過(guò)程以及處理后的代碼中也不需要添加額外的依賴不见。
- 處理速度較快,只占用較少的編譯時(shí)間崔步。
最后稳吮,卡頓優(yōu)化還會(huì)涉及到布局和繪制方面的優(yōu)化,慢慢把坑補(bǔ)上吧井濒。
本文參考:
Android開發(fā)高手課:06 | 卡頓優(yōu)化(下):如何監(jiān)控應(yīng)用卡頓灶似?
BlockCanary — 輕松找出Android App界面卡頓元兇
深入探索編譯插樁技術(shù)(二、AspectJ)
深入探索編譯插樁技術(shù)(四瑞你、ASM 探秘)
編譯插樁操縱字節(jié)碼酪惭,實(shí)現(xiàn)不可能完成的任務(wù)
DroidAssist
Android AspectJ詳解
AOP在Android中最佳用法
AspectJ AOP教程:實(shí)現(xiàn)Android基于注解無(wú)侵入埋點(diǎn)、性能監(jiān)控