写点什么

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

  • 2021-01-05
  • 本文字数:4569 字

    阅读完需:约 15 分钟

滴滴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<ByteData> 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<ByteData> Function(ByteData message) handler) { origin.setMessageHandler(channel, handler); }
@override void setMockMessageHandler( String channel, Future<ByteData> 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 的未来需要大家共同的努力。

2021-01-05 14:355018

评论 1 条评论

发布
用户头像
这个点了我还是不困
2023-11-24 15:17 · 北京
回复
没有更多了
发现更多内容

一种分表平滑扩容方案

非著名架构师

6道tomcat面试题,最后两道难倒我了

田维常

面试

期货合约系统软件开发|期货合约APP开发

系统开发

当前岗位的理想岗位模型

白生

【并发编程】- 内存模型篇

双木之林

并发编程

4K高清视频下载(4K视频素材下载)图文教程

科技猫

下载器 4k高清视频下载 4k视频素材下载 8k视频下载 高清视频下载

一字一句的让你彻底掌握JavaScript中的回调函数

华为云开发者联盟

JavaScript 大前端 同步 回调函数

Spring 源码学习 15:finishBeanFactoryInitialization(重点)

程序员小航

spring 源码

产品经理训练营

纳豆卡玛

求职 岗位要求 职能描述

第一次作业提交

涅米丶

产品0期-第一周作业

曾烧麦

产品训练营

云原生动态周报 | KubeEdge被评为2020十大边缘计算开源项目

华为云原生团队

云计算 云原生 边缘计算 华为云 边缘技术

如何构建高效可信的持续交付能力,华为云有绝活!

华为云开发者联盟

软件 DevOps 持续交付 华为云

作业-week1

赝品

产品经理训练营-第一章作业

泡面加煎蛋

Soul学习笔记---运行 soul-examples-http(二)

fightingting

Soul网关

产品经理训练营第一周作业

Empty

产品经理训练营 极客大学产品经理训练营

IO系列专题分享 - 概览

公众号:程序猿成神之路

io

个人选择理财产品有哪些好方法

v16629866266

经验说丨华为云视频Cloud Native架构下实践

华为云开发者联盟

架构 微服务 华为云 CloudNative

Java 程序经验小结:消除GC触及不到的过期对象引用

后台技术汇

28天写作

《原神》运维自动化的探索与实践

OpsMind

运维 运维自动化

开发实战:Float如何保留2位小数

worry

场外OTC交易APP系统软件开发

系统开发

开发实战:LocalDateTime转RFC3339格式

worry

雪天专注行路思考一则

石君

冥想 28天写作

干货丨深度迁移学习方法的基本思路

博文视点Broadview

这样提问,大牛才会为你解答(提问的智慧)

yes

AI无人机出手,让输电线路巡检更“聪明”!

华为云开发者联盟

华为云 modelarts 视觉处理

Soul 学习笔记---soul 数据同步的浅显分析(四)

fightingting

Soul网关

产品经理训练营-第一周作业

羽室

滴滴DoKit For Flutter正式开源,功能及核心实现解读_开源_林基宗_InfoQ精选文章