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

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