Dart VM Service 实战

2,023 阅读4分钟

背景:

Android 开发中反射技术被广泛应用作用如下:

  • 创建类、获取私有成员、方法
  • 动态代理
  • 运行时调用对象任意方法
  • 热修复、插件化、Retrofit、Gson 各种库等

Flutter 中为了包大小,健壮性等的原因禁止了反射

Flutter 如何动态调用方法、获取对象信息?

可以借助 Dart VM 来实现。


本文将实战以下内容:

使用 VM Service 调用 Dart 方法获取私有对象。

  • _element 树信息
  • _MyHomePageState 中的 _counter 值。

Dart VM Service

一 什么是 Dart VM

Java 中的 JVM 类似,Dart VM 是用于在本地执行 Dart 代码的组件集合。

一句话来说 Dart VM 是一个虚拟机 它包括以下内容:

  • 运行时系统
    • 对象模型
    • 垃圾收集
    • 快照
  • 核心库原生方法
  • 可通过服务协议访问的开发体验组件 调试分析 热重载
  • 即时 (JIT) 和提前 (AOT) 编译管道
  • 口译员
  • ARM 模拟器

Dart VM

二、什么是 Dart VM Service

是 Dart 虚拟机内部提供的一套 Web 服务,通过 JSON-RPC 2.0 协议访问 Dart VM 服务协议的库。

通过JSON-RPC 2.0意味着可以从其他进程获取App中的数据

通过 yaml 文件我们可以使用 vm service 的功能了

实战:获取 _MyAppState 中的 _counter 对象

我们将在电脑(PC)端中的 Dart 进程来获取 App 中的数据。

创建一个 Flutter 新工程。

image.png

创建一个测试文件在test目录下。

void main() async {

}

添加 vm_service 依赖

dependencies:
 vm_service: ^7.5.0

1.1 在 main.dart 顶层文件位置添加获取 WidgetsBinding.instance!.renderViewElement! 的方法

Element instance() => WidgetsBinding.instance.renderViewElement;

void main() {
  runApp(const MyApp());
}

1.2 拿到 VM Service URI

如何拿到 URI

方法一: 我们启动 Flutter App 的时候日志已经告诉我们了:

√  Built build\app\outputs\flutter-apk\app-debug.apk.
Installing build\app\outputs\flutter-apk\app.apk...
Debug service listening on ws://127.0.0.1:50122/HPw25EKJ0bs=/ws

方法二: 通过 Service.getInfo() 方法获取 URI 。该方法能让我们的 App 知道 VM Service URI

需要注意:

  • 不同的进程会得到不同的结果
Future<String?> getObservatoryUri() async {
    ServiceProtocolInfo serviceProtocolInfo = await Service.getInfo();
    _observatoryUri = serviceProtocolInfo.serverUri;
    Uri url = convertToWebSocketUrl(serviceProtocolUrl: _observatoryUri!);
    return url.toString();
}

通过以上两种方法我们能得到 URI: ws://127.0.0.1:50122/HPw25EKJ0bs=/ws 。本次实战将使用方法一来实现

1.3 VM Service 获取 renderViewElement 实例。打印 Element 树信息并找到 MyHomePageState_counter

获取流程如下:

    1. 通过 URIAppVM Service 建立连接
    1. 获取 mainIsolatelibrary 的 id。
    1. 通过 invoke 方法调用我们在 1.1中的 main.dart 中写好的函数, 得到 Json 格式的 Response
    1. Response 转换为 Instance 引用对象
    1. 根据引用对象的 id 转换为实例对象
    1. 重复以上操作遍历 element 树找到 _MyHomePageState 对象拿到 _counter
import 'package:vm_service/vm_service.dart';
        import 'package:vm_service_demo/vm_service_utils.dart';```````

const libraryPath = 'package:vm_service_demo/main.dart';
final utils = VmServerUtils(uriStr: 'ws://127.0.0.1:63645/l_CQSE1CsE8=/ws');

late final VmService vms;
late final Isolate mainIsolate;
late final LibraryRef library;
late final Instance renderViewElement;

void main() async {
  vms = await utils.getVmService();
  mainIsolate = (await utils.findMainIsolate())!;
  library = (await utils.findLibrary(libraryPath))!;

  /// 私有方法,getter、setter 方法不能调用
  Response? renderViewElementResponse =
      await vms.invoke(mainIsolate.id!, library.id!, 'binding', []);
  renderViewElement = Instance.parse(renderViewElementResponse.json)!;

  /// 从 renderViewElement 深度优先遍历
  await depthFirst(renderViewElement.id!);
}

Future<void> depthFirst(String keyId) async {
  while (true) {
    final obj = await _depthFirst(keyId);
    if (obj == null) break;

    ///  List<Widget> 递归遍历
    if (obj.elements != null && obj.elements!.isNotEmpty) {
      for (final e in obj.elements!) {
        await depthFirst(e.id!);
      }
    }
    return;
  }
}

Future<Instance?> _depthFirst(String objId) async {
  Obj obj = await vms.getObject(mainIsolate.id!, objId);
  var element = Instance.parse(obj.json)!;

  while (true) {
    Response widgetResponse = await vms.invoke(mainIsolate.id!, element.id!, 'toString', []);
    final widgetRef = Instance.parse(widgetResponse.json);

    print(' ${widgetRef!.valueAsString}');
    if (widgetRef.valueAsString!.startsWith('MyHomePage')) {
      final _counter = await findCounterField(element);
      print('_counter = $_counter');
      return null;
    }

    final child = element.getFieldValueInstance('_child');

    if (child is InstanceRef) {
      obj = await vms.getObject(mainIsolate.id!, child.id!);
      element = Instance.parse(obj.json!)!;
    } else if (child == null) {
      final _children = element.getFieldValueInstance('_children');
      if (_children == null) return null;

      obj = await vms.getObject(mainIsolate.id!, _children.id!);
      element = Instance.parse(obj.json!)!;
      return element;
    } else {
      return null;
    }
  }
}

Future<String> findCounterField(Instance element) async {
  final state = element.getFieldValueInstance('_state');
  final stateObj = await vms.getObject(mainIsolate.id!, state.id!);
  var _counterRef = Instance.parse(stateObj.json)!;

  return _counterRef.valueAsString!;
}

我们成功拿到了 WidgetsFlutterBinding 对象,并打印了所有 element 对象信息并打印出了 counter = 10

image.png

二、ObjRef , Obj 和 id 的作用

先介绍 vm_service 中的核心内容:ObjRefObjid

vm_service 返回的数据主要分为两大类, ObjRef(引用类型)和 Obj(对象实例类型)。其中 Obj 完整的包含了 ObjRef 的数据,并在其基础上增加了额外信息(ObjRef 只包含了一些基本信息,例如:idname 等)。

基本所有的 API 返回的数据都是 ObjRef,当 ObjRef 里面的信息满足不了你的时候,再调用 getObject(,,,)来获取 Obj

关于 id  Obj 和 ObjRef 都含有 id,这个 id 是对象实例在 vm_service 里面的一个标识符, vm_service 几乎所有的 API 都需要通过 id 来操作,比如:getInstance(isolateId, classId, ...)、 getIsolate(isolateId)、 getObject(isolateId, objectId, ...)

总结:

  1. Dart VM Serviceinvoke() 方法虽然跟反射很像,但是它不能执行 构造函数、getter、setter 方法。
  2. Flutter Driver 就是封装了 Dart VM Service 完成了自动化测试的功能。
  3. Dart VM Service 只有在 Debug、ProFile 模式下才能运行,release 模式不能使用。
  4. vm_service_demo github

引用:

Dart VM

vm_service

sdk/service.md at main · dart-lang/sdk