簡介
因爲本人工做須要,須要解決一些性能問題,雖然有 Profiler
、Systrace
等工具,可是沒法實時監控,多少有些不方便,因而計劃寫一個能實時監控性能的小工具。通過學習大佬們的文章,最終完成了這個開源的性能實時檢測庫。初步能達到預期效果,這裏作個記錄,算是小結了。java
開源庫的地址是:android
幸苦各位能給個小小的 star 鼓勵下。github
這個性能檢測庫,能夠檢測如下問題:面試
- [x] UI 線程 block 檢測。
- [x] App 的 FPS 檢測。
- [x] 線程的建立和啓動監控以及線程池的建立監控。
- [x] IPC (進程間通信)監控。
同時還實現瞭如下功能:微信
- [x] 實時經過 logcat 打印檢測到的問題。
- [x] 保存檢測到的信息到文件。
- [x] 提供上報信息文件接口。
接入指南
1 在 APP
工程目錄下面的 build.gradle
添加以下內容。app
dependencies { // 必選 debugImplementation "com.xander.performance:perf:0.1.11" releaseImplementation "com.xander.performance:perf-noop:0.1.11" // hook 方案封裝,必須添加 debugImplementation "com.xander.performance:perf-hook:0.1.11" // 如下是 hook 方案選擇一個就行了。若是運行報錯,就換另一個,若是仍是報錯,就提個 issue // SandHook 方案,推薦添加。若是運行報錯,能夠替換爲 epic 庫。 debugImplementation "com.xander.performance:perf-hook-sandhook:0.1.11" // epic 方法。若是運行報錯,能夠替換爲 SandHook。 // debugImplementation "com.xander.performance:perf-hook-epic:0.1.11" }
2 APP
工程的 Application
類新增相似以下初始化代碼。框架
Java 初始化示例異步
private void initPERF(final Context context) { final PERF.LogFileUploader logFileUploader = new PERF.LogFileUploader() { @Override public boolean upload(File logFile) { return false; } }; PERF.init(new PERF.Builder() .checkUI(true, 100) // 檢查 ui lock .checkIPC(true) // 檢查 ipc 調用 .checkFps(true, 1000) // 檢查 fps .checkThread(true) // 檢查線程和線程池 .globalTag("test_perf") // 全局 logcat tag ,方便過濾 .cacheDirSupplier(new PERF.IssueSupplier<File>() { @Override public File get() { // issue 文件保存目錄 return context.getCacheDir(); } }) .maxCacheSizeSupplier(new PERF.IssueSupplier<Integer>() { @Override public Integer get() { // issue 文件最大佔用存儲空間 return 10 * 1024 * 1024; } }) .uploaderSupplier(new PERF.IssueSupplier<PERF.LogFileUploader>() { @Override public PERF.LogFileUploader get() { // issue 文件上傳接口 return logFileUploader; } }) .build()); }
kotlin 示例ide
private fun doUpload(log: File): Boolean { return false } private fun initPERF(context: Context) { PERF.init(PERF.Builder() .checkUI(true, 100)// 檢查 ui lock .checkIPC(true) // 檢查 ipc 調用 .checkFps(true, 1000) // 檢查 fps .checkThread(true)// 檢查線程和線程池 .globalTag("test_perf")// 全局 logcat tag ,方便過濾 .cacheDirSupplier { context.cacheDir } // issue 文件保存目錄 .maxCacheSizeSupplier { 10 * 1024 * 1024 } // issue 文件最大佔用存儲空間 .uploaderSupplier { // issue 文件的上傳接口實現 PERF.LogFileUploader { logFile -> doUpload(logFile) } } .build() ) }
主要更新記錄
- 0.1.11 優化 hook 方案的封裝,經過 SandHook ,IPC 的監控能夠按照耗時時間來檢測。
- 0.1.10 FPS 的檢測時間間隔從默認 2s 調整爲 1s,同時支持自定義時間間隔。
- 0.1.9 優化線程池建立的監控。
- 0.1.8 第一版發佈,完成基本的功能。
不建議直接在線上使用這個庫,在編寫這個庫,測試 hook 的時候,在不一樣的機器和 rom
上,會有不一樣的問題,這裏建議先只在線下自測使用這個檢測庫。
後期計劃
- [x] IPC 的監控目前只能監控調用,沒法檢測 IPC 調用耗時時間,後期計劃按照耗時時間來檢測。
- [x] Issue 保存到文件的邏輯優化,目前寫的自我感受不是很好,後面計劃優化下。
原理介紹
UI 線程 block 檢測原理
主要參考了 AndroidPerformanceMonitor
庫的思路,對 UI
線程的 Looper
裏面處理 Message
的過程進行監控。
具體作法是,在 Looper
開始處理 Message
前,在異步線程開啓一個延時任務,用於後續收集信息。若是這個 Message
在指定的時間段內完成了處理,那麼在這個 Message
被處理完後,就取消以前的延時任務,說明 UI
線程沒有 block 。若是在指定的時間段內沒有完成任務,說明 UI
線程有 block 。此時,異步線程能夠執行剛纔的延時任務。若是咱們在這個延時任務裏面打印 UI
線程的方法調用棧,就能夠知道 UI
線程在作什麼了。這個就是 UI
線程 block 檢測的基本原理。
可是這個方案有一個缺點,就是沒法處理 InputManager
的輸入事件,好比 TV
端的遙控按鍵事件。經過對按鍵事件的調用方法鏈進行分析,發現最終每一個按鍵事件都調用了 DecorView
類的 dispatchKeyEvent
方法,而非 Looper
的處理 Message
流程。因此 AndroidPerformanceMonitor
庫是沒法準確監控 TV 端應用 UI
block 的狀況。針對 TV
端應用按鍵處理,須要找到一個新的切入點,這個切入點就是剛剛的 DecorView
類的 dispatchKeyEvent
方法。
那如何介入 DecorView
類的 dispatchKeyEvent
方法呢?咱們能夠經過 epic
庫來 hook
這個方法的調用。hook
成功後,咱們能夠在 DecorView
類的 dispatchKeyEvent
方法調用先後都接收到一個回調方法,在 dispatchKeyEvent
方法調用前咱們能夠在異步線程執行一個延時任務,在 dispatchKeyEvent
方法調用後,取消這個延時任務。若是 dispatchKeyEvent
方法耗時時間小於指定的時間閾值,延時任務在執行前被取消,能夠認爲沒有 block ,此時移除了延時任務。若是 dispatchKeyEvent
方法耗時時間大於指定的時間閾值說明此時 UI
線程是有 block 的。此時,異步線程能夠執行這個延時任務來收集必要的信息。
以上就是修改後的 UI
線程 block 的檢測原理了,目前作的還比較粗糙,後續計劃考慮參考 AndroidPerformanceMonitor
打印 CPU 、內存等更多的信息。
最終終端 log 打印效果以下:
com.xander.performace.demo W/demo_Issue: ================================================= type: UI BLOCK msg: UI BLOCK create time: 2021-01-13 11:24:41 trace: java.lang.Thread.sleep(Thread.java:-2) java.lang.Thread.sleep(Thread.java:442) java.lang.Thread.sleep(Thread.java:358) com.xander.performance.demo.MainActivity.testANR(MainActivity.kt:49) java.lang.reflect.Method.invoke(Method.java:-2) androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397) android.view.View.performClick(View.java:7496) android.view.View.performClickInternal(View.java:7473) android.view.View.access$3600(View.java:831) android.view.View$PerformClick.run(View.java:28641) android.os.Handler.handleCallback(Handler.java:938) android.os.Handler.dispatchMessage(Handler.java:99) android.os.Looper.loop(Looper.java:236) android.app.ActivityThread.main(ActivityThread.java:7876) java.lang.reflect.Method.invoke(Method.java:-2) com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
FPS 檢測的原理
FPS 檢測的原理,利用了 Android 的屏幕繪製原理。這裏簡單說下 Android 的屏幕繪製原理。
系統每隔 16 ms 就會發送一個 VSync
信號。 若是應用註冊了這個 VSync
信號,就會在 VSync
信號到來的時候,收到回調,從而開始準備繪製。若是準備順利,也就是 CPU
準備數據、GPU
柵格化等,若是這些任務在 16 ms 以內完成,那麼下一個 VSync
信號到來前就能夠繪製這一幀界面了。就沒有掉幀,界面很流暢。若是在 16 ms 內沒準備好,可能就須要更多的時間這個畫面才能顯示出來,在這種狀況下就發生了丟幀,若是丟幀不少就卡頓了。
檢測 FPS 的原理其實挺簡單的,就是經過一段時間內,好比 1s,統計繪製了多少個畫面,就能夠計算出 FPS 了。那如何知道應用 1s 內繪製了多少個界面呢?這個就要靠 VSync
信號監聽了。
在開始準備繪製前,往 UI
線程的 MessageQueue
裏面放一個同步屏障,這樣 UI
線程就只會處理異步消息,直到同步屏障被移除。刷新前,應用會註冊一個 VSync
信號監聽,當 VSync
信號到達的時候,系統會通知應用,讓應用會給 UI
線程的 MessageQueue
裏面放一個異步 Message
。因爲以前 MessageQueue
裏有了一個同步屏障,因此後續 UI
線程會優先處理這個異步 Message
。這個異步 Message
作的事情就是從 ViewRootImpl
開始咱們熟悉的 measure
、layout
和 draw
。
咱們能夠經過 Choreographer
註冊 VSync
信號監聽。16ms 後,咱們收到了 VSync
的信號,給 MessageQueue
裏面放一個同步消息,咱們不作特別處理,只是作一個計數,而後監聽下一次的 VSync
信號,這樣,咱們就能夠知道 1s 內咱們監聽到了多少個 VSync
信號,就能夠得出幀率。
爲何監聽到的 VSync
信號數量就是幀率呢?
因爲 Looper
處理 Message
是串行的,就是一次只處理一個 Message
,處理完了這個 Message
纔會處理下一個 Message
。而繪製的時候,繪製任務 Message
是異步消息,會優先執行,繪製任務 Message
執行完成後,就會執行上面說的 VSync
信號計數的任務。若是忽略計數任務的耗時,那麼最後統計到的 VSync
信號數量能夠粗略認爲是某段時間內繪製的幀數。而後就能夠經過這段時間的長度和 VSync
信號數量來計算幀率了。
最終終端 log 打印效果以下:
com.xander.performace.demo W/demo_FPSTool: APP FPS is: 54 Hz com.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hz com.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hz
線程的建立和啓動監控以及線程池的建立監控
線程和線程池的監控,主要是監控線程和線程池在哪裏建立和執行的,若是咱們能夠知道這些信息,咱們就能夠比較清楚線程和線程池的建立和啓動時機是否合理。從而得出優化方案。
一個比較容易想到的方法就是,應用代碼裏面的全部線程和線程池繼承同一個線程基類和線程池基類。而後在構造函數和啓動函數裏面打印方法調用棧,這樣咱們就知道哪裏建立和執行了線程或者線程池。
讓應用全部的線程和線程池繼承同一個基類,能夠經過編譯插件來實現,定製一個特殊的 Transform
,經過 ASM
編輯生成的字節碼來改變繼承關係。可是,這個方法有必定的上手難度,不太適合新手。
除了這個方法,咱們還有另一種方法,就是 hook
。經過 hook
線程或者線程池的構造方法和啓動方法,咱們就能夠在線程或者線程池的構造方法和啓動方法的先後作一些切片處理,好比打印當前方法調用棧等。這個也就是線程和線程池監控的基本原理。
線程池的監控沒有太大難度,通常都是 ThreadPoolExecutor
的子類,因此咱們 hook
一下 ThreadPoolExecutor
的構造方法就能夠監控線程池的建立了。線程池的執行主要就是 hook
住 ThreadPoolExecutor
類的 execute
方法。
線程的建立和執行的監控方法就稍微要費些腦筋了,由於線程池裏面會建立線程,因此這個線程的建立和執行應該和線程池綁定的。須要找到線程和線程池的聯繫,以前看到一個庫,好像是經過線程和線程池的 ThreadGroup
來創建關聯的,原本我也計劃按照這個關係來寫代碼的,可是我發現,咱們有的小夥伴寫的線程池的 ThreadFactory
裏面建立線程並無傳入ThreadGroup
,這個就尷尬了,就創建不了聯繫了。通過查閱相關源碼發現了一個關鍵的類,ThreadPoolExecutor
的內部類Worker
,因爲這個類是內部類,因此這個類實際的構造方法裏面會傳入一個外部類的實例,也就是 ThreadPoolExecutor
實例。同時, Worker
這個類仍是一個 Runnable
實現,在 Worker
類經過 ThreadFactory
建立線程的時候,會把本身做爲一個 Runnable
傳給 Thread
因此,咱們經過這個關係,就能夠知道 Worker
和 Thread
的關聯了。這樣,咱們經過 ThreadPoolExecutor
和 Worker
的關聯,以及 Worker
和 Thread
的關聯,就能夠獲得 ThreadPoolExecutor
和它建立的 Thread
的關聯了。這個也就是線程和線程池的監控原理了。
最終終端 log 打印效果以下:
com.xander.performace.demo W/demo_Issue: ================================================= type: THREAD msg: THREAD POOL CREATE create time: 2021-01-13 11:23:47 create trace: com.xander.performance.StackTraceUtils.list(StackTraceUtils.java:39) com.xander.performance.ThreadTool$ThreadPoolExecutorConstructorHook.afterHookedMethod(ThreadTool.java:158) de.robv.android.xposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:265) me.weishu.epic.art.entry.Entry64.onHookObject(Entry64.java:64) me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:239) java.util.concurrent.Executors.newSingleThreadExecutor(Executors.java:179) com.xander.performance.demo.MainActivity.testThreadPool(MainActivity.kt:38) java.lang.reflect.Method.invoke(Method.java:-2) androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397) android.view.View.performClick(View.java:7496) android.view.View.performClickInternal(View.java:7473) android.view.View.access$3600(View.java:831) android.view.View$PerformClick.run(View.java:28641) android.os.Handler.handleCallback(Handler.java:938) android.os.Handler.dispatchMessage(Handler.java:99) android.os.Looper.loop(Looper.java:236) android.app.ActivityThread.main(ActivityThread.java:7876) java.lang.reflect.Method.invoke(Method.java:-2) com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
IPC(進程間通信)監控的原理
進程間通信的具體原理,也就是 Binder
機制,這裏不作詳細的說明,也不是這個框架庫的原理。
檢測進程間通信的方法和前面檢測線程的方法相似,就是找到全部的進程間通信的方法的共同點,而後對共同點作一些修改或者說切片,讓應用在進行進程間通信的時候,打印一下調用棧,而後繼續作原來的事情。就達到了 IPC 監控的目的。
那如何找到共同點,或者說切片,就是本節的重點。
進程間通信離不開 Binder
,須要從 Binder
入手。
寫一個 AIDL
demo 後發現,自動生成的代碼裏面,接口 A
繼承自 IInterface
接口,而後接口裏面有個內部抽象類 Stub
類,繼承自 Binder
,同時實現了接口 A
。這個 Stub
類裏面還有一個內部類 Proxy
,實現了接口 A
,並持有一個 IBinder
實例。
咱們在使用 AIDL
的時候,會用到 Stub
類的 asInterFace
的方法,這個方法會新建一個 Proxy
實例,並給這個 Proxy
實例傳入 IBinder
, 或者若是傳入的 IBinder
實例若是是接口 A
的話,就強制轉化爲接口 A 實例。通常而言,這個 IBinder
實例是 ServiceConnection
的回調方法裏面的實例,是 BinderProxy
的實例。因此 Stub
類的 asInterFace
通常會建立一個 Proxy
實例,查看這個 Proxy
接口的實現方法,發現最終都會調用 BinderProxy
的 transact
方法,因此 BinderProxy
的 transact
方法是一個很好的切入點。
原本我也是計劃經過 hook
住 BinderProxy
類的 transact
方法來作 IPC 的檢測的。可是 epic
庫在 hook
含有 Parcel
類型參數的方法的時候,不穩定,會有異常。因爲暫時還沒能力解決這個異常,只能從新找切入點。最後發現 AIDL
demo 生成的代碼裏面,除了調用了 調用 BinderProxy
的 transact
方法外,還調用了 Parcel
的 readException
方法,因而決定 hook
這個方法來切入 IPC
調用流程,從而達到 IPC
監控的目的。
最終終端 log 打印效果以下:
com.xander.performace.demo W/demo_Issue: ================================================= type: IPC msg: IPC create time: 2021-01-13 11:25:04 trace: com.xander.performance.StackTraceUtils.list(StackTraceUtils.java:39) com.xander.performance.IPCTool$ParcelReadExceptionHook.beforeHookedMethod(IPCTool.java:96) de.robv.android.xposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:229) me.weishu.epic.art.entry.Entry64.onHookVoid(Entry64.java:68) me.weishu.epic.art.entry.Entry64.referenceBridge(Entry64.java:220) me.weishu.epic.art.entry.Entry64.voidBridge(Entry64.java:82) android.app.IActivityManager$Stub$Proxy.getRunningAppProcesses(IActivityManager.java:7285) android.app.ActivityManager.getRunningAppProcesses(ActivityManager.java:3684) com.xander.performance.demo.MainActivity.testIPC(MainActivity.kt:55) java.lang.reflect.Method.invoke(Method.java:-2) androidx.appcompat.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:397) android.view.View.performClick(View.java:7496) android.view.View.performClickInternal(View.java:7473) android.view.View.access$3600(View.java:831) android.view.View$PerformClick.run(View.java:28641) android.os.Handler.handleCallback(Handler.java:938) android.os.Handler.dispatchMessage(Handler.java:99) android.os.Looper.loop(Looper.java:236) android.app.ActivityThread.main(ActivityThread.java:7876) java.lang.reflect.Method.invoke(Method.java:-2) com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
聯繫我
- 微信