首頁>技術>

由於工作需要,需要解決一些效能問題,雖然有 Profiler 、Systrace 等工具, 但是無法實時監控,於是計劃寫一個能實時監控效能的小工具,經過學習大佬們的文章, 最終完成了這個開源的效能實時檢測庫。初步能達到預期效果,這裡做個記錄,算是小結了。

這個效能檢測庫,可以檢測以下問題

UI 執行緒 block 檢測App 的 FPS 檢測執行緒和執行緒池的建立和啟動監控IPC(程序間通訊)監控

同時還實現了以下功能

實時透過 logcat 列印問題高效儲存檢測資訊到本地提供上報到指定伺服器介面接入指南

1 在 APP 工程目錄下面的 build.gradle 新增如下內容

dependencies {  debugImplementation "com.xander.performance:perf:0.1.9"  releaseImplementation "com.xander.performance:perf-noop:0.1.9"}

2 APP 工程的 Application 類新增類似如下初始化程式碼

    private void initPerformanceTool(Context context) {        PERF.Builder builder = new PERF.Builder().globalTag("p-tool") // 全域性 log 日誌 tag ,可以快速過濾日誌            .checkUI(true, 100) // 檢查 ui 執行緒, 超過指定時間還未結束,會被認為 ui 執行緒 block            .checkThread(true) // 檢查執行緒和執行緒池的建立            .checkFps(true) // 檢查 Fps            .checkIPC(true) // 檢查 IPC 呼叫            .issueSupplier(new PERF.IssueSupplier() {                @Override                public long maxCacheSize() {                    // issue 檔案快取的最大空間                    return 1024 * 1024 * 20;                 }                @Override                public File cacheRootDir() {                    // issue 檔案儲存的根目錄                     return getApplicationContext().getCacheDir();                 }                @Override                public boolean upLoad(File file) {                    // 上傳入口,返回 true 表示上傳成功                    return false;                }            }).build();        PERF.init(builder);    }
原理介紹UI 執行緒 block 檢測原理

主要參考了 AndroidPerformanceMonitor 庫的思路,對 UI 執行緒的 Looper 裡面處理的 Message 過程進行監控。 在 Looper 開始處理 Message 前,在非同步執行緒開啟一個延時任務,用於後續收集資訊。如果這個 Message 在指定的 時間段內完成了處理,那麼在這個 Message 被處理完後,就取消之前的延時任務,說明 UI 執行緒沒有 block 。如果在指定 的時間段內沒有完成任務,說明 UI 執行緒有 block ,在判斷髮生 block 的同時,我們可以在非同步執行緒執行剛才的延時任務, 如果我們在這個延時任務裡面列印 UI 執行緒的方法呼叫棧,就可以知道 UI 執行緒在做什麼了。

但是這個方案有一個缺點,就是無法處理 InputManager 的輸入事件,比如 TV 端的遙控按鍵事件。透過按鍵事件的呼叫方法 鏈進行分析,最終每個按鍵事件都呼叫了 DecorView 類的 dispatchKeyEvent 方法,而非 Looper 的 loop Message 流程。所以 AndroidPerformanceMonitor 庫是無法準確監控 TV 端應用的耗時情況。針對 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)複製程式碼
App 的 FPS 檢測的原理

FPS 檢測的原理,利用了 Android 的螢幕繪製原理。

這裡簡單說下 Android 的螢幕繪製原理。

系統每隔 16 ms 就會發送一個 VSync 訊號,如果 App 註冊了這個 VSync 訊號,就會在 VSync 訊號到來的時候,收到回撥, 從而開始準備繪製,如果準備順利,也就是 cpu 準備好資料, gpu 柵格化完成。如果這些任務在 16 ms 之內完成,那麼下一個 VSync 訊號到來的時候就可以繪製這一幀介面了。這個準備好的畫面就會被顯示出來。如果沒準備好,可能就需要 32 ms 後 或者更久的時間後,才能準備好,這個畫面才能顯示出來,這種情況下就發生了丟幀。

上面提到了 VSync 訊號,當 VSync 訊號到來的時候會通知應用開始準備繪製,具體的通知細節不做表述。大概的原理就是, 開始準備繪製前,往 MessageQueue 裡面放一個同步屏障,這樣 UI 執行緒就只會處理非同步訊息,直到同步屏障被移除, 然後 App 註冊一個 VSync 訊號監聽,當 VSync 訊號到達的時候,給 MessageQueue 裡面放一個非同步 Message 。 由於之前 MessageQueue 裡有了一個同步屏障訊息,所有後續 UI 執行緒會優先處理這個非同步 Message 。 這個非同步 Message 做的事情就是從 ViewRootImpl 開始了我們熟悉的 measure 、layout 和 draw 。

檢測 FPS 的原理其實挺簡單的,就是透過一段時間內,比如 1s,統計繪製了多少個畫面,就可以計算出 FPS 了。

那如何知道應用 1s 內繪製了多少個介面呢?這個就要靠 VSync 訊號監聽了。我們透過 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 Hzcom.xander.performace.demo W/demo_FPSTool: APP FPS is: 60 Hzcom.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)

19
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 53w字!阿里首推系統性能最佳化指南太香了,堪稱效能最佳化最優解