本文会介绍一个帮助我们快速调试 UI 参数的插件开发过程以及开发思路, 可能需要一些简单的 Idea 平台插件开发经验, 希望对大家会有一些帮助.
插件介绍
插件基于 Layout Inspector, 强化了这个工具, 故取名 Layout Master https://github.com/wuapnjie/LayoutMaster .
使用方式同 Layout Inspector, 呼出 Android Studio(3.1 以上)或 Idea(2017.3 以上)的 Action 面板, 输入 Layout Master 点击即可
插件效果如下(图中仅演示了部分属性修改效果, 支持很多属性)
项目 Github 地址: https://github.com/wuapnjie/LayoutMaster
为什么要做这个插件
我在平时的 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 http://facebook.github.io/stetho/ , 另外一个就是 Android Studio 自带的 Layout Inspector https://developer.android.google.cn/studio/debug/layout-inspector.html?hl=zh-cn . 这两个工具都与手机端建立了一个 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 项目 https://github.com/JetBrains/intellij-community 查看, 代码入口在 android 插件 https://github.com/JetBrains/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 中发现了一个 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 的插件中心下载使用, 喜欢这篇文章可以给个喜欢, 有什么问题可以评论或私信我.
插件审核需要时间, 可以直接在这里下载: https://github.com/wuapnjie/LayoutMaster/releases/tag/v1.0.0
插件项目 Github 地址: https://github.com/wuapnjie/LayoutMaster 欢迎 Star 和 PR
来源: https://juejin.im/post/5ad21a746fb9a028cf32e93a