[TOC]
# 線下
## 流暢度
gfxinfo、開發者模式的GPU渲染、perfdog、LayoutInspect都提供了卡頓監控的能力,不過都是肉眼觀察數據判斷是否發生卡頓。
## 慢函數
### TraceView
Traceview利用 Android Runtime 函數調用的 event 事件,將函數運行的耗時和調用關系寫入 trace 文件中。
由此可見,Traceview 屬于 instrument 類型,它可以用來查看整個過程有哪些函數調用,但是工具本身帶來的性能開銷過大,有時無法反映真實的情況。比如一個函數本身的耗時是 1 秒,開啟 Traceview 后可能會變成 5 秒,而且這些函數的耗時變化并不是成比例放大。
使用`Debug.startMethodTracing()`以及`Debug.stopMethodTracing()`可以在程序中動態開啟TraceView。
在 Android 5.0 之后,新增了`Debug.startMethodTracingSampling`方法,可以使用基于樣本的方式進行分析,以減少分析對運行時的性能影響。新增了 sample 類型后,就需要我們在開銷和信息豐富度之間做好權衡。
### systrace
[systrace](https://source.android.com/devices/tech/debug/systrace?hl=zh-cn)是 Android 4.1 新增的性能分析工具。我通常使用 systrace 跟蹤系統的 I/O 操作、CPU 負載、Surface 渲染、GC 等事件。
systrace 利用了 Linux 的[ftrace](https://source.android.com/devices/tech/debug/ftrace)調試工具,相當于在系統各個關鍵位置都添加了一些性能探針,也就是在代碼里加了一些性能監控的埋點。Android 在 ftrace 的基礎上封裝了[atrace](https://android.googlesource.com/platform/frameworks/native/+/master/cmds/atrace/atrace.cpp),并增加了更多特有的探針,例如 Graphics、Activity Manager、Dalvik VM、System Server 等。
systrace 工具只能監控特定系統調用的耗時情況,所以它是屬于 sample 類型,而且性能開銷非常低。但是它不支持應用程序代碼的耗時分析,所以在使用時有一些局限性。
由于系統預留了`Trace.beginSection`接口來監聽應用程序的調用耗時,那我們有沒有辦法在 systrace 上面自動增加應用程序的耗時分析呢?
劃重點了,我們可以通過**編譯時給每個函數插樁**的方式來實現,也就是在重要函數的入口和出口分別增加`Trace.beginSection`和`Trace.endSection`。當然出于性能的考慮,我們會過濾大部分指令數比較少的函數,這樣就實現了在 systrace 基礎上增加應用程序耗時的監控。通過這樣方式的好處有:
* 可以看到整個流程系統和應用程序的調用流程。包括系統關鍵線程的函數調用,例如渲染耗時、線程鎖,GC 耗時等。
* 性能損耗可以接受。由于過濾了大部分的短函數,而且沒有放大 I/O,所以整個運行耗時不到原來的兩倍,基本可以反映真實情況。
systrace 生成的也是 HTML 格式的結果,我們利用跟 Nanoscope 相似方式實現對反混淆的支持。

# 線上
## 卡頓監控
### 主進程-Handle
#### 替換 Looper 的 Printer
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);
? ? ? ?}
? ? ? ?...
? ?}
}
~~~
簡單實現
~~~
class LooperMonitor implements Printer {
@Override
public void println(String x) {
if (!mPrintingStarted) {
mStartTimestamp = System.currentTimeMillis();
mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
mPrintingStarted = true;
// 1:處理消息前
startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
if (isBlock(endTime)) {
// 2:處理消息后,如果超時了就獲取堆棧并輸出
notifyBlockEvent(endTime);
}
stopDump();
}
}
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
private void startDump() {
BlockCanaryInternals.getInstance().stackSampler.start();
}
private void stopDump() {
BlockCanaryInternals.getInstance().stackSampler.stop();
}
}
~~~
缺點
1. View的TouchEvent中的卡頓這種方案是無法監控的
2. IdleHandler的queueIdle()回調方法也是無法被監控的
3. SyncBarrier(同步屏障)的泄漏同樣無法被監控到
4. 需要使用idleHandler循環的檢測Looper.mLogging
5. 沒有開啟Looper的子線程,無法監控
優點
1. 真正有任務執行的時候才監控
#### 插入空消息到消息隊列
通過一個監控線程,每隔1秒向主線程消息隊列的頭部插入一條空消息。假設1秒后這個消息并沒有被主線程消費掉,說明阻塞消息運行的時間在0~1秒之間。換句話說,如果我們需要監控3秒卡頓,那在第4次輪詢中,頭部消息依然沒有被消費的話,就可以確定主線程出現了一次3秒以上的卡頓。

### Choreographer#doFrame 間隔檢測
簡單代碼實現如下:
~~~
Choreographer.getInstance().postFrameCallback(new?Choreographer.FrameCallback() {
? ?@Override?? ?
? ?public?voiddoFrame(long?frameTimeNanos) {
if(frameTimeNanos?\-?mLastFrameNanos?\>100) {
...
}
? ? ? ?mLastFrameNanos?\=?frameTimeNanos;
? ? ? ?Choreographer.getInstance().postFrameCallback(this);
}
});
~~~
## 慢函數
### 線程采樣
#### 高頻采樣
在事件進入時,開啟一個延時的定時任務,如果任務在規定時間內完成則取消掉,否則開始間隔52ms抓取堆棧對象,最多抓取3秒數據的堆棧。當事件執行結束時,如果總耗時超過卡頓閾值,則將抓取到的多個堆棧,進行合并,將合并后的堆棧樹進行上報,如下所示:

一個堆棧樹節點包含以下內容:
1. method:節點對應的方法,如 android.app.ActivityThread.performLaunchActivity
2. weight:節點方法的耗時權重,即該方法在整個消息執行過程中,堆棧數組中出現的個數
3. sliceIndex:節點方法所在的時間片集合,一個完整的堆棧為一個時間片(52ms)
4. children:節點方法下的子節點,即下一個執行的方法
### 3.1.2 堆棧樹設計
### 微信 ASM插樁
方案:
1. 為每個插樁的函數分配一個獨立 ID
2. 在方法前后插入了?MethodBeat.i/o 的方法
3. MethodBeat有個預先初始化好的數組 long\[\] 中 index 的位置(預先分配記錄數據的 buffer 長度為 100w,內存占用約 7.6M)。
4. 數組保存并當前執行的是 MethodBeat i或者o、mehtod id 及時間 offset
5. Choreographer 注冊監聽,在每一幀 doframe 回調時判斷距離上一幀的時間差是否超出閾值(卡頓),如果超出閾值,則獲取數組 index 前的所有數據(即兩幀之間的所有函數執行信息)進行分析上報。同時,我們在每一幀 doFrame 到來時,重置一個定時器,如果 5s 內沒有 cancel,則認為 ANR 發生,這時會主動取出當前記錄的 buffer 數據進行獨立分析上報,
優化點:
1. 掃描的函數是否只含有 PUT/READ FIELD 等簡單的指令,來過濾一些默認或匿名構造函數,以及 get/set 等簡單不耗時函數。
2. 時間不是實時獲取,而是每 5ms 去更新一個時間變量
缺點:
Matrix的堆棧信息并不包含非系統堆棧信息,無法進行進行一些系統場景,比如Binder耗時,鎖耗時等問題,當其無法監聽系統堆棧時,其顯示效果如下
### Facebook- Profilo
2018 年 3 月,Facebook 開源了一個叫[Profilo](https://github.com/facebookincubator/profilo)的庫,它收集了各大方案的優點,令我眼前一亮。具體來說有以下幾點:
**第一,集成 atrace 功能**。ftrace 所有性能埋點數據都會通過 trace\_marker 文件寫入內核緩沖區,Profilo 通過 PLT Hook 攔截了寫入操作,選擇部分關心的事件做分析。這樣所有 systrace 的探針我們都可以拿到,例如四大組件生命周期、鎖等待時間、類校驗、GC 時間等。
**不過大部分的 atrace 事件都比較籠統,從事件“B|pid|activityStart”,我們并不知道具體是哪個 Activity 的創建**。同樣我們可以統計 GC 相關事件的耗時,但是也不知道為什么發生了這次 GC。

**第二,快速獲取 Java 堆棧。很多同學有一個誤區,覺得在某個線程不斷地獲取主線程堆棧是不耗時的。但是事實上獲取堆棧的代價是巨大的,它要暫停主線程的運行。**
Profilo 的實現非常精妙,它實現類似 Native 崩潰捕捉的方式快速獲取 Java 堆棧,通過間隔發送 SIGPROF 信號,整個過程如下圖所示。

Signal Handler 捕獲到信號后,拿取到當前正在執行的 Thread,通過 Thread 對象可以獲取當前線程的 ManagedStack,ManagedStack 是一個單鏈表,它保存了當前的 ShadowFrame 或者 QuickFrame 棧指針,先依次遍歷 ManagedStack 鏈表,然后遍歷其內部的 ShadowFrame 或者 QuickFrame 還原一個可讀的調用棧,從而 unwind 出當前的 Java 堆棧。通過這種方式,可以實現線程一邊繼續跑步,我們還可以幫它做檢查,而且耗時基本忽略不計。代碼可以參照:[Profilo::unwind](https://github.com/facebookincubator/profilo/blob/master/cpp/profiler/unwindc/android_712/arm/unwinder.h)和[StackVisitor::WalkStack](http://androidxref.com/7.1.1_r6/xref/art/runtime/stack.cc#772)。
不用插樁、性能基本沒有影響、捕捉信息還全,那 Profilo 不就是完美的化身嗎?當然由于它利用了大量的黑科技,兼容性是需要注意的問題。它內部實現有大量函數的 Hook,unwind 也需要強依賴 Android Runtime 實現。Facebook 已經將 Profilo 投入到線上使用,但由于目前 Profilo 快速獲取堆棧功能依然不支持 Android 8.0 和 Android 9.0,鑒于穩定性問題,建議采取抽樣部分用戶的方式來開啟該功能。
### 西瓜
通過偏移+校驗的方式讀取到 Thread List,使用 SusopendThraedByPeer 函數傳入目標THread 對象的 jobkect 完成對目標線程的掛起。
獲取到 ThreadList 和函數指針后,在棧回溯的前后調用 Suspend 和 Resume 即可完成一次跨線程棧回溯。
在 Suspend 和 Resume 之間,僅執行 WalkStack 的操作,堆棧記錄和其他操作則放在 Resume 后執行,保證線程被掛起的時間足夠短,這樣可以確保性能最優。線程掛起機制經過實踐驗證可以滿足現有需求,實際運行中也未發現無法攻克的問題。另外線程掛起相較于信號機制,可以將大部分操作放到 Resume 后執行,而信號機制記錄堆棧等操作需要在回調內做,額外增加耗時,所以最后決定選用線程掛起方案。
# 參考資料
[抖音 Android 性能優化系列:新一代全能型性能分析工具 Rhea](https://toutiao.io/posts/p99i0mc/preview)
[西瓜視頻穩定性治理體系建設三:Sliver 原理及實踐](https://blog.csdn.net/ByteDanceTech/article/details/119621240)
[Matrix TraceCanary -- 初戀·卡頓](https://mp.weixin.qq.com/s/W4-1tfepKg2XMYvVn62B-Q)
[微信Android客戶端的卡頓監控方案](https://mp.weixin.qq.com/s/3dubi2GVW\_rVFZZztCpsKg)
[ 面試官又來了:你的app卡頓過嗎?](https://juejin.cn/post/6844903949560971277)
- Android
- 四大組件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介紹
- MessageQueue詳細
- 啟動流程
- 系統啟動流程
- 應用啟動流程
- Activity啟動流程
- View
- view繪制
- view事件傳遞
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大數據
- Binder小結
- Android組件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 遷移與修復
- Sqlite內核
- Sqlite優化v2
- sqlite索引
- sqlite之wal
- sqlite之鎖機制
- 網絡
- 基礎
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP進化圖
- HTTP小結
- 實踐
- 網絡優化
- Json
- ProtoBuffer
- 斷點續傳
- 性能
- 卡頓
- 卡頓監控
- ANR
- ANR監控
- 內存
- 內存問題與優化
- 圖片內存優化
- 線下內存監控
- 線上內存監控
- 啟動優化
- 死鎖監控
- 崩潰監控
- 包體積優化
- UI渲染優化
- UI常規優化
- I/O監控
- 電量監控
- 第三方框架
- 網絡框架
- Volley
- Okhttp
- 網絡框架n問
- OkHttp原理N問
- 設計模式
- EventBus
- Rxjava
- 圖片
- ImageWoker
- Gilde的優化
- APT
- 依賴注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 協程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 運行期Java-hook技術
- 編譯期hook
- ASM
- Transform增量編譯
- 運行期Native-hook技術
- 熱修復
- 插件化
- AAB
- Shadow
- 虛擬機
- 其他
- UI自動化
- JavaParser
- Android Line
- 編譯
- 疑難雜癥
- Android11滑動異常
- 方案
- 工業化
- 模塊化
- 隱私合規
- 動態化
- 項目管理
- 業務啟動優化
- 業務架構設計
- 性能優化case
- 性能優化-排查思路
- 性能優化-現有方案
- 登錄
- 搜索
- C++
- NDK入門
- 跨平臺
- H5
- Flutter
- Flutter 性能優化
- 數據跨平臺