解读京东零售云mPaaS中Flutter中热重载原理

云计算 PaaS
熟悉JS的同学,可能会嗤之以鼻,在N年前就已经用上热重载了,但是对客户端开发人员来说,简直是福音。

本文要点:

  • 了解京东零售云mPaaS中Flutter的热重载原理,有利于日常开发中高效排查问题;
  • 掌握如何调试断点Flutter工具链源码;

一、前言

1.1 热重载是什么?

熟悉JS的同学,可能会嗤之以鼻,在N年前就已经用上热重载了,但是对客户端开发人员来说,简直是福音。

那先来看下Flutter官方的定义:

  • Flutter’s hot reload feature helps you quickly and easily experiment, build UIs, add features, and fix bugs. Hot reload works by injecting updated source code files into the running
  • Dart Virtual Machine (VM). After the VM updates classes with the new versions of fields and functions, the Flutter framework automatically rebuilds the widget tree,
  • allowing you to quickly view the effects of your changes.

简单来说,就是通过将修改后的源代码文件注入到正在运行的 Dart 虚拟机来实现,注入之后, Flutter 会自动重新构建 widget 树。

1.2 为什么需要热重载?

程序猿在刀耕火种的时代,开发调试是这样子的:

当项目不大,人数不多的情况下,画面是非常和谐的,效率也是毋庸置疑的高效。但现实是,在大公司,项目往往很大,编译巨慢无比,同时开发人员众多,有着非常严格的流程制度,导致看起来本没有问题的开发调试流程,变得异常的痛苦,降低了个体的效率,这里强调下,指的是个体的效率,个人认为越是完善的流程体系,对个体的约束往往越强,但从团队的角度去看待效率,一定是能 1+ 1 大于 2 的。

而此时的心情是这样子的:

[[424264]]

而有了热重载,开发调试是这样子的:

心情也就成这样子的:

[[424266]]

1.3 抛出问题

从热重载定义来看,不少人脑子里蹦出不少跟我一样的疑惑:

  • 怎么知道哪个文件被修改?
  • 修改的源代码到底被转成什么?
  • 修改的源代码是怎么注入到Dart虚拟机的?
  • Flutter框架又是怎么触发widget重绘的?

同时在日常使用热重载的过程中,也会碰到不少这样那样的疑惑:

  • 为什么运行flutter attach后还需要手动输入r来热重载?
  • 手动敲r,这么无(gou)语(shi)的设计,我们能做成自动化吗?

当你在网上看过大量热重载文章后,又衍生了额外的问题:

  • 尝试去探索源码时,case太多,怎么能模拟真实环境?能否断点调试Flutter源码?
  • 热重载看着跟动态化很像,那能否运用在动态化技术上?

不急,本文会对上述疑问进行一一解答。

二、dart的热重载

由于Flutter采用dart作为开发语言,我们先从dart角度来验证下热重载。

2.1 编写验证demo

考虑到dart执行完会关闭当前进程,我们写了个定时器来保证进程存活,同时能看到热重载效果。

2.2 开启VMService

终端下执行 dart --enable-vm-service main.dart,其中的main.dart为2.1中代码文件:

可以看到终端会不断输出"Hello JDFlutter"的字符。

2.3 执行热重载

我们将main.dart文件中打印日志修改为”Hello JD”,同时打开终端输出的Observatory链接地址,如下:

找到我们main.dart的Isolate(读者可以简单理解为是dart中的线程,只不过Isolate没有共享内存),图中红圈部分,进入后找到Reload Source:

点击Reload Source后,终端开始输出”Hello JD”的字符,完成了一次热重载过程,如下图:

2.4 自动化热重载

还是以上面为例子基础例子,我们加入文件监听,并且通过发送消息给vm_service来实现热重载,代码如下:

直接运行 dart --enable-vm-service main.dart,期间修改”Hello JDFlutter”为”Hello JD”,运行结果如下:

可以看出,我们成功实现了自动化热重载,上述代码跟Dart虚拟机通信步骤如下:

  • 获取Dart VM的websocket服务URI
  • 通过URI连接上Dart VM的service
  • 通过service获取Dart VM
  • 通过Dart VM获取isolateId
  • 通过service重载指定isolateId的任务

2.5 Dart虚拟机可做的事情

到这里,大家可以放飞自我,Dart Service提供了大量对外协议,包含断点、获取虚拟机状态,性能等协议,可以参考:Dart虚拟机服务接口。

三、Flutter的热重载

Flutter的热重载,本质是在封装dart热重载并且对不同的设备启动安装加载等流程,接下来准备好在Flutter源码世界里翱翔吧,以下分析基于v1.22.5分支的源码。

俗话说,工欲善其事必先利其器,在源码翱翔久了,容易迷茫,找不到东西南北,看到关键方法,又不知道是不是代码真实的case,需要能验证我们的想法,最简单的办法打断点,有针对性的去看源码。

3.1 IDE断点

Flutter源码的下载也很简单,这里就不赘述了,大家可以上网搜下。Flutter工具链的源码位于packages/flutter_tools下。

本文是通过Android Studio(比较熟)来配置和查看源码,配置如下:

  • 第一步,先新建一个运行配置,选Dart Command Line App;
  • 第二步,找到Flutter源码中工具链的入口文件,flutter_tools.dart;
  • 第三步,输入想运行的命令;
  • 第四步,找到要调试的Flutter工程;

一顿配置下来,就可以用工具链完美的debug指定Flutter工程的源码,接下来就是选好设备,点击debug按钮,如下图:

3.2 整体流程

以下是Flutter热重载流程图:

简述为:

  • 代码改动:工具会扫描工程下的文件,通过修改时间来比对哪些文件被修改;
  • 首次编译:第一次启动会生成全量app.dill文件;
  • 增量编译:对修改的文件编译生成app.dill.incremental.dill增量文件;
  • 更新文件:将增量产物推送到设备中;
  • UI更新:DartVM收到增量文件后进行合并,并通知Flutter引擎更新UI

整个过程并没有让App重启,从而达到高效开发调试效果。

3.3 源码分析

3.3.1 run命令流程

我们从flutter run命令为入口分析,类位于packages/flutter_tools/lib/executable.dart中的main()方法,run命令最终实现类位于packages/flutter_tools/lib/src/commands/run.dart。

RunCommand在构造函数中默认开启了hot标识,如果需要关闭,要新增入参--no-hot。

从run命令的流程,可以看出,主要是做了默认参数设置,参数校验,flutter设备初始,模式判断等,热重载是从HotRunner.run中开始执行。

3.3.2 热重载流程-首次启动

在HotRunner中,流程也并不复杂:

可以看出,HotRunner做了三件事:

  • 对目标设备,编译生成dill文件(有人叫kernel文件,本质是一种中间描述,后文会介绍);
  • 对目标设备,安装运行App;
  • 对目标设备进行attach,从而开启attach;

第二步会涉及到不同平台不同做法,对iOS和Android来说,分别对应xcrun和adb,不是本文重点,流程也比较长,以后有机会再展开讲,重点说第一步和第三步。

编译生成dill文件

最终调用到_compile方法,代码太过于繁琐,我们直接断点看,如下:

从断点信息可以获知,dart文件会被转为kernel文件app.dill,以下截取部分app.dill内容,可以看出app.dill是一份完整的代码文件,包含了main.dart的内容,右边为main.dart源文件,左边为app.dill文件内容:

 

生成的app.dill是一份全量的代码,接下来编译不同设备(Android、iOS)的安装包,同时运行指定的包。

此时生成app.dill的进程,我们暂且称为“编译进程”,后续热重载增量的dill,也是驱动该进程生成。

attach设备

在上述的第二步,设备在启动运行App时,会打开App中DartVM的Observatory服务,本质是一个websocket服务,按照自定义的jsonrpc2.0协议进行通信,在attach时,会通过URI连接上设备服务,如下图:

连上DartVM服务后,会注册几个热重载事件:reloadSources,reloadMethod,hotRestart,这几个事件并不是注册到App中的Dart虚拟机,而是提供给flutter tool其他命令使用,如下图:

同时通过DartVM服务,来初始设备中flutter产物,设备中产物路径是临时生成,用XXX代替,产物路径为:

  • Android中为:file:///data/user/0/com.example.flutter_app/code_cache/XXX/flutter_app/
  • iOS模拟器中为:/Users/hexianting/资源库/Developer/CoreSimulator/Devices/BC003085-8F19-4EF3-AB84-BD44282F79B7(模拟器设备ID)/data/Containers/Data/Application/745DE582-59F1-4193-9692-131E611A9359/tmp/XXX/flutter_app/

具体代码如下:

3.3.3 触发热重载

下面分别从源码角度,看看到底做了什么?

开发者在执行flutter run或者flutter attach后,在终端中输入r,即可体验到重载效果,如果在Android Studio和VSCode中,直接Ctrl+S或者Cmd+S即可。

对应到源码入口:

不管是HotReload还是HotRestart,最终都是调用HotRunner.restart方法,一路跟进,最终会到某个具体设备update方法,并再次调用上述《热重载流程-首次启动》中的_compile方法,通知编译进程生成增量的dill文件app.dill.incremental.dill。那这个增量文件到底是什么呢?demo中修改字符串"Flutter Demo Home Page"为"Flutter Demo Home Page2",来看看dill文件内容:

第一张图为修改前,第二种为修改后,第三张为增量的dill内容。可以看出增量的dill文件仅包含改动的dart文件代码。

生成增量的dill后,会通过_DevFSHttpWriter写入设备,如下图:

当同步完增量文件,最后还需要通知DartVM去刷新UI界面,这个步骤就跟我们上述的2.4节内容类似:

vmService.reloadSources最终调用了_call方法,这是一个dart官方库,如下:

HotRestart与HotReload区别

Flutter官方提供两种快速调试方法,一种是HotReload,另一种是HotRestart。前者无感知局部刷新,体验最好,但是缺点也很明显,适用比较局限,可以参考官网给出样例:HotReload,主要有这几种场景不适用:

  • enum改成class类;
  • 字体修改;
  • 泛类型修改;
  • Android和iOS原生修改;

而在HotRestart流程中,相比HotReload流程,增加了清除资源操作,同时不再生成增量的dill文件,每次改动都是生成全量的app.dill文件,该细节就不展开,感兴趣读者可以debug源码看。

上述可以看出HotRestart额外处理了一些事情,包括杀掉非UI的isolate,重置UI的isolate等。

对于dill文件同步到设备中位置,不同设备不一样:

  • Android:file:///data/user/0/com.example.flutter_app/code_cache/XXX/flutter_app/lib/
  • iOS模拟器:/Users/hexianting/Library/Developer/CoreSimulator/Devices/BC003085-8F19-4EF3-AB84-BD44282F79B7(模拟器设备ID)/data/Containers/Data/Application/9C8E4694-AC99-4A5C-BC46-63567F1C6FD9/tmp/XXX/flutter_app/lib/

至此,热重载源码就告一段落,很多奇技淫巧并不能一一展现,值得大家动手去看看。

四、总结

经过上述一顿探索,文章最早提出的几个疑问,想必都有了答案。这里只是介绍了Flutter源码的冰山一角,更多源码还需要继续探索,通过阅读源码,可做的事情很多:

  • 通过文件监听+vm_service通信,干掉手动输入r或者R的这种无(gou)语(shi)设计;
  • 源码中并没有限制多个设备,flutter run同时运行在多个模拟器中,并开启热重载;
  • iOS模拟器不重新安装App的情况下,直接替换模拟器中的flutter产物,以达到快速调试手段;
  • debug状态下的DartVM可以通过热重载来动态化,但性能较低,与谷歌Flutter的高性能目标不符;

总之,可做的事情很多,那我们看源码的意义就非常清晰:

  • 深入了解Flutter运行机制,去定制Flutter框架;
  • 通过研究这些顶级工程师的实现思路,去完善我们自己的逻辑体系,从而成为一个更加严谨的人。

五、参考资料

  • https://flutter.dev/docs/development/tools/hot-reload
  • http://gityuan.com/2019/09/07/flutter_run/
  • https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md
  • http://static.kancloud.cn/alex_wsc/flutter_demo/1570089

 

责任编辑:未丽燕 来源: 京东零售云
相关推荐

2021-09-17 18:40:55

京东mPaaS移动端

2021-09-16 18:44:05

京东云PaaS平台Android

2019-03-21 19:19:35

新零售阿里云零售云

2018-01-22 10:33:01

云计算 新零售

2021-11-04 08:00:00

人工智能机器学习技术

2021-09-08 18:12:57

京东零售云

2023-03-30 10:06:58

2016-10-19 18:31:13

云存储

2022-06-28 13:41:43

京东数据处理

2018-06-06 17:39:03

2022-05-18 13:24:47

京东调优实践

2023-05-11 08:00:30

2018-03-20 09:56:50

新零售

2017-09-30 10:00:41

2020-06-07 10:07:04

机器人零售业人工智能

2020-02-05 13:00:51

云计算数据开发

2019-07-17 05:33:33

零售物联网IOT

2021-08-13 11:38:51

京东零售云智能出行

2017-10-30 17:18:15

阿里云
点赞
收藏

51CTO技术栈公众号