做过Flutter的同学都知道,Flutter支持 AOT(Ahead of time)和 JIT(Just in time) 两种编译模式。
AOT模式是运行前预先编译好的,运行过程中不需要进行分析,编译,此模式的运行速度是最快的。
JIT模式在运行过程中支持热重载Hot Reload,Flutter执行Hot Reload是一个增量的过程,由系统对本次的代码和上次的代码进行比对,找出差异性文件,然后将文件上传到DartVM中进行更新。
Flutter同时采用两种方案,在开发阶段采用JIT模式开发,在release阶段采用AOT模式,将代码打包成二进制进行发布。
Flutter支持JIT,那么理论上来说是可以支持热更新的,通过服务器来下发代码。但是这样会引起性能消耗,还有苹果禁止使用热更新,Flutter并没有采用热更新技术。
通过这篇文章,我们一起来探索一下Flutter热重载的实现。
1.热重载挂载
打开Flutter SDK目录下的packages/flutter_tools,将该工程拖至Android studio打开。
然后将工程挂载到需要调试的项目中
另外,如果没有指定flutter sdk,请指定sdk路径。
至此,挂载完成,接下来我们开始断点调试。
2.断点调试
程序入口在flutter_tools.dart的main函数
开始我们第一个断点
main里面就一个executable.main函数,点进去,我们可以看到main函数的代码,可以看到,main函数前面在做一些指令分析以及参数分析,这里不做过多介绍,重点是看下面的异步方法runner.run,不要再点进去断点调试了,因为这个方法都是回调,在generateCommands方法中已经调用过很多关键的方法,这里需要耐心的动态断点调试跟静态分析,通过调试与分析,我们来看一下RunCommand方法。点击进去。
还是好多异步回调,然后还是各种继承,有点闹心,继续跟进吧,找HotRunner,找到createRunner方法下return了一个HotRunner类,下个断点,看他的执行情况。
通过代码的分析,我们找到核心的方法_reloadSources,其中有一个关键的执行步骤_updateDevFS
Future<OperationResult> _reloadSources({
String targetPlatform,
String sdkName,
bool emulator,
bool pause = false,
String reason,
void Function(String message) onSlow,
}) async {
final Map<FlutterDevice, List<FlutterView>> viewCache = <FlutterDevice, List<FlutterView>>{};
for (final FlutterDevice device in flutterDevices) {
final List<FlutterView> views = await device.vmService.getFlutterViews();
viewCache[device] = views;
for (final FlutterView view in views) {
if (view.uiIsolate == null) {
return OperationResult(2, 'Application isolate not found', fatal: true);
}
}
}
final Stopwatch reloadTimer = _stopwatchFactory.createStopwatch('reloadSources:reload')..start();
if (!(await hotRunnerConfig.setupHotReload())) {
return OperationResult(1, 'setupHotReload failed');
}
final Stopwatch devFSTimer = Stopwatch()..start();
UpdateFSReport updatedDevFS;
try {
updatedDevFS= await _updateDevFS();
} finally {
hotRunnerConfig.updateDevFSComplete();
}
// Record time it took to synchronize to DevFS.
bool shouldReportReloadTime = true;
_addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
if (!updatedDevFS.success) {
return OperationResult(1, 'DevFS synchronization failed');
}
String reloadMessage = 'Reloaded 0 libraries';
final Stopwatch reloadVMTimer = _stopwatchFactory.createStopwatch('reloadSources:vm')..start();
final Map<String, Object> firstReloadDetails = <String, Object>{};
if (updatedDevFS.invalidatedSourcesCount > 0) {
final OperationResult result = await _reloadSourcesHelper(
this,
flutterDevices,
pause,
firstReloadDetails,
targetPlatform,
sdkName,
emulator,
reason,
);
if (result.code != 0) {
return result;
}
reloadMessage = result.message;
} else {
_addBenchmarkData('hotReloadVMReloadMilliseconds', 0);
}
reloadVMTimer.stop();
await evictDirtyAssets();
final Stopwatch reassembleTimer = _stopwatchFactory.createStopwatch('reloadSources:reassemble')..start();
final ReassembleResult reassembleResult = await _reassembleHelper(
flutterDevices,
viewCache,
onSlow,
reloadMessage,
updatedDevFS.fastReassembleClassName,
);
shouldReportReloadTime = reassembleResult.shouldReportReloadTime;
if (reassembleResult.reassembleViews.isEmpty) {
return OperationResult(OperationResult.ok.code, reloadMessage);
}
// Record time it took for Flutter to reassemble the application.
reassembleTimer.stop();
_addBenchmarkData('hotReloadFlutterReassembleMilliseconds', reassembleTimer.elapsed.inMilliseconds);
reloadTimer.stop();
final Duration reloadDuration = reloadTimer.elapsed;
final int reloadInMs = reloadDuration.inMilliseconds;
// Collect stats that help understand scale of update for this hot reload request.
// For example, [syncedLibraryCount]/[finalLibraryCount] indicates how
// many libraries were affected by the hot reload request.
// Relation of [invalidatedSourcesCount] to [syncedLibraryCount] should help
// understand sync/transfer "overhead" of updating this number of source files.
HotEvent('reload',
targetPlatform: targetPlatform,
sdkName: sdkName,
emulator: emulator,
fullRestart: false,
reason: reason,
overallTimeInMs: reloadInMs,
finalLibraryCount: firstReloadDetails['finalLibraryCount'] as int ?? 0,
syncedLibraryCount: firstReloadDetails['receivedLibraryCount'] as int ?? 0,
syncedClassesCount: firstReloadDetails['receivedClassesCount'] as int ?? 0,
syncedProceduresCount: firstReloadDetails['receivedProceduresCount'] as int ?? 0,
syncedBytes: updatedDevFS.syncedBytes,
invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
transferTimeInMs: updatedDevFS.transferDuration.inMilliseconds,
fastReassemble: featureFlags.isSingleWidgetReloadEnabled
? updatedDevFS.fastReassembleClassName != null
: null,
compileTimeInMs: updatedDevFS.compileDuration.inMilliseconds,
findInvalidatedTimeInMs: updatedDevFS.findInvalidatedDuration.inMilliseconds,
scannedSourcesCount: updatedDevFS.scannedSourcesCount,
reassembleTimeInMs: reassembleTimer.elapsed.inMilliseconds,
reloadVMTimeInMs: reloadVMTimer.elapsed.inMilliseconds,
).send();
if (shouldReportReloadTime) {
globals.printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadDuration)}.');
// Record complete time it took for the reload.
_addBenchmarkData('hotReloadMillisecondsToFrame', reloadInMs);
}
// Only report timings if we reloaded a single view without any errors.
if ((reassembleResult.reassembleViews.length == 1) && !reassembleResult.failedReassemble && shouldReportReloadTime) {
globals.flutterUsage.sendTiming('hot', 'reload', reloadDuration);
}
return OperationResult(
reassembleResult.failedReassemble ? 1 : OperationResult.ok.code,
reloadMessage,
);
}
我们点进去跟进一下,可以看到他在遍历设备去更新
继续点进去
这里我们看到content是一个路径,我们不妨去找到它
我们来用终端看他里面是什么东西
是咱们main.dart,我们在来去别的文件夹改变一下代码再来看看,r执行热重载
变了,真的变了,这个文件就是我们刚才改过的文件,而且只有我们改过的文件(可以多改几个文件试试),这证明了flutter热重载是在找到被修改的文件,去增量更新。我们继续向下看代码。
_httpWriter是虚拟机的地址,这里将增量的文件上传到了DartVM虚拟机,然后DartVM利用RPC协议(底层是sockt)调用reloadsource进行资源重载,有兴趣的同学可以再点进去看看,这里就不过多介绍了。
到这里就已经跟不下去了,已经传输到了虚拟机,然后虚拟机会发送给Flutter引擎,这里我们利用flutter引擎(引擎源码下载以及转译较为繁琐,小编有空会为大家更新)挂载去调试一下看看。
挂载Flutter引擎调试
打开挂载工程iOS的Runner
再回到flutter_tools去配置一下,run --local-engine-src-path /Users/wangmeng/Desktop/gogogo/engine_download/src --local-engine=ios_debug_sim_unopt
运行,xcode添加调试现有工程,Debug -> Attach to process,选择你的工程,直接下断点,br set -n "IsolateGroupReloadContext::Reload"
下断点成功,那么我们直接去flutter_tools执行热重载(记得去更改一下代码,不然断点不会走)
果然来到了这里,flutter引擎接收到了增量文件,触发widgets的重新建立布局绘制(),服务触发后,BindingBase.reassembleApplication-> WidgetsBinding. performReassemble -> BuildOwner.reassemble -> Element.reassemble 由根节点开始一步步实现widgets树重建。
总结
Flutter在执行热重载的时候,首先会将工程文件逐一扫描,检查是否有删除/新增等改动,扫描完成后生成pp.dill.incremental.dill文件,通过http端口发送给Dart虚拟机,资源重载(RPC协议),重载成功之后,将FlutterDevice UI线程重置,通过RPC接口,触发flutter widgets树重建、重绘。
调试的篇幅较长,笔者只截取了自认为重点的部分,有不当之处,希望给笔者及时指正,或者私信我,共同探索flutter原理。