一、背景
在日常的Android开发中,开发者经常需要查看视图。为了解决这一问题,Android Studio开发团队提供了LayoutInspector插件。较新版本还提供了LiveLayoutInspector,支持3D视图,但是这两个插件都存在一些问题,如使用起来非常困难等。例如,速度极慢,遇到复杂的布局时经常超时;某些情况下无法选中指定的View。为了解决这些问题,本文将分析LayoutInspector的问题,并尝试修复它们,使LayoutInspector成为一个稳定且易用的插件。
二、加速Dump View Hierarchy
2.1 问题描述
在使用LayoutInspector时,复杂业务的同学经常会遇到以下错误:由于View树结构过于复杂而导致超时。网上也有其他相关的解决方法,其原理是修改timeout值。目前默认值为20秒,因此将其改为1分钟可能就足够了。
为了更好地解决这个问题,我们可以尝试加速Dump View Hierarchy的过程。首先,我们需要了解整个LayoutInspector抓取视图的流程。在开始之前,我们需要找到功能的入口。
2.2 问题分析
2.2.1 Dump总流程
通常情况下,开发者使用LayoutInspector的流程如下:
与Attachdebugger类似,首先需要获取要进行LayoutInspector的进程;如果该进程中存在多个ViewRootImpl对象,则还需要选择window;接下来,我们需要找到LayoutInspector的功能入口。在IDEA插件框架体系中,大多数插件的功能入口都依赖于Action。因此,最快速、准确的方法是在AnAction#actionPerformed方法之前加上断点。通过这种方式,我们可以从AndroidRunLayoutInspectorAction出发找到真正的任务:LayoutInspectorCaptureTask。
下面是抓取View视图的关键方法:
我们可以看到,首先在这里构造了一个 Options 对象,其中有一个参数:ProtocolVersion。目前我们可以使用的 ProtocolVersion 是 ProtocolVersion.Version1,而在 Google Studio 中,可以通过 StudioFlags 打开 ProtocolVersion.Version2。
接下来,我们来简要了解一下 capture view 的流程,涉及到 adb 通信原理。adb server 运行在我们的 PC 开发机上,监听 5037 端口;adb daemon 运行在 Android 设备上;adb server 通过 USB/tcp 和 adbd 进行通信。了解了基本的 adb 通信基础之后,我们再来看整个 captureview 的原理。
通过 ClientWindow 发起 loadWindowData 请求(在这里可以看到默认超时时间是 20s)。ClientImpl 收到请求后,让 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:
.1 v1 dump 原理
我们可以通过查看源码了解到,v1 dump 是获取被 @ExportedProperty 注解作用的 field 和 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,这个蓝框盖在了「收礼」这个红圈上。在我们点击这个红圈的时候,却是选中的蓝框。
.2 问题分析
LayoutInspector 的 Swing 组件组成如下:中间图片的预览为 myPreview。为了解决这个问题,我们看一下这个点击选中的逻辑。IDEA 自定义插件中使用的 GUI 框架是 Java Swing,组件的鼠标点击、鼠标移入、鼠标退出等事件都可以通过 MouseAdapter 来监听。ViewNodeActiveDisplay 的 MouseAdapter 如下所示:
查找指定的 View 逻辑:代码反映出,LayoutInspector 为了满足点击事件消费的顺序,是从后往前遍历的,Z 轴值较大的 View 优先消费事件。但是在很多情况下,我们更需要通过比较 View 的面积大小来选中指定的 View。
3.3 解决方案
实际上代码可以很容易地修复,但比较麻烦的是,如何替换 ViewNodeActiveDisplay 中的 getNode 和 updateSelection 相关逻辑。我注意到调用 getNode 的地方都是 click/mouseEnter 等事件,所以我们可以替换掉 MouseAdapter,然后重写 getNode 和 updateSelection。
四、手把手教你搭建 IDEA Plugin 开发环境
修复上述两个痛点需要新建一个 IDEA Plugin。与一般插件开发环境略有不同的是,我们需要依赖 android plugin。然后在 build.gradle 中添加如下配置:
```groovy
// 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 的两个痛点以及解决方案,旨在让原生 LayoutInspector 稳定且易用。同时,文章还提供了如何搭建插件工程的方法,方便那些初次接触插件的新人快速入门。
在文章一开始,我们提到 LayoutInspector 是 Android 插件的一部分,所以我们需要声明 plugins = ['android']。这样才能正确地使用 LayoutInspector 插件。
接下来,我们详细介绍了原生 LayoutInspector 面临的两个痛点,以及如何通过引入 LayoutInspector 插件来解决这些问题。首先,我们讨论了原生 LayoutInspector 在布局检查方面的性能问题。为了解决这个问题,我们提出了一种基于自定义 ViewTreeObserver 的方案,以提高布局检查的性能和稳定性。其次,我们还探讨了原生 LayoutInspector 在处理动态布局时的挑战。为了克服这个难点,我们引入了 LayoutInspector 插件,它能够自动识别并处理动态布局。
最后,在文章的结尾部分,我们简要介绍了如何搭建插件工程,以便那些未接触过插件的新人也能轻松进入插件的世界。通过搭建插件工程,你可以更方便地开发和调试原生 LayoutInspector 插件,从而实现对原生 LayoutInspector 的优化和改进。