揭秘Flutter Hot Reload(基础篇)

1. 前言

自闲鱼引入Flutter后,越来越多的业务场景在Flutter上使用。Flutter的亚秒级热重载一直是开发者的神兵利器,提供给开发者快速修改UI,增加功能,修复bug,不需要重新启动应用,即可看到改动效果。

热重载(HotReload)到底是如何实现的呢?

本文带你一步步揭开Hot Reload神秘面纱。

2. 源码分析

2.1 FlutterTools调试

想了解HotReload如何运行,首先,我们需要掌握flutter_tools的调试方法。

我们创建一个名为fluttertest的简单Flutter项目作为例子。

使用AndroidStudio打开fluttertools(/flutter/packages/fluttertools),断点设置为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/fluttertools/lib/src/runhot.dart)

  1. Future<OperationResult> _reloadSources({ bool pause = false })

_reloadSources方法中:

  1. 首先_updateDevFS()会将工程中文件逐一扫描,检查是否有删除、新增或者改动,扫描完成后,生成kernel files,命名为app.dill.incremental.dill文件,通过HTTP端口发送给DartVM;

  2. 将扫描生成的.dill文件路径,通过RPC接口调用_reloadSources,进行资源加载;

  3. 确认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/appflutter/flutterassets下,生成三个文件;

其中,kernel_blob.bin通过strings命令解析,发现内容与app.dill一致;

首次启动应用后,生成的完整业务代码文件app.dill,在设备上体现为kernel_blob.bin;

我们启动HotReload,_updateDevFS()这一步骤执行完毕后,

(/flutter/packages/flutter_tools/lib/src/devfs.dart)

  1.  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/appflutter/flutterassets/kernel_blob.bin 没有改变。

上文中可以知道Flutter Tools生成app.dill.incremental.dill文件后,通过RPC调用_reloadSources,实际触发的是,Flutter Engine中DartVM Reload方法,该方法中,对.incremental.dill进行增量编译,替换编译产物,实现改动文件的更新。

(/engine/src/thirdparty/dart/runtime/vm/isolatereload.cc)

  1. 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通信,服务触发后能够触发根节点树重建操作。

服务触发后,由根节点开始一步步实现widgets树重建。

BindingBase.reassembleApplication-> WidgetsBinding. performReassemble -> BuildOwner.reassemble -> Element.reassemble 。

(/flutter/packages/flutter/lib/src/foundation/binding.dart)

  1. Future<Null> reassembleApplication()

3. 结语

Flutter不同于以往Native开发,广受赞誉的,其一便是亚秒级热重载,理解HotReload的原理,有助于辅助我们日常开发,更为后续动态化方案提供理论支持。

加入闲鱼,一起玩些酷的。(阿里巴巴集团闲鱼官方技术号,欢迎同道者技术交流。) 简历投递:guicai.gxy@alibaba-inc.com

工程Flutter
2