滴滴 DoKit For Flutter 正式开源,功能及核心实现解读

DoKit For Flutter是一个 DoKit 针对 Flutter 环境的产研工具包,内部集成了各种丰富的小工具,UI、网络、内存、监控等等。


Github地址


Pub仓库地址


操作文档

Flutter 是 Google 开源的跨端技术框架。凭借其区别于 RN/Weex 的自渲染模式,在社区里引起了广泛关注,不管是终端还是前端的小伙伴都趋之若鹜,大有一统大前端江湖的气势。而国内大厂如闲鱼、字节、美团等,也都在其核心业务上完成了落地。

早在两年前,滴滴就有多个内部团队开始在 Flutter 领域进行尝试。但是在开发过程中,我们遇到了很多调试性问题,如日志、帧率、抓包等。为了解决这些开发测试过程中遇到的各类问题,DoKit 团队联合滴滴代驾和货运团队,把平时工作过程中沉淀下来的效率工具进行业务剥离和脱敏,并最终打造出 DoKit For Flutter
,在服务内部业务的同时,也为社区贡献一份力量。

那么接下来就让我来列举一下 DoKit For Flutter
的功能以及核心实现。

工具详解

基本信息

基本信息模块会展示当前 dart 虚拟机进程、CPU、Flutter 版本信息、当前 App 包名和 dart 工程构建版本信息。

VM 信息通过 VMService
获取。Flutter 版本实际上是通过 Devtools 服务注入的”flutterVersion”方法获取到的,在 flutter attach 后,本地会起一个 websocket 服务,连接 VMService 并注入 flutterVersion 和其余方法(HotReload、HotRestart 等),通过 VMService 调用 flutterVersion 方法,会从本地 flutter sdk 目录下解析 version 文件返回版本号。

路由信息

在 Flutter 中,每个页面对应一个 Route,通过 Navigator 管理 Route。
Navigator 内部会包含一个 Overlay Widget,每个 Route 最终都转化成一个_OverlayEntryWidget 添加到 Overlay 上。这个地方可以把 Overlay 理解为 Android 中的 FrameLayout,内部子 View 上下叠加。每打开一个新的 Route,都相当于往 FrameLayout 添加一个新的子 View。
Navigator 会存在嵌套的情况,即 Route 所创建的页面本身也包含一个 Navigator,比如 App 的根 Widget 是 MaterialApp(自带 Navigator),Route 页面也用 MaterialApp 包裹,就会形成 Navigator 嵌套的情况。还是以 FrameLayout 来理解,这也就相当于嵌套的 FrameLayout。
路由信息功能会打印出当前栈顶页面所处的 Route 信息,如果存在 Navigator 嵌套的情况,也会向上遍历打印出每层 Navigator 的信息。
具体的实现方式是,先获取当前根 app 根 Element,可以使用 WidgetsBinding.instance.renderViewElement 作为根 Element,再通过递归调用 element 的 visitChildElements 方法,向下遍历整棵树找到最后一个 RenderObejctElement,该 RenderObejctElement 即为当前显示的页面上的元素。然后使用 ModalRoute.of(element)方法即可获取到当前页面的路由信息。
至于嵌套的路由信息,则可以通过找到的 RenderObejctElement 的 findAncestorStateOfType 方法,反向向上递归遍历,获得所处的 Navigator 的 NavigatorState,再调用 ModalRoute.of(navigatorState.context),如果返回不为空则表示存在嵌套。

方法通道

Flutter 的 Method Channel 调用最终都会经过 ServiceBinding.instance._defaultBinaryMessenger 这个对象,类型为 BinaryMessenger,由于这个对象是个私有对象,无法动态进行修改。不过查看 ServiceBinding 的源码可以发现这个对象是通过 ServiceBinding.createBinaryMessenger 方法创建的,通过使用 flutter 的 mixins,可以实现对该方法的重写。
我们知道,ServiceBinding 实际也是通过 mixins 在 WidgetsFlutterBinding.ensureInitialized 方法中一起被初始化的,所以只要在 WidgetsFlutterBinding 这个类额外 mixin 一个继承于 ServiceBinding 并且重写了 createBinaryMessenger 方法的类,就能实现对 ServiceBinding 中 createBinaryMessenger 的覆盖,代码如下:

class DoKitWidgetsFlutterBinding extends WidgetsFlutterBinding    with DoKitServicesBinding {  static WidgetsBinding ensureInitialized() {    if (WidgetsBinding.instance == null) DoKitWidgetsFlutterBinding();    return WidgetsBinding.instance;  }}
mixin DoKitServicesBinding on BindingBase, ServicesBinding { @override BinaryMessenger createBinaryMessenger() { return DoKitBinaryMessenger(super.createBinaryMessenger()); }}

复制代码

接下去把 runApp 的入口调用改成如下,就能实现 BinaryMessenger 的替换 static void _runWrapperApp(DoKitApp wrapper) { DoKitWidgetsFlutterBinding.ensureInitialized() ..scheduleAttachRootWidget(wrapper) ..scheduleWarmUpFrame(); } 至于 Method Channel 具体信息的捕获,只要 hook 住 BinaryMessenger.handlePlatformMessage 和 BinaryMessenger.send 两个方法就行了,具体可看 DoKitBinaryMessenger 这个类

控件检查

和路由功能类似,通过从根 element 向下遍历,在遍历过程中记录和选中的 View 有交集的所有 RendereObjectElement,并且记录用以标志当前页面的 RendereObjectElement,获取它的 Route 信息。
遍历完成后,遍历记录下来的 RendereObjectElement,过滤掉 Route 信息和当前页面不一致的,这些 Element 属于被遮盖住的页面。然后通过比对 RendereObjectElement 和选中 View 的交叉区域面积占 RendereObjectElement 面积的比例,占比最大的为当前选中的组件。
在 Debug 模式下可以获取选中组件在工程中的代码位置,将 WidgetInspectorService.instance.selection.current 赋值为选中 element 的 renderObject,再调用 WidgetInspectorService.instance.getSelectedSummaryWidget 方法,会返回一个 json 字符串,解析这个字符串就能获取源码文件名、行列信息等。

日志查看

日志查看功能比较简单,只要使用 runZoned 方法替代 runApp,传入 zoneSpecification,就能为日志输出设置一个代理函数,在这个代理函数内进行日志捕获,同时,还可以为 onError 设置一个代理函数,在这里将捕获的异常也会传入到日志当中。

帧率

使用 WidgetsBinding.instance.addTimingsCallback 可以统计帧率信息,在每帧渲染完成时会触发回调,包含该帧渲染的信息。

内存

同 VM 信息,使用 VMService 可以获取到内存详细使用信息。

网络请求

Flutter 自带的网络请求通过 HttpClient 类发送,只要 hook 住 HttpClient 的创建就可以 hook 整个网络请求的过程。查看 HttpClient 的构造函数可以发现,如果存在 HttpOverrides,就会使用 HttpOverrids 来创建 HttpClient

factory HttpClient({SecurityContext? context}) {  HttpOverrides? overrides = HttpOverrides.current;  if (overrides == null) {    return new _HttpClient(context);  }  return overrides.createHttpClient(context);}// 所以这里重写了一个HttpOverridsclass DoKitHttpOverrides extends HttpOverrides {  final HttpOverrides origin;
DoKitHttpOverrides(this.origin);
@override HttpClient createHttpClient(SecurityContext context) { if (origin != null) { return DoKitHttpClient(origin.createHttpClient(context)); } // 置空,防止递归调用,使得_HttpClient可以被初始化 HttpOverrides.global = null; HttpClient client = DoKitHttpClient(new HttpClient(context: context)); // 创建完成后继续置回DoKitHttpOverrides HttpOverrides.global = this; return client; }}

复制代码

替换 HttpOverrides

HttpOverrides origin = HttpOverrides.current;HttpOverrides.global = new DoKitHttpOverrides(origin);

复制代码

hook 住 HttpClient 方法后,对于请求和返回结果的 hook 过程就和 Android 中的 HttpUrlConnection 类似了,具体可以看 DoKitHttpClient、DoKitHttpClientRequest、DoKitHttpClientResponse 三个类。

版本 API 兼容

Flutter 版本更新还是比较快的,每一个大版本更新都会带来一些 API 的变更,目前 DoKit 的方案需要重写一些 framework 层的类,在兼容多版本时就会有一些问题。以上面的 BinaryMessager 为例,1.17 版本只有四个方法,用来 hook 的 DoKitBinaryMessager 是这么写的

class DoKitBinaryMessenger extends BinaryMessenger {  final MethodCodec codec = const StandardMethodCodec();  final BinaryMessenger origin;
DoKitBinaryMessenger(this.origin);
@override Future<void> handlePlatformMessage(String channel, ByteData data, callback) { ChannelInfo info = saveMessage(channel, data, false); PlatformMessageResponseCallback wrapper = (ByteData data) { resolveResult(info, data); callback(data); }; return origin.handlePlatformMessage(channel, data, wrapper); }
@override Future send(String channel, ByteData message) async { ChannelInfo info = saveMessage(channel, message, true); ByteData result = await origin.send(channel, message); resolveResult(info, result); return result; }
@override void setMessageHandler( String channel, Future Function(ByteData message) handler) { origin.setMessageHandler(channel, handler); }
@override void setMockMessageHandler( String channel, Future Function(ByteData message) handler) { origin.setMockMessageHandler(channel, handler); }}

复制代码

用来 hook 的 wrapper 类需要调用 oring 对象的同名方法。但在 1.20 版本 BinaryMessager 增加了两个新方法 checkMessageHandler 和 checkMockMessageHandler,如果使用 1.17.5 版本的 flutter sdk 去编译,就无法调用 origin.checkMessageHandler 方法,因为不存在;如果使用 1.20.4 版本的 flutter sdk 去编译,编译和发布没问题,但编出来的 sdk 在 1.17.5 的工程被引用后,也会因为 checkMessageHandler 方法不存在导致编译失败。
针对这种多个 Flutter 版本 API 不同导致的兼容性问题,可以使用扩展方法 extension 关键字来解决。 建立一个_BinaryMessengerExt 类如下:

extension _BinaryMessengerExt on BinaryMessenger {  bool checkMessageHandler(String channel, MessageHandler handler) {    return this.checkMessageHandler(channel, handler);  }
bool checkMockMessageHandler(String channel, MessageHandler handler) { return this.checkMockMessageHandler(channel, handler); }}

复制代码

在 1.17.5 版本,调用 origin.checkMessageHandler 会走到扩展方法的 checkMessageHandler 中,编译能通过,由于这个方法在 1.17.5 中是绝对不会被调用到的,虽然会形成递归调用,但没影响。而在 1.20 版本,BinaryMessenger 本身实现了 checkMessageHandler 方法,所以调用 checkMessageHandler 方法会走到 BinaryMessenger 的 checkMessageHandler 方法中,也能正常使用。 通过 extentsion,只要以最低兼容版本的类作为基础,在扩展类中定义新版本中新增的 API,就能解决多版本 API 兼容的问题。

总结

以上就是 DoKit For Flutter
的现有功能以及工具的基本原理介绍。 我们知道当前它的功能还不是完善,后续我们会继续不断深入的挖掘业务中的痛点并持续输出各种提高用户效率的工具,努力让 DoKit For Flutter
变得更加优秀,符合大家的期望。
DoKit 一直追求给开发者提供最便捷和最直观的开发体验,同时我们也十分欢迎社区中能有更多的人参与到 DoKit 的建设中来并给我们提出宝贵的意见或 PR。 DoKit 的未来需要大家共同的努力。