- 如何定義發(fā)生了卡頓現(xiàn)象:
如果App的FPS平均值小于30接癌,最小值小于24总寻,即表明應(yīng)用發(fā)生了卡頓。
- 線下很難復(fù)現(xiàn),與發(fā)生場景強(qiáng)相關(guān)(所以需要我們?nèi)プ隹D監(jiān)控,收集現(xiàn)場信息)
CPU相關(guān)知識
- 現(xiàn)在最新的主流機(jī)型都使用了多級能效的CPU架構(gòu)(即多核分層架構(gòu))
- 從 CPU 到 GPU 再到 AI 芯片NPU,隨著手機(jī) CPU 整體性能的飛躍, 我們可以充分利用移動端的計算能力來降低高昂的服務(wù)器成本箫柳;
- 評價一個 CPU 的性能,需要看主頻啥供、核心數(shù)悯恍、緩存等參數(shù),具體表現(xiàn)出來的是計算能力和指令執(zhí)行能力伙狐,也就是每秒執(zhí)行的浮點計算數(shù)和每秒執(zhí)行的指令數(shù)坪稽;
- 造成卡頓的原因很多(涉及到代碼、內(nèi)存鳞骤、繪制窒百、IO、CPU等)豫尽,最終都反映到 CPU 時間上篙梢,CPU時間 可以分為用戶時間和系統(tǒng)時間;
- 用戶時間:執(zhí)行用戶態(tài)應(yīng)用程序代碼所消耗的時間;
- 系統(tǒng)時間:執(zhí)行內(nèi)核態(tài)系統(tǒng)調(diào)用所消耗的時間美旧,包括 I/|O渤滞、鎖、中斷以及其他系統(tǒng)調(diào)用的時間;
- 常用命令:
adb shell
// 獲取 CPU 核心數(shù)
cat /sys/devices/system/cpu/possible
// 獲取第一個 CPU 的最大頻率
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq <
// 獲取第二個CPU的最小頻率
cat /sys/devices/system/cpu/cpu1/cpufreq/cpuinfo_min_freq <
//整個系統(tǒng)的 CPU 使用情況
cat /proc/[pid]/stat
top 命令可以幫助我們查看哪個進(jìn)程是 CPU 的消耗大戶榴嗅;
vmstat 命令可以實時動態(tài)監(jiān)視操作系統(tǒng)的虛擬內(nèi)存和 CPU 活動妄呕;
strace 命令可以跟蹤某個進(jìn)程中所有的系統(tǒng)調(diào)用
vmstat命令或者/proc/[pid]/schedstat文件來查看 CPU 上下文切換次數(shù)
/proc/[pid]/stat // 進(jìn)程CPU使用情況
/proc/[pid]/task/[tid]/stat // 進(jìn)程下面各個線程的CPU使用情況
/proc/[pid]/sched // 進(jìn)程CPU調(diào)度相關(guān)
/proc/loadavg // 系統(tǒng)平均負(fù)載,uptime命令對應(yīng)文件
- 否存在高優(yōu)先級的線程空等低優(yōu)先級線程嗽测,例如主線程等待某個后臺線程的鎖
- CPU相關(guān)的三類問題:
- CPU資源冗余使用:
- 算法效率低
- 沒使用緩存
- 計算時使用的基本類型不對(如int足夠卻用long绪励,運(yùn)算壓力多出4倍)
- CPU資源爭搶:
- 搶主線程的CPU資源
- 搶音視頻的CPU資源,
- 音視頻編解碼本身會消耗大量的CPU資源唠粥,并且其對于解碼的速度是有硬性要求的疏魏,如果達(dá)不到就可能產(chǎn)生播放流暢度的問題;
- 采取兩種方式去優(yōu)化:
- 盡量排除非核心業(yè)務(wù)的消耗。
- 優(yōu)化自身的性能消耗晤愧,把CPU負(fù)載轉(zhuǎn)化為GPU負(fù)載大莫,如使用renderscript來處理視頻中的影像信息。
- 大家平等官份,互相搶(三個和尚沒水喝)
- CPU資源利用率低:
- 有磁盤和網(wǎng)絡(luò)I/O只厘,還有鎖操作烙丛、sleep等等, 對于鎖的優(yōu)化,通常是盡可能地縮減鎖的范圍;
- CPU資源冗余使用:
卡頓排查工具
- Traceview 和 systrace 都是我們比較熟悉的排查卡頓的工具,從實現(xiàn)上這些工具分為兩個流派
- instrument: 獲取一段時間內(nèi)所有函數(shù)的調(diào)用過程羔味,可以通過分析這段時間內(nèi)的函數(shù)調(diào)用流程蜀变,再進(jìn)一步分析待優(yōu)化的點;
- sample: 有選擇性或者采用抽樣的方式觀察某些函數(shù)調(diào)用過程,可以通過這些有限的信息推測出流程中的可疑點介评,然后再繼續(xù)細(xì)化分析;
-
Traceview
- 類型:instrument;
- 原理:利用 Android Runtime 函數(shù)調(diào)用的 event 事件,將函數(shù)運(yùn)行的耗時和調(diào)用關(guān)系寫入 trace 文件中;
- 特點:
- 用來查看整個過程有哪些函數(shù)調(diào)用,但工具本身帶來的性能開銷過大,有時無法反映真實的情況爬舰;
- Android 5.0 之后们陆,新增了startMethodTracingSampling方法,可以使用基于樣本的方式進(jìn)行分析情屹,以減少分析對運(yùn)行時的性能影響坪仇。
新增了 sample 類型后,就需要我們在開銷和信息豐富度之間做好權(quán)衡垃你。 - 無論是哪種的 Traceview 對 release 包支持的都不太好椅文,例如無法反混淆
-
Nanoscope
- 類型:instrument
- 原理:直接修改 Android 虛擬機(jī)源碼,在ArtMethod執(zhí)行入口和執(zhí)行結(jié)束位置增加埋點代碼惜颇,將所有的信息先寫到內(nèi)存皆刺,等到 trace 結(jié)束后才統(tǒng)一生成結(jié)果文件;
- 特點:性能損耗較小,適合做啟動耗時的自動化分析凌摄,但是 trace 結(jié)束生成結(jié)果文件這一步需要的時間比較長羡蛾。另一方面它可以支持分析任意一個應(yīng)用,可用于做競品分析锨亏。但是它也有不少限制:
- 需要自己刷 ROM痴怨,并且當(dāng)前只支持 Nexus 6P,或者采用其提供的 x86 架構(gòu)的模擬器;
- 默認(rèn)只支持主線程采集器予,其他線程需要代碼手動設(shè)置浪藻。
考慮到內(nèi)存大小的限制,每個線程的內(nèi)存數(shù)組只能支持大約 20 秒左右的時間段乾翔。 - 我們可以每天定期去跑自動化啟動測試爱葵,查看是否存在新增的耗時點
-
systrace
- 類型:sample
- Android 4.1 新增的性能分析工具。我通常使用 systrace 跟蹤系統(tǒng)的 I/O 操作反浓、CPU 負(fù)載钧惧、Surface 渲染、GC 等事件勾习。
- 特點:只能監(jiān)控特定系統(tǒng)調(diào)用的耗時情況浓瞪,性能開銷低,但不支持應(yīng)用程序代碼的耗時分析巧婶;但是系統(tǒng)預(yù)留了Trace.beginSection接口來監(jiān)聽?wèi)?yīng)用程序的調(diào)用耗時乾颁,我們可以通過編譯時給每個函數(shù)插樁的方式來實現(xiàn)在 systrace 基礎(chǔ)上增加應(yīng)用程序耗時的監(jiān)控
-
Simpleperf
- 類型:sample
- 如果我們想分析 Native 函數(shù)的調(diào)用涂乌,上面的三個工具都不能滿足這個需求,Android 5.0 新增了Simpleperf性能分析工具
- 利用 CPU 的性能監(jiān)控單元(PMU)提供的硬件 perf 事件,可以看到所有的 Native 代碼的耗時,同時封裝了 systrace 的監(jiān)控功能
- Android Studio 3.2 也在 Profiler 中直接支持 Simpleperf
- 匯總一下
- 如果需要分析 Native 代碼的耗時,可以選擇 Simpleperf英岭;
- 如果想分析系統(tǒng)調(diào)用湾盒,可以選擇 systrace;
- 如果想分析整個程序執(zhí)行流程的耗時诅妹,可以選擇 Traceview 或者插樁版本的 systrace罚勾。
- 可視化方法
- Android Studio 3.2 的 Profiler 中直接集成了幾種性能分析工具:
- Sample Java Methods 的功能類似于 Traceview 的 sample 類型
- Trace Java Methods 的功能類似于 Traceview 的 instrument 類型
- Trace System Calls 的功能類似于 systrace
- SampleNative (API Level 26+) 的功能類似于 Simpleperf
- 雖然不夠全面和強(qiáng)大,但大大降低了開發(fā)者的使用門檻
- 分析結(jié)果的展示方式:這些分析工具都支持了 Call Chart 和 Flame Chart 兩種展示方式
- Call Chart 是 Traceview 和 systrace 默認(rèn)使用的展示方式吭狡,按照應(yīng)用程序的函數(shù)執(zhí)行順序來展示尖殃,適合用于分析整個流程的調(diào)用
- Flame Chart 也就是大名鼎鼎的火焰圖,以一個全局的視野來看待一段時間的調(diào)用分布划煮,時間和空間兩個維度上的信息融合在一張圖上
- StrictMode
- 是Android 2.3引入的一個工具類送丰,它被稱為嚴(yán)苛模式,是Android提供的一種運(yùn)行時檢測機(jī)制弛秋,可以用來幫助開發(fā)人員用來檢測代碼中一些不規(guī)范的問題器躏。
- 主要用來檢測兩大問題:
- 線程策略: 檢測內(nèi)容是一些自定義的耗時調(diào)用、磁盤讀取操作以及網(wǎng)絡(luò)請求等;
- 虛擬機(jī)策略: 檢測內(nèi)容包括Activity泄漏蟹略,SqLite對象泄漏登失,檢測實例數(shù)量;
- 使用:在Application的onCreate方法中對StrictMode進(jìn)行統(tǒng)一配置,在日志輸出欄中注意使用“StrictMode”關(guān)鍵字過濾出對應(yīng)的log即可
private void initStrictMode() { // 1、設(shè)置Debug標(biāo)志位挖炬,僅僅在線下環(huán)境才使用StrictMode if (BuildConfig.isDebug) { // 2壁畸、設(shè)置線程策略 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectCustomSlowCalls() //API等級11,使用StrictMode.noteSlowCode .detectDiskReads() .detectDiskWrites() .detectNetwork() // or .detectAll() for all detectable problems .penaltyLog() //在Logcat 中打印違規(guī)異常信息 //.penaltyDialog() //也可以直接跳出警報dialog //.penaltyDeath() //或者直接崩潰 .build()); // 3茅茂、設(shè)置虛擬機(jī)策略 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() // 給Person對象的實例數(shù)量限制為1 .setClassInstanceLimit(Person.class, 1) .detectLeakedClosableObjects() //API等級11 .penaltyLog() .build()); } }
卡頓監(jiān)控
- 消息隊列
- 方式1:通過替換 Looper 的 Printer 實現(xiàn);
- 首先捏萍,我們需要使用Looper.getMainLooper().setMessageLogging()去設(shè)置我們自己的Printer
實現(xiàn)類去打印輸出logging。這樣空闲,在每個message執(zhí)行的之前和之后都會調(diào)用我們設(shè)置的這個Printer實現(xiàn)類令杈。 - 如果我們匹配到">>>>> Dispatching to "之后,我們就可以執(zhí)行一行代碼:也就是在指定的時間閾值之后碴倾,
我們在子線程去執(zhí)行一個任務(wù)逗噩,這個任務(wù)就是去獲取當(dāng)前主線程的堆棧信息以及當(dāng)前的一些場景信息,比如:內(nèi)存大小跌榔、電腦异雁、網(wǎng)絡(luò)狀態(tài)等。 - 如果在指定的閾值之內(nèi)匹配到了"<<<<< Finished to "僧须,那么說明message就被執(zhí)行完成了纲刀,
則表明此時沒有產(chǎn)生我們認(rèn)為的卡頓效果,那我們就可以將這個子線程任務(wù)取消掉
- 首先捏萍,我們需要使用Looper.getMainLooper().setMessageLogging()去設(shè)置我們自己的Printer
- 方式2:通過一個監(jiān)控線程担平,每隔 1 秒向主線程消息隊列的頭部插入一條空消息; 如果我們需要監(jiān)控 3 秒卡頓示绊,那在第 4 次輪詢中頭部消息依然沒有被消費的話锭部,就可以確定主線程出現(xiàn)了一次 3 秒以上的卡頓;
- 方式1:通過替換 Looper 的 Printer 實現(xiàn);
- 插裝
- 基于消息隊列的卡頓監(jiān)控并不準(zhǔn)確,正在運(yùn)行的函數(shù)有可能并不是真正耗時的函數(shù)面褐;
假設(shè)一個消息循環(huán)里面順序執(zhí)行了 A拌禾、B、C 三個函數(shù)展哭,當(dāng)整個消息執(zhí)行超過 3 秒時湃窍,因為函數(shù) A 和 B 已經(jīng)執(zhí)行完畢, 我們只能得到的正在執(zhí)行的函數(shù) C 的堆棧匪傍,事實上它可能并不耗時,不過對于線上大數(shù)據(jù)來說您市,因為函數(shù) A 和 B 相對 比較耗時,所以抓取到它們的概率會更大一些析恢,通過后臺聚合后捕獲到函數(shù) A 和 B 的卡頓日志會更多一些; 如果跟 Traceview 一樣,可以拿到整個卡頓過程所有運(yùn)行函數(shù)的耗時秧饮,就可以明確知道其實函數(shù) A 和 B 才是造成卡頓的主要原因映挂; 那能否利用 Android Runtime 函數(shù)調(diào)用的回調(diào)事件,做一個自定義的 Traceview++ 呢盗尸?
- 需要使用 Inline Hook 技術(shù)柑船。我們可以實現(xiàn)類似 Nanoscope 先寫內(nèi)存的方案;需要注意兩點:
- 避免方法數(shù)暴增
- 過濾簡單的函數(shù)
- 實現(xiàn)參考:微信的Matrix
- 雖然插樁方案對性能的影響總體還可以接受泼各,但只會在灰度包使用鞍时;
- 短板:只能監(jiān)控應(yīng)用內(nèi)自身的函數(shù)耗時,無法監(jiān)控系統(tǒng)的函數(shù)調(diào)用扣蜻,整個堆椖嫖。看起來好像“缺失了”一部分
- Profilo
- 參考了JVM的 AsyncGetCallTrace 思路,然后適配 Android Runtime 的實現(xiàn)
- Facebook 開源庫莽使,它收集了各大方案的優(yōu)點
- 集成 atrace 功能
ftrace 所有性能埋點數(shù)據(jù)都會通過 trace_marker 文件寫入內(nèi)核緩沖區(qū)锐极,Profilo 通過 PLT Hook 攔截了寫入操作,選擇部分關(guān)心的事件做分析芳肌。這樣所有 systrace 的探針我們 都可以拿到灵再,例如四大組件生命周期、鎖等待時間亿笤、類校驗翎迁、GC 時間等。
- 快速獲取 Java 堆棧
- 獲取堆棧的代價是巨大的净薛,它要暫停主線程的運(yùn)行,Profilo 的實現(xiàn)非常精妙汪榔,它實現(xiàn)類似
Native 崩潰捕捉的方式快速獲取 Java 堆棧,通過間隔發(fā)送 SIGPROF 信號;
- 獲取堆棧的代價是巨大的净薛,它要暫停主線程的運(yùn)行,Profilo 的實現(xiàn)非常精妙汪榔,它實現(xiàn)類似
- 集成 atrace 功能
- AndroidPerformanceMonitor
- 一個非侵入式的性能監(jiān)控組件肃拜,可以通過通知的形式彈出卡頓信息揍异。
- 優(yōu)勢:非侵入式全陨,方便精準(zhǔn),能夠定位到代碼的某一行代碼衷掷。
- 使用:
//1. build.gradle下配置它的依賴 api 'com.github.markzhai:blockcanary-android:1.5.0' // 僅在debug包啟用BlockCanary進(jìn)行卡頓監(jiān)控和提示的話辱姨,可以這么用 debugApi 'com.github.markzhai:blockcanary-android:1.5.0' //2. Application的onCreate方法中開啟卡頓監(jiān)控 BlockCanary.install(this, new AppBlockCanaryContext()).start(); //3.繼承BlockCanaryContext類去實現(xiàn)自己的監(jiān)控配置上下文類 /** * @Author: LiuJinYang * @CreateDate: 2020/12/9 */ public class AppBlockCanaryContext extends BlockCanaryContext { // 實現(xiàn)各種上下文,包括應(yīng)用標(biāo)識符戚嗅,用戶uid雨涛,網(wǎng)絡(luò)類型,卡頓判斷闕值懦胞,Log保存位置等等 /** * 提供應(yīng)用的標(biāo)識符 * * @return 標(biāo)識符能夠在安裝的時候被指定替久,建議為 version + flavor. */ @Override public String provideQualifier() { return "unknown"; } /** * 提供用戶uid,以便在上報時能夠?qū)?yīng)的 * 用戶信息上報至服務(wù)器 * * @return user id */ @Override public String provideUid() { return "uid"; } /** * 提供當(dāng)前的網(wǎng)絡(luò)類型 * * @return {@link String} like 2G, 3G, 4G, wifi, etc. */ @Override public String provideNetworkType() { return "unknown"; } /** * 配置監(jiān)控的時間區(qū)間躏尉,超過這個時間區(qū)間 蚯根,BlockCanary將會停止, use * with {@code BlockCanary}'s isMonitorDurationEnd * * @return monitor last duration (in hour) */ @Override public int provideMonitorDuration() { return -1; } /** * 指定判定為卡頓的閾值threshold (in millis), * 你可以根據(jù)不同設(shè)備的性能去指定不同的閾值 * * @return threshold in mills */ @Override public int provideBlockThreshold() { return 1000; } /** * 設(shè)置線程堆棧dump的間隔, 當(dāng)阻塞發(fā)生的時候使用, BlockCanary 將會根據(jù) * 當(dāng)前的循環(huán)周期在主線程去dump堆棧信息 * <p> * 由于依賴于Looper的實現(xiàn)機(jī)制, 真實的dump周期 * 將會比設(shè)定的dump間隔要長(尤其是當(dāng)CPU很繁忙的時候). * </p> * * @return dump interval (in millis) */ @Override public int provideDumpInterval() { return provideBlockThreshold(); } /** * 保存log的路徑, 比如 "/blockcanary/", 如果權(quán)限允許的話, * 會保存在本地sd卡中 * * @return path of log files */ @Override public String providePath() { return "/blockcanary/"; } /** * 是否需要通知去通知用戶發(fā)生阻塞 * * @return true if need, else if not need. */ @Override public boolean displayNotification() { return true; } /** * 用于將多個文件壓縮為一個.zip文件 * * @param src files before compress * @param dest files compressed * @return true if compression is successful */ @Override public boolean zip(File[] src, File dest) { return false; } /** * 用于將已經(jīng)被壓縮好的.zip log文件上傳至 * APM后臺 * * @param zippedFile zipped file */ @Override public void upload(File zippedFile) { throw new UnsupportedOperationException(); } /** * 用于設(shè)定包名, 默認(rèn)使用進(jìn)程名胀糜, * * @return null if simply concern only package with process name. */ @Override public List<String> concernPackages() { return null; } /** * 使用 @{code concernPackages}方法指定過濾的堆棧信息 * * @return true if filter, false it not. */ @Override public boolean filterNonConcernStack() { return false; } /** * 指定一個白名單, 在白名單的條目將不會出現(xiàn)在展示阻塞信息的UI中 * * @return return null if you don't need white-list filter. */ @Override public List<String> provideWhiteList() { LinkedList<String> whiteList = new LinkedList<>(); whiteList.add("org.chromium"); return whiteList; } /** * 使用白名單的時候颅拦,是否去刪除堆棧在白名單中的文件 * * @return true if delete, false it not. */ @Override public boolean deleteFilesInWhiteList() { return true; } /** * 阻塞攔截器, 我們可以指定發(fā)生阻塞時應(yīng)該做的工作 */ @Override public void onBlock(Context context, BlockInfo blockInfo) { } }
其他監(jiān)控
- 除了主線程的耗時過長之外,我們還有哪些卡頓問題需要關(guān)注呢教藻?
- Android Vitals 是 Google Play 官方的性能監(jiān)控服務(wù)距帅,涉及卡頓相關(guān)的監(jiān)控有 ANR、啟動括堤、幀率三個
- 幀率
- 業(yè)界都使用 Choreographer 來監(jiān)控應(yīng)用的幀率碌秸;
- 需要排除掉頁面沒有操作的情況,應(yīng)該只在界面存在繪制的時候才做統(tǒng)計悄窃;
// 監(jiān)聽界面是否存在繪制行為 getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
- 平均幀率: 衡量界面流暢度讥电;
- 凍幀率:計算發(fā)生凍幀時間在所有時間的占比;
- 凍幀:Android Vitals 將連續(xù)丟幀超過 700 毫秒定義為凍幀轧抗,也就是連續(xù)丟幀 42 幀以上允趟;
- 出現(xiàn)丟幀的時候,我們可以獲取當(dāng)前的頁面信息鸦致、View 信息和操作路徑上報后臺潮剪,降低二次排查的難度
- 生命周期監(jiān)控
- Activity、Service分唾、Receiver 組件生命周期的耗時和調(diào)用次數(shù)也是我們重點關(guān)注的性能問題抗碰;
- 如:Activity 的 onCreate() 不應(yīng)該超過 1 秒,不然會影響用戶看到頁面的時間
- 對于組件生命周期應(yīng)采用更嚴(yán)格地監(jiān)控绽乔,全量上報弧蝇,在后臺查看各個組件各個生命周期的啟動時間和啟動次數(shù);
- 除了四大組件的生命周期,我們還需要監(jiān)控各個進(jìn)程生命周期的啟動次數(shù)和耗時看疗;
- 生命周期監(jiān)控推薦使用編譯時插樁的方式沙峻,如 Aspect、ASM 和 ReDex 三種插樁技術(shù)两芳;
- Activity、Service分唾、Receiver 組件生命周期的耗時和調(diào)用次數(shù)也是我們重點關(guān)注的性能問題抗碰;
- 線程監(jiān)控
- Java 線程管理是很多應(yīng)用非常頭痛的事情摔寨,應(yīng)用啟動過程就已經(jīng)創(chuàng)建了幾十上百個線程。而且大部分的線程都沒有經(jīng)過線程池管理怖辆,都在自由自在地狂奔著;
- 另外一方面某些線程優(yōu)先級或者活躍度比較高是复,占用了過多的 CPU。這會降低主線程 UI 響應(yīng)能力竖螃,我們需要特別針對這些線程做重點的優(yōu)化淑廊。
- 對于線程需要監(jiān)控兩點
- 線程數(shù)量,以及創(chuàng)建線程的方式:可以通過 got hook 線程的 nativeCreate() 函數(shù)特咆,主要用于進(jìn)行線程收斂季惩,也就是減少線程數(shù)量。
- 監(jiān)控線程的用戶時間 utime腻格、系統(tǒng)時間 stime 和優(yōu)先級
- 導(dǎo)致卡頓的原因會有很多画拾,比如函數(shù)非常耗時、I/O 非常慢荒叶、線程間的競爭或者鎖等碾阁。其實很多時候卡頓問題并不難解決输虱,相較解決來說些楣,更困難的是如何快速發(fā)現(xiàn)這些卡頓點,以及通過更多的輔助信息找到真正的卡頓原因宪睹。
卡頓現(xiàn)場
- 以AssetManager.openNonAsset函數(shù)耗時為例進(jìn)行分析
- 方案一: java實現(xiàn):
- 通過源碼可以發(fā)現(xiàn)愁茁,AssetManager 內(nèi)部有大量的 synchronized 鎖;
- 步驟1: 獲得 Java 線程狀態(tài):通過Thread.getState獲取 , 證實當(dāng)時主線程是 BLOCKED 狀態(tài);
//WAITING、TIME_WAITING 和 BLOCKED 都是需要特別注意的狀態(tài)亭病; //BLOCKED 是指線程正在等待獲取鎖鹅很,對應(yīng)的是下面代碼中的情況一; //WAITING 是指線程正在等待其他線程的“喚醒動作”罪帖,對應(yīng)的是代碼中的情況二促煮; synchronized (object) { // 情況一:在這里卡住 --> BLOCKED doSomething(); object.wait(); // 情況二:在這里卡住 --> WAITING } //不過當(dāng)一個線程進(jìn)入 WAITING 狀態(tài)時,它不僅會釋放 CPU 資源整袁,還會將持有的 object 鎖也同時釋放菠齿。
- 步驟2:獲得所有線程堆棧:通過Thread.getAllStackTraces()獲得
- 注意:在 Android 7.0,getAllStackTraces 是不會返回主線程的堆棧的
- 通過分析收集上來的卡頓日志坐昙,發(fā)現(xiàn)跟 AssetManager 相關(guān)的線程是BackgroundHandler
"BackgroundHandler" RUNNABLE at android.content.res.AssetManager.list at com.sample.business.init.listZipFiles //通過查看AssetManager.list的確發(fā)現(xiàn)是使用了同一個 synchronized 鎖绳匀,而 list 函數(shù)需要遍歷整個目錄,耗時會比較久 public String[] list(String path) throws IOException { synchronized (this) { ensureValidLocked(); return nativeList(mObject, path); } } //另外一方面,“BackgroundHandler”線程屬于低優(yōu)先級后臺線程疾棵,這也是我們前面文章提到的不良現(xiàn)象戈钢,也就是主線程等待低優(yōu)先級的后臺線程
- 方案2:ANR日志實現(xiàn)(SIGQUIT信號)
- 上面java實現(xiàn)方案還不錯,不過貌似ANR 日志的信息更加豐富是尔,如果直接用 ANR 日志呢殉了?
// 線程名稱; 優(yōu)先級; 線程id; 線程狀態(tài) "main" prio=5 tid=1 Suspended // 線程組; 線程suspend計數(shù); 線程debug suspend計數(shù); | group="main" sCount=1 dsCount=0 obj=0x74746000 self=0xf4827400 // 線程native id; 進(jìn)程優(yōu)先級; 調(diào)度者優(yōu)先級; | sysTid=28661 nice=-4 cgrp=default sched=0/0 handle=0xf72cbbec // native線程狀態(tài); 調(diào)度者狀態(tài); 用戶時間utime; 系統(tǒng)時間stime; 調(diào)度的CPU | state=D schedstat=( 3137222937 94427228 5819 ) utm=218 stm=95 core=2 HZ=100 // stack相關(guān)信息 | stack=0xff717000-0xff719000 stackSize=8MB
- Native 線程狀態(tài)
上面的 ANR 日志中“main”線程的狀態(tài)是 Suspended,Java 線程中的 6 種狀態(tài)中并不存在 Suspended 狀態(tài)啊? 事實上嗜历,Suspended 代表的是 Native 線程狀態(tài)宣渗。怎么理解呢?在 Android 里面 Java 線程的運(yùn)行都委托于一個 Linux 標(biāo)準(zhǔn)線程 pthread 來運(yùn)行梨州,而 Android 里運(yùn)行的線程可以分成兩種痕囱,一種是 Attach 到虛擬機(jī)的,一種是 沒有 Attach 到虛擬機(jī)的暴匠,在虛擬機(jī)管理的線程都是托管的線程鞍恢,所以本質(zhì)上 Java 線程的狀態(tài)其實是 Native 線程 的一種映射。不同的 Android 版本 Native 線程的狀態(tài)不太一樣每窖,例如 Android 9.0 就定義了 27 種線程狀態(tài)帮掉, 它能更加明確地區(qū)分線程當(dāng)前所處的情況。
- 如何拿到卡頓時的 ANR 日志?
- 第一步:當(dāng)監(jiān)控到主線程卡頓時窒典,主動向系統(tǒng)發(fā)送 SIGQUIT 信號蟆炊。
- 第二步:等待 /data/anr/traces.txt 文件生成。
- 第三步:文件生成以后進(jìn)行上報
- 通過 ANR 日志瀑志,我們可以直接看到主線程的鎖是由“BackgroundHandler”線程持有涩搓。相比之下通過 getAllStackTraces 方法,我們只能通過一個一個線程進(jìn)行猜測劈猪。
// 堆棧相關(guān)信息 at android.content.res.AssetManager.open(AssetManager.java:311) - waiting to lock <0x41ddc798> (android.content.res.AssetManager) held by tid=66 (BackgroundHandler) at android.content.res.AssetManager.open(AssetManager.java:289)
- 存在的問題:
- 可行性:很多高版本系統(tǒng)已經(jīng)沒有權(quán)限讀取 /data/anr/traces.txt 文件昧甘,需要刷ROM;
- 性能:獲取所有線程堆棧以及各種信息非常耗時战得,對于卡頓場景不一定合適充边,它可能會進(jìn)一步加劇用戶的卡頓;
- 方案3:Hook實現(xiàn)
- 通過 Hook 方式我們實現(xiàn)了一套“無損”獲取所有 Java 線程堆棧與詳細(xì)信息的方法:
- 通過 fork 子進(jìn)程方式實現(xiàn)常侦,這樣即使子進(jìn)程崩潰了也不會影響我們主進(jìn)程的運(yùn)行浇冰,而且獲取所有線程堆棧這個過程可以做到完全不卡我們主進(jìn)程;
- 通過libart.so聋亡、dlsym調(diào)用ThreadList::ForEach方法肘习,拿到所有的 Native 線程對象。
- 遍歷線程對象列表杀捻,調(diào)用Thread::DumpState方法井厌;
- 通過 Hook 方式我們實現(xiàn)了一套“無損”獲取所有 Java 線程堆棧與詳細(xì)信息的方法:
- 線上ANR監(jiān)控方式:
- ANR的幾種常見的類型:
- KeyDispatchTimeout:按鍵事件在5s的時間內(nèi)沒有處理完成;
- BroadcastTimeout:廣播接收器在前臺10s蚓庭,后臺60s的時間內(nèi)沒有響應(yīng)完成;
- ServiceTimeout:服務(wù)在前臺20s,后臺200s的時間內(nèi)沒有處理完成;
- 之前的崩潰優(yōu)化中說了“怎么去發(fā)現(xiàn)應(yīng)用中的 ANR 異辰銎停”器赞,那么,有沒有更好的實現(xiàn)方式呢墓拜?
- ANR-WatchDog:一種非侵入式的ANR監(jiān)控組件港柜,可以用于線上ANR的監(jiān)控
//1. build.gradle下配置它的依賴 implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0' //2. Application的onCreate方法中初始化ANR-WatchDog new ANRWatchDog().start(); //3.源碼:ANRWatchDog實際上是繼承了Thread類,也就是它是一個線程咳榜,對于線程來說夏醉,最重要的就是其run方法 private static final int DEFAULT_ANR_TIMEOUT = 5000; private volatile long _tick = 0; private volatile boolean _reported = false; private final Runnable _ticker = new Runnable() { @Override public void run() { _tick = 0; _reported = false; } }; @Override public void run() { // 1、首先涌韩,將線程命名為|ANR-WatchDog|畔柔。 setName("|ANR-WatchDog|"); // 2、接著臣樱,聲明了一個默認(rèn)的超時間隔時間靶擦,默認(rèn)的值為5000ms。 long interval = _timeoutInterval; // 3雇毫、然后玄捕,在while循環(huán)中通過_uiHandler去post一個_ticker Runnable。 while (!isInterrupted()) { // 3.1 這里的_tick默認(rèn)是0棚放,所以needPost即為true枚粘。 boolean needPost = _tick == 0; // 這里的_tick加上了默認(rèn)的5000ms _tick += interval; if (needPost) { _uiHandler.post(_ticker); } // 接下來,線程會sleep一段時間飘蚯,默認(rèn)值為5000ms馍迄。 try { Thread.sleep(interval); } catch (InterruptedException e) { _interruptionListener.onInterrupted(e); return ; } // 4、如果主線程沒有處理Runnable孝冒,即_tick的值沒有被賦值為0柬姚,則說明發(fā)生了ANR拟杉,第二個_reported標(biāo)志位是為了避免重復(fù)報道已經(jīng)處理過的ANR庄涡。 if (_tick != 0 && !_reported) { //noinspection ConstantConditions if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) { Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))"); _reported = true; continue ; } interval = _anrInterceptor.intercept(_tick); if (interval > 0) { continue; } final ANRError error; if (_namePrefix != null) { error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace); } else { // 5、如果沒有主動給ANR_Watchdog設(shè)置線程名搬设,則會默認(rèn)會使用ANRError的NewMainOnly方法去處理ANR穴店。 error = ANRError.NewMainOnly(_tick); } // 6、最后會通過ANRListener調(diào)用它的onAppNotResponding方法拿穴,其默認(rèn)的處理會直接拋出當(dāng)前的ANRError泣洞,導(dǎo)致程序崩潰。 _anrListener.onAppNotResponding(error); interval = _timeoutInterval; _reported = true; } } } //但是在Java層去獲取所有線程堆棧以及各種信息非常耗時默色,對于卡頓場景不一定合適球凰,它可能會進(jìn)一步加劇用戶的卡頓。 如果是對性能要求比較高的應(yīng)用,可以通過Hook Native層的方式去獲得所有線程的堆棧信息,參考上面“方案3:Hook實現(xiàn)”
- ANR的幾種常見的類型:
- 現(xiàn)場信息:
- 能不能進(jìn)一步讓卡頓的“現(xiàn)場信息”的比系統(tǒng) ANR 日志更加豐富呕诉?我們可以進(jìn)一步增加這些信息:
- CPU 使用率和調(diào)度信息:參考下面的課后作業(yè)1缘厢;
- 內(nèi)存相關(guān)信息:可以添加系統(tǒng)總內(nèi)存、可用內(nèi)存以及應(yīng)用各個進(jìn)程的內(nèi)存等信息甩挫。如果開啟了 Debug.startAllocCounting 或者 atrace贴硫,還可以增加 GC 相關(guān)的信息。
- I/O 和網(wǎng)絡(luò)相關(guān): 還可以把卡頓期間所有的 I/O 和網(wǎng)絡(luò)操作的詳細(xì)信息也一并收集
- 能不能進(jìn)一步讓卡頓的“現(xiàn)場信息”的比系統(tǒng) ANR 日志更加豐富呕诉?我們可以進(jìn)一步增加這些信息:
- Android 8.0 后伊者,Android 虛擬機(jī)終于支持了 JVM 的JVMTI機(jī)制。Profiler 中內(nèi)存采集等很多模塊也切換到這個機(jī)制中實現(xiàn),
卡頓單點問題檢測方案
- 常見的單點問題有主線程IPC(進(jìn)程間通信)雳攘、DB操作等等
- IPC單點問題檢測方案:
- 在IPC的前后加上埋點智末。但是,這種方式不夠優(yōu)雅
- 線下可以通過adb命令監(jiān)測
// 1法精、對IPC操作開始監(jiān)控 adb shell am trace-ipc start // 2税灌、結(jié)束IPC操作的監(jiān)控,同時亿虽,將監(jiān)控到的信息存放到指定的文件 adb shell am trace-ipc stop -dump-file /data/local/tmp/ipc-trace.txt // 3菱涤、將監(jiān)控到的ipc-trace導(dǎo)出到電腦查看 adb pull /data/local/tmp/ipc-trace.txt
- ARTHook
- AspectJ只能針對于那些非系統(tǒng)方法,也就是我們App自己的源碼洛勉,或者是我們所引用到的一些jar粘秆、aar包;
- ARTHook可以用來Hook系統(tǒng)的一些方法,因為對于系統(tǒng)代碼來說收毫,我們無法對它進(jìn)行更改攻走,但是我們可以Hook住它的一個方法,在它的方法體里面去加上自己的一些代碼此再;
//通過PackageManager去拿到我們應(yīng)用的一些信息昔搂,或者去拿到設(shè)備的DeviceId這樣的信息以及AMS相關(guān)的信息等,最終會調(diào)用到android.os.BinderProxy //在項目中的Application的onCreate方法中使用ARTHook對android.os.BinderProxy類的transact方法進(jìn)行Hook try { DexposedBridge.findAndHookMethod(Class.forName("android.os.BinderProxy"), "transact", int.class, Parcel.class, Parcel.class, int.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { LogHelper.i( "BinderProxy beforeHookedMethod " + param.thisObject.getClass().getSimpleName() + "\n" + Log.getStackTraceString(new Throwable())); super.beforeHookedMethod(param); } }); } catch (ClassNotFoundException e) { e.printStackTrace(); }
- 除了IPC調(diào)用的問題之外,還有IO输拇、DB摘符、View繪制等一系列單點問題需要去建立與之對應(yīng)的檢測方案
- 對于卡頓問題檢測方案的建設(shè),主要是利用ARTHook去完善線下的檢測工具策吠,盡可能地去Hook相對應(yīng)的操作逛裤,以暴露、分析問題猴抹。
使用Lancet統(tǒng)計界面耗時
- Lancet 是一個輕量級Android AOP框架带族,編譯速度快, 并且支持增量編譯.
- 使用Demo如下
//1. 在根目錄的 build.gradle 添加:
dependencies {
classpath 'me.ele:lancet-plugin:1.0.6'
}
//2. 在 app 目錄的'build.gradle' 添加
apply plugin: 'me.ele.lancet'
dependencies {
provided 'me.ele:lancet-base:1.0.6'
}
//3. 基礎(chǔ)API使用
/**
* @Author: LiuJinYang
* @CreateDate: 2020/12/10
*/
public class LancetUtil {
//@Proxy 指定了將要被織入代碼目標(biāo)方法i, 織入方式為Proxy(將使用新的方法替換代碼里存在的原有的目標(biāo)方法)
@Proxy("i")
//TargetClass指定了將要被織入代碼目標(biāo)類 android.util.Log
@TargetClass("android.util.Log")
public static int anyName(String tag, String msg){
msg = "LJY_LOG: "+msg ;
//Origin.call() 代表了 Log.i() 這個目標(biāo)方法
return (int) Origin.call();
}
}
//4. 統(tǒng)計界面耗時
/**
* @Author: LiuJinYang
* @CreateDate: 2020/12/10
*/
public class LancetUtil {
public static ActivityRecord sActivityRecord;
static {
sActivityRecord = new ActivityRecord();
}
@Insert(value = "onCreate",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
protected void onCreate(Bundle savedInstanceState) {
sActivityRecord.mOnCreateTime = System.currentTimeMillis();
// 調(diào)用當(dāng)前Hook類方法中原先的邏輯
Origin.callVoid();
}
@Insert(value = "onWindowFocusChanged",mayCreateSuper = true)
@TargetClass(value = "android.support.v7.app.AppCompatActivity",scope = Scope.ALL)
public void onWindowFocusChanged(boolean hasFocus) {
sActivityRecord.mOnWindowsFocusChangedTime = System.currentTimeMillis();
LjyLogUtil.i(getClass().getCanonicalName() + " onWindowFocusChanged cost "+(sActivityRecord.mOnWindowsFocusChangedTime - sActivityRecord.mOnCreateTime));
Origin.callVoid();
}
public static class ActivityRecord {
/**
* 避免沒有僅執(zhí)行onResume就去統(tǒng)計界面打開速度的情況,如息屏蟀给、亮屏等等
*/
public boolean isNewCreate;
public long mOnCreateTime;
public long mOnWindowsFocusChangedTime;
}
}
卡頓分析
- 在客戶端捕獲卡頓之后蝙砌,最后數(shù)據(jù)需要上傳到后臺統(tǒng)一分析
- 卡頓率
- 評估卡頓的影響面:UV 卡頓率 = 發(fā)生過卡頓 UV / 開啟卡頓采集 UV阳堕,一個用戶如果命中采集,那么在一天內(nèi)都會持續(xù)的采集數(shù)據(jù)
- 評估卡頓的嚴(yán)重度:PV 卡頓率 = 發(fā)生過卡頓 PV / 啟動采集 PV择克,對于命中采集 PV 卡頓率的用戶嘱丢,每次啟動都需要上報作為分母
課后作業(yè)
- 模仿ProcessCpuTracker.java拿到一段時間內(nèi)各個線程的耗時占比
- 使用 PLTHook 技術(shù)來獲取 Atrace 的日志
- 使用 PLTHook 技術(shù)來獲取線程創(chuàng)建的堆棧
參考
- Android開發(fā)高手課-卡頓優(yōu)化(上):你要掌握的卡頓分析方法
- Linux環(huán)境下進(jìn)程的CPU占用率
- Traceview
- Uber開源的 Nanoscope
- systrace
- atrace
- Simpleperf
- Android開發(fā)高手課-卡頓優(yōu)化(下):如何監(jiān)控應(yīng)用卡頓?
- Matrix
- Profilo
- ftrace 簡介
- 使用 ftrace 調(diào)試 Linux 內(nèi)核(上)
- 使用 ftrace 調(diào)試 Linux 內(nèi)核(下)
- atrace 介紹
- atrace 實現(xiàn)
- Android Choreographer 源碼分析
- Android開發(fā)高手課-卡頓優(yōu)化(補(bǔ)充):卡頓現(xiàn)場與卡頓分析
- 啃碎并發(fā)(四):Java線程Dump分析
- 手Q Android線程死鎖監(jiān)控與自動化分析實踐
- JVMTI 和 Agent 實現(xiàn)
- Android線程的創(chuàng)建過程
- Android Performance Monitor(BlockCanary)
- BlockCanary — 輕松找出Android App界面卡頓元兇
- ANR-WatchDog