Community Day 深圳站社区嘉年华,诚邀对技术抱有高度热忱的你!报名戳 了解详情
写点什么

滴滴 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:354582

评论

发布
暂无评论
发现更多内容

AfterShip 亿级流量 API 网关的演进

AfterShip

架构演进 kong API Gateway

内测邀请|宜搭应用搭建体验全新升级,邀您抢先体验!

钉钉宜搭低代码

钉钉 低代码 数字化转型 低代码平台 钉钉宜搭

大规模任务调度在 AfterShip 的高可用实践

AfterShip

高可用 任务调度 基础架构

传统IT开发与低代码开发争议白热化,技术开发规则或将重构?

优秀

低代码 低代码开发 低代码平台

19 K8S之临时/hostPath/网络存储卷

穿过生命散发芬芳

k8s 11月日更

24H营业的无人售药机上线 EasyDL帮你实现“排队自由”

百度大脑

人工智能 百度

惊为天人!Alibaba新产SpringBoot深度历险(嵩山版)开源

热爱java的分享家

Java 架构 面试 程序人生 编程语言

linux shell 命令行移动光标快捷键之跳到行首尾

WindFlying

MatrixDB 4.3 新特性:从“持续聚集”说起

YMatrix 超融合数据库

时序数据库 分布式时序数据库 MatrixDB

阿里云发布云原生加速器,携手生态企业拥抱数字时代

阿里巴巴云原生

云计算 阿里云 云原生 生态企业 创投

如何巧妙使用Camtasia制作PPT讲解视频?

淋雨

Camtasia

Python代码阅读(第60篇):列表是否包括指定的元素

Felix

Python 编程 列表 阅读代码 Python初学者

起飞!Alibaba全新出品JDK源码学习指南(终极版)限时开源

热爱java的分享家

Java 架构 面试 程序人生 编程语言

自定义View:如何手动实现ViewGroup的拖拽

Changing Lin

11月日更

StarRocks在格洛博电商系统的应用实践

StarRocks

数据库 大数据 数据分析 StarRocks

阿里云发布云原生加速器,携手生态企业拥抱数字时代

阿里巴巴中间件

云计算 阿里云 云原生 中间件 数字化

“阿里爸爸”又爆新作!Github新开源303页Spring全家桶高级笔记

热爱java的分享家

Java 架构 面试 程序人生 编程语言

文本检测实战:使用OpenCV实现文本检测(EAST 文本检测器)

AI浩

跨越速运 x StarRocks:统一查询引擎,强悍性能带来极速体验

StarRocks

数据库 大数据 数据分析 StarRocks

Go 语言学习查缺补漏ing Day1

Regan Yue

Go 语言 11月日更

用Python保住“设计大哥“的头发,直接甩给他10000张参考图,爬虫采集【稿定设计】平面模板素材

梦想橡皮擦

11月日更

技术分享 | 让实时协同的框架绘图变得丝滑起来

ShowMeBug

架构 绘图 实时消息

NLP进阶,使用TextRNN和TextRNN_ATT实现文本分类

AI浩

Can't Connect to MySQL Server on IP Address (10061) 错误的解决方案

蒋川

MySQL 数据库 MySQL 运维 MySQL 数据库

猿辅导 x StarRocks:构建统一OLAP平台,全面升级数据分析能力

StarRocks

数据库 数据分析 StarRocks

Lenovo x StarRocks:简化数据处理链路,极大提升BI分析效率

StarRocks

数据库 大数据 数据分析 StarRocks

前端遇到的那些技术难点

CRMEB

【得物技术】基于自注意机制的图像识别算法

得物技术

算法 图像识别 识别 图像 算法实践

中国邮政邮科院 X StarRocks:统一OLAP平台,大幅降低运维成本

StarRocks

数据库 大数据 数据分析 StarRocks

我为什么坚持在公司开分享会?

boshi

团队管理 知识管理

蚁剑的分析和利用

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 安全漏洞

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