JDWP远程方法调用流程详解
在 Android 中使用 JDWP 实现 View 层数据 dump 的原理,可以归纳为:
调试器通过 JDWP 协议与目标进程建立通信,定位当前 Activity 的根视图(DecorView),再远程调用 ViewDebug.dump() 方法将视图结构与属性信息导出。
下面我们重点从“流程”角度,按阶段逐步拆解说明整个执行过程:
总体流程概览
目标:从外部抓取 Android 应用当前页面的 View 层级结构及属性信息。
核心动作:调试器借助 JDWP 协议远程调用 ViewDebug.dump(),抓取视图信息。
分阶段详细流程
阶段一:连接目标进程
1.1 查看可调试进程
adb jdwp
输出为 JDWP 支持的进程 PID 列表。
1.2 建立端口转发
adb forward tcp:8700 jdwp:<pid>
此操作将本地端口(如 8700)与目标进程 JDWP 服务建立 Socket 通道。
⚠️ 此时,目标进程的 JDWP Agent 正在监听一个 Socket,调试器通过该通道传输指令。
阶段二:定位目标类和方法
调试器通过 JDWP 协议命令获取 ViewDebug 的信息:
2.1 获取 ViewDebug 类引用(ClassID)
- 发送 VirtualMachine.ClassesByName 命令,传入类名 android.view.ViewDebug;
- 返回包含 ViewDebug 的 ClassID,后续操作都通过这个 ID 进行。
2.2 获取
dump()
方法 ID
- 使用 ReferenceType.Methods 命令,列出该类的所有方法;
- 匹配方法签名 dump(Landroid/view/View;Ljava/io/OutputStream;)V;
- 提取该方法的 MethodID。
阶段三:构造参数对象
ViewDebug.dump() 需要两个参数:
- 第一个是 View 对象:要 dump 的根视图,一般是 DecorView;
- 第二个是 OutputStream 对象:输出数据的目标流,用于数据回传。
3.1 获取当前 Activity 的 DecorView(root view)
这个过程可通过多种方式实现,常见方法:
- 获取 ActivityThread 的 mActivities 列表;
- 遍历找到 Activity 对象,再通过 getWindow().getDecorView() 获取根 View;
- 使用 JDWP 的 Field/Get, ObjectReference/InvokeMethod 等指令逐层获取。
3.2 构造 OutputStream,用于接收 dump 内容
常见方式:
- 在目标进程中创建一个 java.io.PipedOutputStream;
- 它连接一个 PipedInputStream,另一端由调试器读取;
- 使用 ObjectReference.NewInstance 构造对象,再通过 InvokeMethod 初始化连接。
阶段四:远程调用 ViewDebug.dump()
有了 ClassID、MethodID、参数对象引用后:
4.1 调用 ClassType.InvokeMethod
通过 JDWP 发送如下指令:
ClassType.InvokeMethod
- ClassID = ViewDebug
- MethodID = dump
- Arguments = [DecorViewRef, OutputStreamRef]
- Options = SINGLE_THREADED | INVOKE_NONVIRTUAL
系统在目标进程中启动一个线程,执行 ViewDebug.dump(),递归遍历 View 树结构、属性值,并写入 OutputStream。
阶段五:读取 dump 数据并处理
5.1 数据输出回传
目标进程中的 ViewDebug.dump() 方法会将信息写入 OutputStream:
包括 View 的类名、ID、坐标、宽高、visibility、@ExportedProperty 注解值等;
格式类似:
com.android.internal.policy.DecorView@ab12c3d4
+– FrameLayout@12de45f6
+– LinearLayout@34fa789b
+– TextView@56bc789a (text=”Hello World”)
5.2 调试器读取并解析数据
- 调试器通过与 PipedInputStream 配对的端口读取数据;
- 将原始 dump 文本解析为可视化结构,展示在 Layout Inspector 或其他工具中。
总结:完整流程图(文字版)
调试器 (Android Studio / Layout Inspector)
│
├─ ① adb jdwp → 获取可调试进程
├─ ② adb forward tcp:8700 jdwp:<pid> → 建立 JDWP 通道(Socket)
│
├─ ③ JDWP: ClassesByName → 获取 ViewDebug 的 ClassID
├─ ④ JDWP: ReferenceType.Methods → 获取 dump 方法的 MethodID
│
├─ ⑤ JDWP: 获取 DecorView 对象引用
├─ ⑥ JDWP: 构造并连接 OutputStream
│
├─ ⑦ JDWP: ClassType.InvokeMethod → 远程执行 ViewDebug.dump(view, stream)
│
└─ ⑧ 读取 OutputStream → 分析/渲染布局数据
补充说明
- ViewDebug.dump() 只会导出当前主线程视图(UI 线程中的 View 层级);
- 调试时目标进程必须是 debuggable,否则 JDWP 会被禁用;
- 若目标 View 层级中某些字段未标注 @ExportedProperty,则不会被导出;
- 一些工具如 LayoutInspector 支持 dump memory snapshot、截图、属性值编辑,实际调用远不止一个方法。
伪代码
基于 JDWP 协议实现 ViewDebug.dump 的伪代码流程,展示了调试器如何一步步与目标 Android App 通信、定位并调用 ViewDebug.dump() 方法并回收数据。
JDWP View dump 调用伪代码
// 假设当前你是调试器端(如自己实现一个 JDWP 客户端)
// ① 通过 ADB 建立 JDWP 通道(实际在命令行中执行)
adb jdwp // 获取目标进程 PID
adb forward tcp:8700 jdwp:<pid>
// ② 与目标 JDWP 服务建立 Socket 连接
JDWPConnection conn = JDWP.connect("localhost", 8700);
// ③ 查询类 android.view.ViewDebug
ClassID viewDebugClassId = conn.send(
JDWPCommand.VirtualMachine.ClassesByName("android.view.ViewDebug")
).getFirstClassId();
// ④ 查询方法 dump(View, OutputStream)
MethodID dumpMethodId = conn.send(
JDWPCommand.ReferenceType.Methods(viewDebugClassId)
).findMethod("dump", "(Landroid/view/View;Ljava/io/OutputStream;)V");
// ⑤ 获取当前 Activity 的 DecorView(通过调用 getWindow().getDecorView())
ObjectID activityThread = conn.findStaticField("android.app.ActivityThread", "sCurrentActivityThread");
ObjectID activityClientRecord = conn.invokeMethod(activityThread, "getActivities", ...).getFirst();
ObjectID activity = conn.getField(activityClientRecord, "activity");
ObjectID window = conn.invokeMethod(activity, "getWindow", "()");
ObjectID decorView = conn.invokeMethod(window, "getDecorView", "()");
// ⑥ 创建 OutputStream(使用 java.io.PipedOutputStream 或自定义 wrapper)
ObjectID outputStream = conn.newObject("java.io.ByteArrayOutputStream");
// ⑦ 执行 ViewDebug.dump(rootView, outputStream)
conn.invokeMethod(
viewDebugClassId, // class ref
dumpMethodId, // method ref
[decorView, outputStream], // arguments
options = SINGLE_THREADED // optional flags
);
// ⑧ 获取 outputStream 中的内容
byte[] data = conn.invokeMethod(outputStream, "toByteArray", "()[B");
// ⑨ 解码 byte[] 得到文本内容
String layoutDump = new String(data, "UTF-8");
// ⑩ 解析 dump 数据并呈现
LayoutTree tree = LayoutParser.parse(layoutDump);
UI.render(tree);
补充说明
- 上述是一个高层抽象逻辑,真实的 JDWP 通信使用的是二进制协议,需要使用 JDWP command set / command code 和编解码结构;
- 类、字段、方法、对象在 JDWP 协议中都是通过 ID(数字)引用的;
- 所有参数都需要包装为 Value 类型(如 ObjectReference、IntValue、StringValue);
- invokeMethod 等指令必须考虑线程上下文、安全性与异常捕获;
- 对于 OutputStream 的实现,可以用 ByteArrayOutputStream(简单)或 Pipe/Socket 输出到调试器(复杂但高效)。
你可以把这段伪代码理解为:
一个简化版的 Layout Inspector 所做的事情,用纯调试通信方式,远程执行 Java 方法,不需要修改 App 代码、不需要插桩或 SDK 接入。
如果你有兴趣,我还可以提供 JDWP 二进制通信命令格式的样例,或者指导你用 Java 开发一个简化的 JDWP 客户端。是否需要?