本文會介紹一個幫助我們快速除錯UI引數的外掛開發過程以及開發思路,可能需要一些簡單的Idea平臺外掛開發經驗,希望對大家會有一些幫助。
外掛介紹
外掛基於Layout Inspector,強化了這個工具,故取名Layout Master。
使用方式同Layout Inspector,撥出Android Studio(3.1以上)或Idea(2017.3以上)的Action皮膚,輸入Layout Master點選即可,雙擊Property,支援修改的話會彈出Popup,同Layout Inspector一樣,每次Activity重啟了就需要再次執行Layout Master才可。
外掛效果如下(圖中僅演示了部分屬性修改效果,支援很多屬性)
專案Github地址:github.com/wuapnjie/La…
為什麼要做這個外掛
我在平時的Android開發過程中,會經常修改一些UI的引數,比如padding,margin,color等等,有時View是通過非XML程式碼動態注入的,很多引數設定在真機除錯時才能看到(而且我是那種一定要看真機跑效果的人),所以很多時候效果不滿意就要改引數繼續看效果,設計師們也會經常讓我們改一些UI上的引數,這樣每次都要重新編譯執行一次程式碼,或者Instant Run一下,專案小還好,專案一大,這個重新編譯執行的時間成本就會很大,大大降低了開發效率。於是我決定開發這個外掛,快速看到UI引數改變的效果。
外掛的簡單原理介紹
不同於React Native和Flutter這些框架實現的熱載入(哈哈,其實我也不知道這些框架怎麼實現的),這個外掛對View的引數實時設定都是通過Java反射呼叫View自身的setXXX()方法實現的,所以只能看效果,程式碼本質上沒有改變,需要達到滿意效果後再去修改,但這還是大大節省了時間,至少對我來說是。
那要怎麼樣做到從電腦端(IDE端)呼叫APP上View的setXXX()方法呢?很簡單,讓手機與電腦之間進行一個Socket長連線,定義一些命令協議,就可以實現電腦端對手機端的控制。
實現思路與過程
最初的思考
首先要實現想要的功能,第一步就是建立手機端與電腦端的Socket長連線並拿到關於Activity的View Hierarchy和View的Properties。這樣的功能我在兩個地方看到過,一個是Facebook強大的除錯框架Stetho,另外一個就是Android Studio自帶的Layout Inspector。這兩個工具都與手機端建立了一個Socket長連線,建立了自己的通訊協議。下面我會簡單介紹一下兩者的區別,並解釋了為什麼我選擇基於Layout Inspector做一個外掛,而不是基於Stetho做一個程式碼擴充套件。
Stetho
Stetho這個專案功能十分強大,不光可以看到View Hierarchy,還可以除錯資料庫,監測網路等等,實現上和我之前介紹的一樣,建立了一個Socket長連線,APP負責獲取需要的資料,通過Socket傳輸到Chrome DevTools,這裡Chrome DevTools有一個開發API,接收到特定的Json,會進行渲染顯示,在DevTools的操作也會Json格式包裝成特定的資料包傳送給APP進行操作。由於Stetho的程式碼比較複雜,我沒有對其深入研究,也不瞭解Chrome DevTools的API,但大致原理已經介紹了,如果你感興趣或有什麼想法,可以去研究研究。
Layout Inspector
同樣,Layout Inspector也是通過Socket長連線來獲取APP的相關UI資訊,由於Idea的社群版程式碼是開源的,而作為Android外掛的Layout Inspector程式碼也是開源,具體可以編譯Idea專案檢視,程式碼入口在android外掛的AndroidRunLayoutInspectorAction.java
類中。
兩者差別
Stetho的Socket連線相關程式碼是寫在它的庫中的,需要除錯的APP依賴這個專案,進行一些配置,侵入性較強,但功能強大。而Layout Inspector則對程式碼零侵入,那它是怎麼實現Socket長連線的呢?其實我們在除錯時,一直有一個長連線連線著電腦,那就是ADB,ADB工具在電腦端建立了一個Socket服務端,連線著所有開啟了USB除錯模式的手機客戶端,所以所有我們除錯的應用都可以使用Layout Inspector工具。
所以我選擇了基於Layout Inspector製作了一款外掛,程式碼零侵入,使用方便簡單,而且Android SDK中和Idea中已經幫我做好了很多程式碼工作,實現起來簡單,接下來我會介紹。
Layout Inspector分析
要基於Layout Inspector做,勢必要對這個工具的實現過程有了解,這裡我簡單分析一下它的原始碼,同時也會涉及到Android SDK中的一個類ViewDebug
。
Action入口
做過Idea外掛開發的同學肯定都知道Idea的Action系統,很多我們進行的快捷操作在Idea平臺中是一個個的Action
我們可以通過這個Action去快速找到它的入口類,上面也介紹了,在AndroidRunLayoutInspectorAction.java
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getProject();
assert project != null;
if (!AndroidSdkUtils.activateDdmsIfNecessary(project)) {
return;
}
AndroidProcessChooserDialog dialog = new AndroidProcessChooserDialog(project, false);
dialog.show();
if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
Client client = dialog.getClient();
if (client != null) {
new LayoutInspectorAction.GetClientWindowsTask(project, client).queue();
}
else {
Logger.getInstance(AndroidRunLayoutInspectorAction.class).warn("Not launching layout inspector - no client selected");
}
}
}
複製程式碼
從 入口程式碼中可以看出,我們要先選一個Process,也就是下面這個介面
Window選擇
之後會在Background執行LayoutInspectorAction.GetClientWindowsTask
,這個Task會獲取當前活躍的ClientWindow(也就是Android中的Window),如果超過一個的話,會出現對話方塊讓我們選擇,這裡就不貼圖了,反正大家都用過。
Capture View
選擇了Window之後就會在Background執行LayoutInspectorCaptureTask
,這個Task會獲取到需要顯示的View Hierarchy,View Properties以及一張BufferedImage(選擇Window的截圖),這些資訊全部以二進位制的資訊儲存在.li檔案中
顯示
然後Layout Inspector自定義了一個FileEditor以支援.li檔案的顯示,也就是我們能看到View Tree和Properties Table的主介面。具體顯示細節可參考LayoutInspectorContext
類
Android SDK中的響應
上面介紹了Layout Inspector在外掛端的簡單流程,它想Android端要了Window資訊,View的資訊,相關程式碼都在HandleViewDebug
類,下面是這個類的一些結構
也就是說服務端發出了一些命令的包,那作為客戶端的Android是在哪裡作出響應的呢?經過我的程式碼查詢,我在Android SDK中發現了一個DdmHandleViewDebug
類和ViewDebug
類
從兩個類的structure中就可以看出,Android端是在ViewDebug
這個類獲取各種資訊的,具體程式碼就不分析了,大家感興趣可以研究研究。
同時,這個類中有一個註解,叫ExportedProperty
/**
* This annotation can be used to mark fields and methods to be dumped by
* the view server. Only non-void methods with no arguments can be annotated
* by this annotation.
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportedProperty {
……
}
複製程式碼
檢視這個註解用的地方,可以發現,所有Layout Inspector中顯示的Properties都被標註了這個註解。
通過這個註解我們可以給一些自定義的View暴露出想要顯示的屬性。
擴充套件Layout Inspector
經過上面的對Layout Inspector的分析,我們已經足夠了解它並可以對其做擴充套件了。Layout Inspector只能檢視View Hierarchy和Properties,它完全可以做更多的事情。
在HandleViewDebug
類中有一個方法invokeMethod
,這個方法可以做到呼叫View的相關方法,目前只支援primitive arguments的方法,很可惜,意味著我們不能改變TextView的text。
觸發的方法在Android SDK的ViewDebug
的invokeViewMethod
方法中,可以看到是通過反射實現的,view post出去的
/**
* Invoke a particular method on given view.
* The given method is always invoked on the UI thread. The caller thread will stall until the
* method invocation is complete. Returns an object equal to the result of the method
* invocation, null if the method is declared to return void
* @throws Exception if the method invocation caused any exception
* @hide
*/
public static Object invokeViewMethod(final View view, final Method method,
final Object[] args) {
final CountDownLatch latch = new CountDownLatch(1);
final AtomicReference<Object> result = new AtomicReference<Object>();
final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
view.post(new Runnable() {
@Override
public void run() {
try {
result.set(method.invoke(view, args));
} catch (InvocationTargetException e) {
exception.set(e.getCause());
} catch (Exception e) {
exception.set(e);
}
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (exception.get() != null) {
throw new RuntimeException(exception.get());
}
return result.get();
}
複製程式碼
接下來就好辦了,核心方法Idea和Android SDK都為我們提供好了,我們只需要構建我們的外掛UI,寫入View的相關方法即可。
由於我們需要對View的Property進行操作,由於負責顯示View Properties的控制元件是私有的,所以這裡我通過反射獲取了例項,併為其新增了一個雙擊滑鼠事件。
private var propertyTable: PTable
init {
val editorReflect = Reflect.on(layoutInspectorEditor)
val context = editorReflect.get<LayoutInspectorContext>("myContext")
propertyTable = context.propertiesTable
...
}
...
fun hook() {
propertyTable.addMouseListener(object : MouseAdapter() {
...
}
}
複製程式碼
雙擊過後就是顯示一個Popup,不同的型別顯示不同的Popup。
不支援動畫的普通屬性
支援動畫的屬性
顏色屬性
Enum型別的屬性
Bitwise型別的屬性
自定義的屬性
可以支援自定義View的自定義的屬性無疑是最棒的,實現起來也很簡單,在介紹ViewDebug
類時,介紹了ExportedProperty
註解,我們只需要在自定義的View中運用這個註解就可以了,並設定好setXXX()方法,一個簡單例子,注意這個category一定要為custom,外掛才會做出響應,屬性名中帶有color會被認為是顏色。
public class ColorView extends TextView {
@ViewDebug.ExportedProperty(category = "custom", formatToHexString = true)
private int color = Color.BLACK;
@ViewDebug.ExportedProperty(category = "custom")
private int number = 0;
@ViewDebug.ExportedProperty(category = "custom")
private boolean needShowText = true;
public ColorView(Context context) {
super(context);
}
public ColorView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ColorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setColor(int color) {
this.color = color;
setBackgroundColor(color);
}
public void setNeedShowText(boolean needShowText) {
this.needShowText = needShowText;
if (!needShowText) {
setText("");
} else {
setText("" + number);
}
}
public void setNumber(int number) {
this.number = number;
setText("" + number);
}
}
複製程式碼
之後的細節就不具體展開了,畢竟核心原理已經介紹過了。外掛程式碼開源,感興趣的同學可以看看,不要噴我程式碼寫的差就行。
結語
如果大家喜歡這個外掛,可以在Android Studio或Idea的外掛中心下載使用,喜歡這篇文章可以給個喜歡,有什麼問題可以評論或私信我。
請給個好評,嘿嘿?
也可以直接在這裡下載:github.com/wuapnjie/La…
安裝時不要解壓那個zip包
外掛專案Github地址:github.com/wuapnjie/La… 歡迎Star和PR
希望這篇文章可以對你有什麼幫助,我也會繼續努力~