写点什么

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

评论 1 条评论

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

中秋节,华为云AI送上超级大月亮制作教程,体验赢开发者键鼠套装

华为云开发者联盟

人工智能 华为云 中秋节 企业号九月金秋榜

这份数据安全自查checklist请拿好,帮你补齐安全短板的妙招全在里面!

京东科技开发者

数据库 安全 数据安全 云安全 京东云

Zilliz 论文入选数据库顶会 VLDB'22

Zilliz

数据库 分布式 云原生 VLDB'22

数据治理(十一):数据安全管理Ranger初步认识

Lansonli

数据治理 9月月更

库调多了,都忘了最基础的概念 <锁与线程篇1>

知识浅谈

线程 9月月更

4天带你上手HarmonyOS ArkUI开发

HarmonyOS开发者

HarmonyOS

客户案例|宜泊科技怎样实现智慧停车可观测

观测云

如何让开发者直接在应用后台控制用户的运动状态?

HarmonyOS SDK

为什么越来越多博士逃离科研?

博文视点Broadview

下一代 SCA:流水线成分分析

SEAL安全

DevSecOps SCA 软件成分分析 软件组成分析 软件供应链安全

Qt|自定义Widget实现互斥效果问题

中国好公民st

qt QWidget 9月月更

云数据库技术|“重磅升级”后再测TDSQL-C

数据库 polarDB 玖章算术 TDSQL-C

【活动预告】数据集成海外专场Meetup:走进Shopee,聊透SeaTunnel优化实践

Apache SeaTunnel

技术分享 数据同步 数据集成 社区活动

降本:云原生可观测性新定义

观测云

JavaWeb -JavaBean MVC Filter 监听器 过滤器

喜羊羊

9月月更

Java 多线程:基础

Java快了!

java;

Sprint Review能不能做成Demo演示?

LigaAI

Scrum 敏捷开发 迭代增量开发 高效办公 企业号九月金秋榜

TDengine支持多种写入协议,四种写入方式提效大全

TDengine

tdengine 开源 时序数据库 企业号九月金秋榜

Authing 郑凌:我眼中的 Authing

Authing

讲究卡路里多少的美食出圈了!维小饭被评为“2022中国轻食十大品牌”

联营汇聚

javaweb-JSP

喜羊羊

9月月更

调用 sap.ui.base.ManagedObject 的构造函数时,如何传递绑定路径进去

汪子熙

JavaScript SAP SAP UI5 ui5 9月月更

最全元宇宙概念分析!元宇宙为何发展于区块链?

TinTinLand

区块链 人工智能 元宇宙

关于 eBPF 安全可观测性,你需要知道的那些事儿

OpenAnolis小助手

Linux 开源 内核 ebpf 龙蜥技术

ChaosBlade Java 场景性能优化,那些你不知道的事

Java快了!

java;

数据科学家、数据工程师和数据分析师三个角色的区别是什么

雨果

数据分析师 数据科学 数据工程师

面试突击:什么是跨域问题?如何解决?

Java快了!

java;

JavaScript 基础知识

喜羊羊

9月月更

nft系统开发

开源直播系统源码

区块链 NFT 数字藏品 数字藏品软件开发

Hi3861 通过UART串口协议与其它开发板进行通信

OpenHarmony开发者

OpenHarmony

构建运维监控体系设计思路概述

穿过生命散发芬芳

监控体系 9月月更

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