Flutter对于开发者来说一个很大的亮点就是它的热重载功能。热重载能帮助我们在无需重启整个应用的情况下快速调整UI,新增功能,修复bug等,能很大程度的节省开发时间,提升效率。Flutter是怎么做到这样快速更新的呢?要理解热重载的实现机制,我们先来了解下Flutter的编译模式。

Flutter编译模式

编译模式主要分为两大类:JIT和AOT。

  • JIT:Just in time,即时编译,程序执行过程中边翻译边运行,启动速度快,但执行效率相对于AOT会低一些。

  • AOT:Ahead of time,提前编译,程序在执行前全部被翻译成机器码,减少了运行时的性能损耗,运行更流畅。

Flutter结合了这两种编译方式,开发阶段采用JIT,Release阶段采用AOT。开发阶段边翻译边运行的方式正是热重载功能的前提,运行过程中将有变更的代码文件注入到Dart虚拟机中,虚拟机加载新的文件后,触发重建widget树,从而使开发者能快速看到更新后的效果。
接下来我们结合flutter tools源码深入了解下热重载机制:

Flutter tools

Flutter tools 本质上是一个Dart程序,位于path/to/flutter/packages/flutter_tools路径下,它的入口函数是flutter_tools.dart的main方法:

import 'package:flutter_tools/executable.dart' as executable;void main(List<String> args) {  executable.main(args);}

executable.main(args)中组装了很多种类型的FlutterCommand:

await runner.run(args, [    AnalyzeCommand(verboseHelp: verboseHelp),    AssembleCommand(),    AttachCommand(verboseHelp: verboseHelp),    BuildCommand(verboseHelp: verboseHelp),    ......    RunCommand(verboseHelp: verboseHelp),     ......    UpgradeCommand(),    VersionCommand(),  ], ......}

然后在CommandRunner的

Future runCommand(ArgResults topLevelResults)

方法中根据条件遍历执行这些FlutterCommand的runCommand方法,直到有FlutterCommand处理此次命令。重点了解一下RunCommand,RunCommand处理flutter run命令,在它的runCommond方法中会根据hotMode来判断创建HotRunner或ColdRunner(如果不强制指定--no-hot,在debug模式下默认是启用hotMode的),然后执行所创建的HotRunner/ColdRunner的run方法。
要了解HotRunner,我们先来了解几个类的主要功能:

  • VMService:到设备Dart VM的一个连接

  • _DevFSHttpWriter:通过VMService给出的httpAddress地址,负责向设备指定目录写文件

  • ResidengCompiler:负责文件编译,生成CompilerOutput对象

  • DevFS:使用ResidengCompiler编译生成变更代码(新增,删除,修改)的结果文件,然后通过_DevFSHttpWriter同步到device设备上。

为更直观的了解代码流程,结合场景断点查看(flutter tools的详细断点方法可参见文末附录)。
场景:

  1. 创建测试工程t_flutter_app,效果如下:ba5eb3b8532039ac74b3e2377caaf919.png

  2. 简单修改工程中main.dart的代码 Text('You have pushed the button this many times:',) 修改为Text('You have pushed the button this many times 123:',)

控制台输入r后查看devFS值:b9e7d66fab2d0cf1c2218f1fbab00b8e.png
_baseUri为设备上对应的目录地址。rootDirectory为项目本地代码路径,sources为监听变更的代码文件集合,_httpWriter中的httpAddress为连接到设备的地址,值得一提的是,我们常用的Observatory功能也是基于这个地址94df8e56efe98a9dd1a0881169d5d929.png

那变更代码集合是怎么生成的呢,跟踪到flutter_tools/lib/src/run_hot.dart中的_updateDevFS方法,看相关代码:

final List invalidatedFiles = ProjectFileInvalidator.findInvalidated(      lastCompiled: flutterDevices[0].devFS.lastCompiled,      urisToMonitor: flutterDevices[0].devFS.sources,      packagesPath: packagesFilePath,    );

监听的文件集合就是上面提到的devFS.sources。ProjectFileInvalidator.findInvalidated方法检测文件变更,其判断条件为

updatedAt.millisecondsSinceEpoch > lastCompiled.millisecondsSinceEpoch

,即在上次编译之后文件有更新。通过该方法得到List invalidatedFiles也就是变更代码集合,此场景下仅有一个文件变更,Uri为file:///Users/heq/workspace/t_flutter_app/lib/main.dart。
找到变更代码集合后,compile生成CompileOutput对象740290660b9133978d5cc20726d6886f.png
可以看到这个输出文件路径为 t_flutter_app/build/app.dill.incremental.dill。然后包装这个CompileOutput对象到Map dirtyEntries中,并通过_httpWriter将数据同步到设备上,查看dirtyEntries内容:fcc73771759df2b5ba1e87cf6ccebefe.png
可以发现本地的t_flutter_app/build/app.dill.incremental.dill和设备上的/data/data/com.example.t_flutter_app/code_cache/t_flutter_appLAGDOT/t_flutter_app/lib/main.dart.incremental.dill是匹配的。

.dill文件内容可通过strings命令查看
如 strings app.dill.incremental.dill

Dart VM 收到增量Kernel文件重载成功后,触发flutter widgets树的重建,这里主要是触发Element元素的reassemble方法。

abstract class Element extends DiagnosticableTree implements BuildContext {  ......  @protected  void reassemble() {    markNeedsBuild();    visitChildren((Element child) {      child.reassemble();    });  }}

可简单看下此时reassemble的调用栈005c548d99f4476202954c00c9f12e75.png

即 BindingBase.reassembleApplication ->WidgetsBinding.performReassemble ->BuildOwner.reassemble ->Element.reassemble 触发widget树的更新

热重载整体流程

总结上述内容,热重载基本流程为:

  1. ProjectFileInvalidator.findInvalidated扫描项目工程文件,找到变更(新增,删除,修改)文件集合存储到List invalidatedFiles

  2. 使用generator.recompile编译invalidatedFiles,生成增量Dart Kernel文件app.dill.incremental.dill

  3. 通过_DevFSHttpWriter将增量Dart Kernel文件发送给设备Dart VM

  4. Dart VM收到增量Dart Kernel文件后,与之前的Dart Kernel文件进行合并,然后重新加载新的Dart Kernel文件

  5. 重载成功后,触发flutter widgets树的重建

基本流程如下图:d38b6abd30c9b6c96ea1d6202ed92f74.png

热重载无效场景

根据上面的分析,使用热重载时,Flutter应用并未重新启动,而只是触发重建了widget树,需要注意的是1)此时State状态会被保留 2)只会根据原来的根节点来重建Widget树。所以热重载在以下这些典型场景下不会生效:

  • 全局变量和静态属性的更改

  • main中代码逻辑更改,如:

void main(){    //新增代码逻辑    runApp(MyApp());}
  • 修改Widget树的根节点,如:

void main(){  runApp(MyApp());  //变更为  runApp(const Center(      child: const Text('Hello', textDirection: TextDirection.ltr)));}
  • widget的Stateless和Stateful状态变化,如:

class MyWidget extends StatelessWidge//变更为:class MyWidget extends StatefulWidget
  • initState中的代码逻辑更改

更详细的说明可参考官方文档 Hot reload(https://flutter.dev/docs/development/tools/hot-reload)
针对上面一些hot reload不生效的场景,就需要使用hot restart。

hot reload和hot restart区别

hot reload和hot restart对应到了AndroidStudio工具栏上的两个按钮3debb3fc8c5e52c04d7cde889b18cf4b.png
另外,通过flutter tools启动Flutter应用后控制台中有提示

To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".

表示输入r执行hot reload,输入R执行hot restart。

这里的不同输入对应到TerminalHandler中_commonTerminalInputHandler的不同执行流程。具体代码位于flutter_tools/lib/src/resident_runner.dart

Future<bool> _commonTerminalInputHandler(String character) async {    switch(character) {      case 'r':       ......        final OperationResult result = await residentRunner.restart(fullRestart: false);        ......      case 'R':       ......        final OperationResult result = await residentRunner.restart(fullRestart: true);        ......    }    return false;  }

可以看到r和R后续的处理流程差异其实就是residentRunner.restart方法的fullRestart参数值差别,更具体的后续执行流程可自行查看代码。
hot reload和hot restart的主要区别有:

  1. hot reload保留了state状态,而hot restart会重置所有state状态

  2. hot reload执行generator.recompile方法的输出文件为build/app.dill.incremental.dill,而hot restart的输出文件为build/app.dill

  3. hot restart比hot reload花费更多的时间

附1:flutter tools打印日志

flutter tools源码中会有很多关键节点打印日志的地方,如:

printTrace('Sending reload events to ${device.device.name}');

这些日志能帮我们快速了解执行流程。但默认在控制台中是看不见这些日志输出的,需要做些简单修改。
flutter tools中的默认文件系统初始化位于flutter_tools/lib/src/context_runner.dart文件的runInContext方法中:

Logger: () => platform.isWindows ? WindowsStdoutLogger() : StdoutLogger()

非windows系统下默认使用StdoutLogger,所以根据需要修改StdoutLogger方法便可打印出flutter tools的关键日志,如:

  @override  void printTrace(String message) {    //增加log打印    print('[FlutterTools] $message');  }

即可在控制台查看flutter tools相关日志,类似如下:

[FlutterTools] DevFS: Sync finished 3,071ms (!)[FlutterTools] Synced 0.9MB.[FlutterTools] Sending to VM service: _flutter.listViews({})

另外,如果要使修改后的flutter tools在以后执行flutter命令时生效,只要删除path/to/flutter/bin/cache目录下的flutter_tools.snapshot文件,然后重新执行flutter命令即可生成新的flutter_tools工具相关产物。

附2:flutter tools的调试

  1. 创建flutter测试工程,这里工程名为 t_flutter_app

  2. AndroidStudio打开flutter_tools工程,目录地址 path/to/flutter/packages/flutter_tools

  3. 在flutter_tools工程中所需位置设上断点711e8b0e12e10678a5bad8afdf15d909.png

  4. 添加Debug Configurationd5afc7871f61b7ad5a28ecdf290fc7f3.pngcedd5cf4efdda8ec160b1eac5db8804e.png

  5. debug方式运行flutter_tools工程,待t_flutter_app启动后,控制台显示51592478e01b70ea26310ee93df0cc37.png

  6. 修改t_flutter_app部分代码后,控制台输入 r,等待代码执行到断点位置,即可看到断点成功675faccb55e1132317343a526a0717e6.png

关于Flutter的热重载机制相关内容就介绍到这里,如有错误疏漏之处,烦请指正。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐