作者:闲鱼技术-君爱
1. 前言
闲鱼技术团队在2018年引入Flutter后,越来越多的业务场景在Flutter上使用。Flutter的亚秒级热重载一直是开发者的神兵利器,提供给开发者快速修改UI,增加功能,修复bug,不需要重新启动应用,即可看到改动效果。
热重载(HotReload)到底是如何实现的呢?
本文带你一步步揭开Hot Reload神秘面纱。
2. 源码分析
2.1 FlutterTools调试
想了解HotReload如何运行,首先,我们需要掌握flutter_tools的调试方法。
我们创建一个名为fluttertest的简单flutter项目作为例子。
使用AndroidStudio打开flutter_tools(/flutter/packages/flutter_tools),断点设置为HotRunner.restart()方法
添加新的Debug Configurations,woking directory设置为fluttertest项目地址
触发flutter_tools debug按钮,待app启动后,简单改动fluttertest代码
在flutter_tools Debug Console中输入r,开始调试。
断点成功!
###2.1 HotReload基本流程
那么HotReload如何运行呢?
当我们使用运行HotReload,无论是通过控制台输入r启动,或是点击闪电运行,最终是运行flutter_tools中的HotRunner.restart(fullRestart: false)方法(上文断点处)。
restart()方法中,调用了_reloadSources(pause: pauseAfterRestart),正是HotReload的主要代码之处。
(源码位于/flutter/packages/flutter_tools/lib/src/run_hot.dart)
Future<OperationResult> _reloadSources({ bool pause = false })
复制代码
_reloadSources方法中:
- 首先_updateDevFS()会将工程中文件逐一扫描,检查是否有删除、新增或者改动,扫描完成后,生成kernel files,命名为app.dill.incremental.dill文件,通过HTTP端口发送给DartVM;
- 将扫描生成的.dill文件路径,通过RPC接口调用_reloadSources,进行资源加载;
- 确认VM资源重载成功,将FlutterDevice UI线程重置,通过RPC接口,触发flutter widgets树重建、重绘
理解这个流程,前提需要明确Flutter的编译模式。
编译模式大体可以分为两种,AOT编译与JIT编译。JIT全称是Just In Time,代码可以在程序执行时期编译,因为要在程序执行前进行分析、编译,JIT编译可能会导致程序执行时间较慢;而AOT编译,全称Ahead Of Time,是在程序运行前就已经编译,从开发者修改代码、编译较慢,但运行时不需要进行分析、编译,因此执行速度更快。
Flutter使用了独特的编译模式,开发阶段下,使用Kernel Snapshot模式(对应JIT编译),将dart代码生成标记化的源代码,运行时编译,解释执行;release阶段,ios使用AOT编译,编译器将dart代码生成汇编代码,最终生成app.framwork,android使用了Core JIT编译,dart转化为二进制模式,在VM启动前载入。
因此,基于开发阶段的Kernel Snapshot编译模式下,我们可以得知Hot Reload扫描项目文件,将有改动的dart文件转化为标记化源代码kernel files,发送到正在运行的DartVM,DartVM替换资源,然后通知Flutter Framework重建、重新布局、重新绘制WidgetsTree,即可看到改动效果。
到这里,我们已经了解HotReload基本运行流程,但app.dill.incremental.dill是怎样的文件,又怎么和旧文件替换的呢?
2.2 增量代码扫描
在启动应用后,启动HotReload之前,编译成功后,项目目录/fluttertest/build文件中,自动生成了app.dill文件。 通过strings命令解析,发现是标记化的源代码,其中包含完整的业务代码。
(篇幅较长,只截取了一部分)
同时,通过adb shell检查,发现设备中/data/data/com.loommo.fluttertest/com.loommo.fluttertest/app_flutter/flutter_assets下,生成三个文件;
其中,kernel_blob.bin通过strings命令解析,发现内容与app.dill一致;
首次启动应用后,生成的完整的业务代码文件app.dill,在设备上体现为kernel_blob.bin;
我们启动HotReload,_updateDevFS()这一步骤执行完毕后,
(源码位于/flutter/packages/flutter_tools/lib/src/devfs.dart)
Future<int> update({@required String mainPath,String target,AssetBundle bundle,DateTime firstBuildTime,bool bundleFirstUpload = false,bool bundleDirty = false,Set<String> fileFilter,@required ResidentCompiler generator,String dillOutputPath,bool fullRestart = false,String projectRootPath,@required String pathToReload,})
复制代码
检查项目,可以发现项目目录/fluttertest/build/下新增了app.dill.incremental.dill文件,通过strings命令解析后,发现仅包含我们所改动的dart文件。
同时,通过adb shell检查,发现设备中/data/data/com.loommo.fluttertest/cache/fluttertestYAYDGJ/fluttertest/lib下,也增加了一个main.dart.incremental.dill ,通过strings命令解析。
果然,与app.dill.incremental.dill内容一致。
而/data/data/com.loommo.fluttertest/com.loommo.fluttertest/app_flutter/flutter_assets/kernel_blob.bin 没有改变。
上文中可以知道Flutter Tools生成app.dill.incremental.dill文件后,通过RPC调用_reloadSources,实际触发的是,Flutter Engine中DartVM Reload方法,该方法中,对.incremental.dill进行增量编译,替换编译产物,实现改动文件的更新。
(源码位于/engine/src/third_party/dart/runtime/vm/isolate_reload.cc)
void IsolateReloadContext::Reload(bool force_reload,const char* root_script_url,const char* packages_url_)
复制代码
后续文章继续深入分析,有兴趣的同学可以先仔细阅读源码。
2.3 WidgetsTree重建
从上文我们可以知道,Hot reload将资源重载完成后,通知flutter framework,触发widgets树的重新建立、重新布局、重新绘制。
那么,flutter是如何触发widgets树的重建呢?
Flutter framework中BindingBase注册了名为reassemble的Dart VM服务,用于外部与正在运行的Dart VM通信,能够触发根节点树重建操作。
服务触发后,BindingBase.reassembleApplication-> WidgetsBinding. performReassemble -> BuildOwner.reassemble -> Element.reassemble 由根节点开始一步步实现widgets树重建。
(源码位于/flutter/packages/flutter/lib/src/foundation/binding.dart)
Future<Null> reassembleApplication()
复制代码
3. 结语
Flutter不同于以往Native开发,广受赞誉的,其一便是亚秒级热重载,理解HotReload的原理,有助于辅助我们日常开发,更为后续动态化方案提供理论支持。
4. 参考文档
加入闲鱼,一起玩些“酷”的
闲鱼技术团队是一只短小精悍的工程技术团队。我们不仅关注于业务问题的有效解决,同时我们在推动打破技术栈分工限制(android/iOS/Html5/Server 编程模型和语言的统一)、计算机视觉技术在移动终端上的前沿实践工作。作为闲鱼技术团队的软件工程师,您有机会去展示您所有的才能和勇气,在整个产品的演进和用户问题解决中证明技术发展是改变生活方式的动力。 简历投递:[email protected]