如何打造穩定、好用的 Android LayoutInspector?
一、背景
Android 開發者在日常的開發中,經常需要用到查看視圖的功能,Android Studio 開發團隊為我們提供了 LayoutInspector 插件。在較新的版本提供了 LiveLayoutInspector,支持 3D,但是不管是 LayoutInspector 還是 LiveLayoutInspector 都非常難用。比如:
- 速度極慢,遇到復雜的布局經常超時
- 某些情況無法選中指定的 View
本文將圍繞 LayoutInspector 的痛點,分析問題并修復,最終將 LayoutInspector 變成一個穩定、好用的插件。
二、加速 Dump View Hierarchy
2.1 問題描述
開發復雜業務的同學在使用 LayoutInspector 時都遇到過上圖所示的錯誤:由于 View 樹結構復雜超時。網上也有其他相關的解決辦法,原理就是修改 timeout 的值,目前默認值是 20s,所以改成 1min,大概率是可以的了。
為了更好的解決這個問題,比如是否能加速?我們看一下整個 LayoutInspector 抓取的流程。梳理流程之前,我們需要找到功能的入口。
2.2 問題分析
2.2.1 Dump 總流程
平常開發者使用 LayoutInspector 的流程一般如下:
- 和 Attach debugger 類似,先獲取要 LayoutInspector 的進程
- 如果進程中不止一個 ViewRootImpl,還需要選擇 window
在 IDEA Plugin 框架體系中,大多數插件的功能入口都依賴 Action,上圖 LayoutInspector 的功能入口對應的 Action 如何找到呢?最快速、準確的辦法就是 Debug,在我們點擊功能入口之前,在 AnAction#actionPerformed 加上斷點。
從 AndroidRunLayoutInspectorAction 出發,我們找到了真正的任務:
- LayoutInspectorCaptureTask。
抓取 View 視圖的關鍵方法如下:
我們可以看到這里先構造了一個 Options,Opentions 中有個參數:ProtocolVersion,目前我們能使用的是 ProtocolVersion.Version1,Goolge 內可以通過 StudioFlags 打開 ProtocolVersion.Version2。
capture view 的流程會比較長,涉及到 adb 通信原理,我們先簡單了解一下 adb 通信架構。
- adb server: 運行在我們的 PC 開發機上,監聽 5037 端口
- adb daemon: 運行在 Android 設備上
- adb server 通過 USB/tcp 和 adbd 通信
了解了基本的 adb 通信基礎之后,我們再來看整個 captureview 的原理:
- 通過 ClientWindow 發起 loadWindowData 的請求(在這里可以看到默認超時時間是 20s)
- ClinetImpl 收到請求,讓 HandleViewDebug 將本次請求封裝成 JDWP,然后準備發送
- ClientImpl 將數據先發送給本 PC 上的 adb server
- adb server 將數據通過 usb/tcp 透傳給 Android 設備上的 adbd
- Android 設備上的 adbd 根據之前選擇的進程信息,將信息再透傳給指定的 jdwp 線程
- jdwp 通過 native 調用 DDMServer 方法
- DdmHandleViewDebug 收到請求開始處理
- 處理完請求后,再通過 socket 返回,LayoutInspector 收到結果解析后展示
參考:debugger.cc
- https://android.googlesource.com/platform/art/+/android-cts-5.0_r9/runtime/debugger.cc#3778
2.2.2 dump v1 原理
在上圖的流程中可以看到在最后的調用中,有 dump 和 dumpv2 兩個方法,而且 dump 方法已經廢棄了。

源碼 ViewDebug.java:
看源碼我們知道 v1 dump 是獲取被 @ExportedProperty 注解作用的 filed 和 method,然后將這些數據寫入 ByteArrayOutputStream。比如 View的 padding 屬性:
當然也有 method:
上面兩圖中的 category: padding 和 focus 體現在 LayoutInspector 的屬性面板中:
上面看源碼的結論:v1 是通過反射遍歷所有的 Filed 和 Method。
在我的手機 One Plus7 Android 10 上,View 的 filed 有 487 個,method 有 915 個。寫一段簡單的代碼展示一下僅遍歷耗時:
輸出:
- D/View#dump: 10705ms and 692 views
可以看到我們還沒有添加邏輯,僅僅遍歷耗時都達到了 10s。
2.2.3 dump v2 原理
- 看 ViewDebug#dumpv2:
調用到了 View#encode:
相比 v1,v2 就很克制了,只返回有限的數據,需要什么數據就獲取什么數據,但不支持自定義的屬性,相當于犧牲了一定的靈活性,加快了 dump 的速度。在靈活性、速度兩個方面,Google 將 v1 和 v2都保留了,并通過 StudioFlags 提供了開關。
2.3 解決方案
對比完 v1 和 v2 之后,基本可以確定 v2 的速度會快很多了。我們通過自定義 Action,并替換掉原生的 LayoutInspectorCaptureTask,關鍵是替換下面這個方法:
2.3 效果&收益
v2 相比 v1 速度快了非常多,下面貼一下抖音直播間的 Dump 數據,設備:One Plus 7 Android 10.
- LayoutInspector V1: 18803ms
- LayoutInspector V2: 328ms
本章節介紹了如何使用 v2 dump 協議來加速,下面介紹第二個痛點:某些情況無法選中指定的 View。
三、精確獲取點擊的 View
3.1 問題描述
LayoutInspector 還有一個不盡人意的地方——無法選中指定的 View。舉個例子:
上圖藍框其實是一個空白的沒有內容的 View,這個藍框蓋在了「收禮」這個紅圈上。在我們點擊這個紅圈的時候,卻是選中的藍框。
3.2 問題分析
我們首先分析一下 LayoutInspector 的 swing 組件組成:
LayoutInspector 中間圖片的預覽就是上圖中的 myPreview。為了解決這個問題,我們看一下這個點擊選中的邏輯。IDEA 自定義插件中使用的 GUI 框架是 Java Swing,組件的鼠標點擊、鼠標移入、鼠標退出等事件都可以通過 MouseAdapter 來監聽。ViewNodeActiveDisplay 的 MouseAdapter 如下:
查找指定的 View 邏輯:
代碼反映出,LayoutInspector 為了滿足點擊事件消費的順序,是從后往前遍歷的,Z 軸值較大的 View 優先消費事件。但是在很多情況,我們更需要通過比較 View 的面積大小,來選中指定的 View。
3.3 解決方案
其實代碼好修復,但是比較麻煩的是,如何替換 ViewNodeActiveDisplay 中getNode 和 updateSelection 相關邏輯呢,我注意到調用 getNode 的地方都是 click/mouseEnter 等事件,所以我們可以替換掉 MosueAdapter,然后重寫 getNode 和 updateSelection。
四、手把手教你搭建 IDEA Plugin 開發環境
修復上述兩個痛點需要新建一個 IDEA Plugin,和一般插件開發環境略有不同的是,我們需要依賴 android plugin。
然后在 build.gradle 中添加如下配置:
- // See https://github.com/JetBrains/gradle-intellij-plugin/
- intellij {
- localPath = "/Users/xx/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-1/202.7231092/Android Studio.app"
- plugins = ['android']
- updateSinceUntilBuild false
- }
localPath 填寫你本地的 Android Studio app 路徑。
前面我們提到 LayoutInspector 是 android 插件的一部分,所以這里我們聲明 plugins = ['android']
五、總結
本文圍繞原生 LayoutInspector 的兩個痛點,介紹了 LayoutInspector 的工作原理,并提出了解決方案,使得原生 LayoutInspector 穩定、好用。在文章最后也介紹了如何搭建插件工程,方便未接觸過插件的新人能進入插件的新世界。









































