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 _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 update({@required String mainPath,String target,AssetBundle bundle,DateTime firstBuildTime,bool bundleFirstUpload = false,bool bundleDirty = false,Set fileFilter,@required ResidentCompiler generator,String dillOutputPath,bool fullRestart = false,String projectRootPath,@required String pathToReload,})
检查项目,可以发现项目目录/fluttertest/build/下新增了app.dill.incremental.dill文件,通过strings命令解析后,发现正是我们所改动的文件。
同时,通过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)
3. 结语
Flutter不同于以往Native开发,广受赞誉的,其一便是亚秒级热重载,理解HotReload的原理,有助于辅助我们日常开发,更为后续动态化方案提供理论支持。
想了解Flutter Hot Reload相关原理的同学请加 QQ:2082723780