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 客户端。是否需要?

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器