如何打造穩定、好用的 Android LayoutInspector?

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Android 開發者在日常的開發中,經常需要用到查看視圖的功能,Android Studio 開發團隊爲我們提供了 LayoutInspector 插件。在較新的版本提供了 LiveLayoutInspector,支持 3D,但是不管是 LayoutInspector 還是 LiveLayoutInspector 都非常難用。比如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"速度極慢,遇到複雜的佈局經常超時"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"某些情況無法選中指定的 View"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文將圍繞 LayoutInspector 的痛點,分析問題並修復,最終將 LayoutInspector 變成一個穩定、好用的插件。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、加速 Dump View Hierarchy"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 問題描述"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/44\/446b9cbc1566b46be7d18c7a5a6a56c0.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"開發複雜業務的同學在使用 LayoutInspector 時都遇到過上圖所示的錯誤:由於 View 樹結構複雜超時。網上也有其他相關的解決辦法,原理就是修改 timeout 的值,目前默認值是 20s,所以改成 1min,大概率是可以的了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了更好的解決這個問題,比如是否能加速?我們看一下整個 LayoutInspector 抓取的流程。梳理流程之前,我們需要找到功能的入口。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 問題分析"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.1 Dump 總流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"平常開發者使用 LayoutInspector 的流程一般如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"和 Attach debugger 類似,先獲取要 LayoutInspector 的進程"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"如果進程中不止一個 ViewRootImpl,還需要選擇 window"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e3\/e3259ce442155111f0eeb678c546ad8c.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 IDEA Plugin 框架體系中,大多數插件的功能入口都依賴 Action,上圖 LayoutInspector 的功能入口對應的 Action 如何找到呢?最快速、準確的辦法就是 Debug,在我們點擊功能入口之前,在 AnAction#actionPerformed 加上斷點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/7c\/7cac4ba39e13c619471fba90bef9075c.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從 AndroidRunLayoutInspectorAction 出發,我們找到了真正的任務:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LayoutInspectorCaptureTask。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"抓取 View 視圖的關鍵方法如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/68\/688bec6fe97395b276759d3bb79cb1a9.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以看到這裏先構造了一個 Options,Opentions 中有個參數:ProtocolVersion,目前我們能使用的是 ProtocolVersion.Version1,Goolge 內可以通過 StudioFlags 打開 ProtocolVersion.Version2。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/4a\/4aeb7e2b74df0a630710d2f33e412f08.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"capture view 的流程會比較長,涉及到 adb 通信原理,我們先簡單瞭解一下 adb 通信架構。"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"adb server: 運行在我們的 PC 開發機上,監聽 5037 端口"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"adb daemon: 運行在 Android 設備上"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"adb server 通過 USB\/tcp 和 adbd 通信"}]}]}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/21\/21109d008b937964d427a5a10837b019.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"瞭解了基本的 adb 通信基礎之後,我們再來看整個 captureview 的原理:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"通過 ClientWindow 發起 loadWindowData 的請求(在這裏可以看到默認超時時間是 20s)"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"ClinetImpl 收到請求,讓 HandleViewDebug 將本次請求封裝成 JDWP,然後準備發送"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"ClientImpl 將數據先發送給本 PC 上的 adb server"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"adb server 將數據通過 usb\/tcp 透傳給 Android 設備上的 adbd"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"Android 設備上的 adbd 根據之前選擇的進程信息,將信息再透傳給指定的 jdwp 線程"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"jdwp 通過 native 調用 DDMServer 方法"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":7,"align":null,"origin":null},"content":[{"type":"text","text":"DdmHandleViewDebug 收到請求開始處理"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":8,"align":null,"origin":null},"content":[{"type":"text","text":"處理完請求後,再通過 socket 返回,LayoutInspector 收到結果解析後展示"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":9,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/1b\/1b60ac8d94c3bdc837edd563735e5dda.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/2c\/2c986cf3307a9dfa8bc03f5614f8628b.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"參考:debugger.cc"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"https:\/\/android.googlesource.com\/platform\/art\/+\/android-cts-5.0_r9\/runtime\/debugger.cc#3778"}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.2 dump v1 原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上圖的流程中可以看到在最後的調用中,有 dump 和 dumpv2 兩個方法,而且 dump 方法已經廢棄了。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/47\/47320ba41681c41997dd592dcb09facb.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"源碼 ViewDebug.java:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/3a\/3a7b91d6c95d4a6cf97f231db4a0a6e9.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看源碼我們知道 v1 dump 是獲取被 @ExportedProperty 註解作用的 filed 和 method,然後將這些數據寫入 ByteArrayOutputStream。比如 View的 padding 屬性:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/d7\/d770c827f644f8653885c0fb1ca9d9fc.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然也有 method:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/00\/005352f589c9566b719717043e411857.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面兩圖中的 category: padding 和 focus 體現在 LayoutInspector 的屬性面板中:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/81\/8181519a999b7c7a76725e0067cf1016.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面看源碼的結論:v1 是通過反射遍歷所有的 Filed 和 Method。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我的手機 One Plus7 Android 10 上,View 的 filed 有 487 個,method 有 915 個。寫一段簡單的代碼展示一下僅遍歷耗時:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/69\/690dbcc4004349eb2aa9520fa4f70ba5.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"輸出:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"D\/View#dump: 10705ms and 692 views\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到我們還沒有添加邏輯,僅僅遍歷耗時都達到了 10s。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2.3 dump v2 原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看 ViewDebug#dumpv2:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/78\/7808fec88f3bcf52f114e7b097e9c21e.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用到了 View#encode:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/97\/97f0049da926e912c2c4fcab998b44f8.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相比 v1,v2 就很剋制了,只返回有限的數據,需要什麼數據就獲取什麼數據,但不支持自定義的屬性,相當於犧牲了一定的靈活性,加快了 dump 的速度。在靈活性、速度兩個方面,Google 將 v1 和 v2都保留了,並通過 StudioFlags 提供了開關。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.3 解決方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對比完 v1 和 v2 之後,基本可以確定 v2 的速度會快很多了。我們通過自定義 Action,並替換掉原生的 LayoutInspectorCaptureTask,關鍵是替換下面這個方法:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ec\/ecc42d62b981fe2d70f377101a3756d1.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.4 效果&收益"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"v2 相比 v1 速度快了非常多,下面貼一下抖音直播間的 Dump 數據,設備:One Plus 7 Android 10."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LayoutInspector V1: 18803ms"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LayoutInspector V2: 328ms"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本章節介紹瞭如何使用 v2 dump 協議來加速,下面介紹第二個痛點:某些情況無法選中指定的 View。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"三、精確獲取點擊的 View"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 問題描述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LayoutInspector 還有一個不盡人意的地方——"},{"type":"text","marks":[{"type":"strong"}],"text":"無法選中指定的 View"},{"type":"text","text":"。舉個例子:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/c0\/c03811c99c4a56a8485303ed5ad3d310.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上圖藍框其實是一個空白的沒有內容的 View,這個藍框蓋在了「收禮」這個紅圈上。在我們點擊這個紅圈的時候,卻是選中的藍框。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2 問題分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們首先分析一下 LayoutInspector 的 swing 組件組成:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/bd\/bd66b030675fa7ebf8014d4d32552c22.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LayoutInspector 中間圖片的預覽就是上圖中的 myPreview。爲了解決這個問題,我們看一下這個點擊選中的邏輯。IDEA 自定義插件中使用的 GUI 框架是 Java Swing,組件的鼠標點擊、鼠標移入、鼠標退出等事件都可以通過 MouseAdapter 來監聽。ViewNodeActiveDisplay 的 MouseAdapter 如下:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a2\/a2ec197455460a7563da5ff027883f4c.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"查找指定的 View 邏輯:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/23\/23adc610bf127d46be7f30baf9041537.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼反映出,LayoutInspector 爲了滿足點擊事件消費的順序,是從後往前遍歷的,Z 軸值較大的 View 優先消費事件。但是在很多情況,我們更需要通過比較 View 的面積大小,來選中指定的 View。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.3 解決方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實代碼好修復,但是比較麻煩的是,如何替換 ViewNodeActiveDisplay 中getNode 和 updateSelection 相關邏輯呢,我注意到調用 getNode 的地方都是 click\/mouseEnter 等事件,所以我們可以替換掉 MosueAdapter,然後重寫 getNode 和 updateSelection。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/6c\/6c8dca2c9ff88c7b3a0e34a399bb3f8b.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/45\/45c2edef463e488894e013c540ed3df3.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、手把手教你搭建 IDEA Plugin 開發環境"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"修復上述兩個痛點需要新建一個 IDEA Plugin,和一般插件開發環境略有不同的是,我們需要依賴 android plugin。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/97\/97bf6e5d3908673b2810a79429975e1a.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後在 build.gradle 中添加如下配置:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\/\/ See https:\/\/github.com\/JetBrains\/gradle-intellij-plugin\/\nintellij {\n    localPath = \"\/Users\/xx\/Library\/Application Support\/JetBrains\/Toolbox\/apps\/AndroidStudio\/ch-1\/202.7231092\/Android Studio.app\"\n    plugins = ['android']\n    updateSinceUntilBuild false\n}\n"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"localPath 填寫你本地的 Android Studio app 路徑。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面我們提到 LayoutInspector 是 android 插件的一部分,所以這裏我們聲明 plugins = ['android']"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"五、總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文圍繞原生 LayoutInspector 的兩個痛點,介紹了 LayoutInspector 的工作原理,並提出瞭解決方案,使得原生 LayoutInspector 穩定、好用。在文章最後也介紹瞭如何搭建插件工程,方便未接觸過插件的新人能進入插件的新世界。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:字節跳動技術團隊(ID:toutiaotechblog)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/Fzh6ZhyothpOGWjNMm3ubg","title":"xxx","type":null},"content":[{"type":"text","text":"如何打造穩定、好用的 Android LayoutInspector?"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章