目录
HotReload是指,在不中断 App 正常运行的情况下,动态注入修改后的代码片段,不需要重新编译、Install App。Flutter HotReload只能在 Debug 模式下使用,是因为 Debug 模式下,Flutter 采用的是 JIT(即时编译或运行时编译) 编译, Release 模式下采用的是 AOT(提前编译或运行前编译) 静态编译。
本文详解Flutter HotReload原理,并回答以下几个问题:
1、混合栈开发的情况下,dart代码通常会被打成平台包(Android:aar,Ios:FrameWork)App进行依赖,此时可以hotReload吗?
2、Flutter HotReload时的增量文件生成的依据是什么?多次HotReload时每次增量文件的生成依赖的第一次还是上一次?
3、为什么很多场景下HotReload不会生效?重启App,再次Attach时为什么之前HotReload的文件没有生效?
4、为什么增量文件中是被修改的dart文件整个文件的内容而不是只有diff内容?
一、原理
Flutter HotReload主要有两大部分:
第一部分:生成增量文件并推送。主要流程: 改动文件的扫描 -> 编译生成增量文件 -> 推送更新。在开发环境上(比如PC)由flutter_tools来完成。
第二部分:编译增量文件,重建widget树。主要流程: 增量文件编译 ->Widget 重建。 在运行flutter项目的device上,由Dart VM和Flutter Engine来完成。
1、扫描工程代码改动。热重载模块会逐一扫描工程中的文件,检查是否有新增、删除或者改动,直到找到在上次编译之后,发生变化的 Dart 代码。
对比的依据是上次编译时的工程文件列表,文件路径缓存在flutterDevice.sources中。attach或者run flutter工程时flutter_tools会运行dart命令来获取初时的列表,后续每次hotRload时会更新文件flutterDevice.sources。 是否改动的算法是:根据lastCompiled时间及每个文件的modified时间来判断。
2、编译生成增量文件。
所有改动的文件会被编译生成app.dill.incremental.dill文件,mac上app.dill.incremental.dill文件可以通过strings 命令查看,其中记录了改动文件的路径以及文件内容(整个文件不是diff)。
3、推送更新。
flutter_tools将增量的 Dart Kernel 文件通过 HTTP 端口,发送给正在移动设备上运行的 Dart VM。
4、触发reload
flutter_tools通过vm service与dart vm进行通信。推送更新成功后会触发reloadSources服务通知Dart Vm.
5、编译增量文件。
Dart VM监听到reloadSources的通知后会加载增量文件,并编译。
6、Widget 重建。
Flutter Framework会注册reassemble服务。Dart VM 资源加载成功后,会通过VM Service 通知 Flutter Framework 重建 Widget。
二、源码详解
flutter_tools中使用flutterDevice来记录每个正在attach的设备,通过vmService来与Dart Vm进行通信。flutterDevice中记录了flutter工程的所有源文件路径、Flutter工程上次编译的时间、vmService地址、设备中增量文件写入路径等
2.1 扫描代码改动
attach 或者run Flutter工程时flutter_tools会运行以下命令compile当前Flutter工程并将文件列表并缓存在flutterDevice.devFs.sources中,hotReload时依据sources中的文件列表逐个判断每个文件是否有更改。每次horReload时会重新运行以下命令,并更新flutterDevice.sources列表。
2.2 编译生成增量文件
hot reload时会判断flutterDevice.devFs.sources中的所有文件是否有更新,来获取更改的文件列表。其中根据工程的上次编译时间lastCompiled以及每个文件的modified时间来判断文件是否有更新。
//run_hot.dart
//urisToScan为flutter工程中的文件列表
Future<InvalidationResult> findInvalidated(...) async {
...
for (final Uri uri in urisToScan) {
final DateTime updatedAt = uri.hasScheme && uri.scheme != 'file'
? _fileSystem.file(uri).statSync().modified
: _fileSystem.statSync(uri.toFilePath(windows: _platform.isWindows)).modified;
if (updatedAt != null && updatedAt.isAfter(lastCompiled)) {
invalidatedFiles.add(uri);
}
}
...
};
recompile生成main.dill.incremental.dill文件。
Future<UpdateFSReport> update(...) async {
...
//重新编译
final Future<CompilerOutput?> pendingCompilerOutput = generator.recompile(
mainUri,
invalidatedFiles,
outputPath: dillOutputPath,
fs: _fileSystem,
projectRootPath: projectRootPath,
packageConfig: packageConfig,
checkDartPluginRegistry: true, // The entry point is assumed not to have changed.
).then((CompilerOutput? result) {
compileTimer.stop();
return result;
});
final CompilerOutput? compilerOutput = await pendingCompilerOutput;
_previousCompiled = lastCompiled;
lastCompiled = candidateCompileTime;
// list of sources that needs to be monitored are in [compilerOutput.sources]
//下次hotreload会依据sources来进行比较
sources = compilerOutput.sources;
//
// Don't send full kernel file that would overwrite what VM already
// started loading from.
if (!bundleFirstUpload) {
final String compiledBinary = compilerOutput.outputFilename;
if (compiledBinary.isNotEmpty) {
final Uri entryUri = _fileSystem.path.toUri(pathToReload);
final DevFSFileContent content = DevFSFileContent(_fileSystem.file(compiledBinary));
syncedBytes += content.size;
dirtyEntries[entryUri] = content;
}
}
......
}
2.3 推送更新
增量的dill文件通过http(deice->devfs中有记录http地址)发送至device
// devfs.dart
Future<UpdateFSReport> update(...) async {
//推送更新至设备
if (dirtyEntries.isNotEmpty) {
await (devFSWriter ?? _httpWriter).write(dirtyEntries, _baseUri!, _httpWriter);
}
}
2.4 触发reload
推送更新成功后,如果有更新的文件,会通过VM Service通知dart vm reloadSources。其中发送的数据格式为json。
//run_hot.dart
Future<List<Future<vm_service.ReloadReport>>> _reloadDeviceSources(
FlutterDevice device,
String entryPath, {
bool pause = false,
}) async {
final String deviceEntryUri = device.devFS.baseUri
.resolve(entryPath).toString();
final vm_service.VM vm = await device.vmService.service.getVM();
return <Future<vm_service.ReloadReport>>[
for (final vm_service.IsolateRef isolateRef in vm.isolates)
//调用vm_service中的reloadSources
device.vmService.service.reloadSources(
isolateRef.id,
pause: pause,
rootLibUri: deviceEntryUri,
)
];
}
//vm_service.dart
Future<ReloadReport> reloadSources(
String isolateId, {
bool? force,
bool? pause,
String? rootLibUri,
String? packagesUri,
}) =>
_call('reloadSources', {
'isolateId': isolateId,
if (force != null) 'force': force,
if (pause != null) 'pause': pause,
//file:///data/user/0/com.xxx.xxx/code_cache/dartmodulexxx/dartmodule/main.dart.incremental.dill
if (rootLibUri != null) 'rootLibUri': rootLibUri,
if (packagesUri != null) 'packagesUri': packagesUri,
});
Future<T> _call<T>(String method, [Map args = const {}]) async {
final request = _OutstandingRequest(method);
_outstandingRequests[request.id] = request;
Map m = {
'jsonrpc': '2.0',
'id': request.id,
'method': method,
'params': args,
};
String message = jsonEncode(m);
_onSend.add(message);
_writeMessage(message);
return await request.future as T;
}
Dart Vm中注册了reloadSources的service。收到“reloadSources”时会调用ReloadSources()函数,最终在IsolateGroupReloadContext::Reload()函数中加载增量文件并编译。
//runtime/vm/service.cc
static const ServiceMethodDescriptor service_methods_[] = {
......
{ "invoke", Invoke, invoke_params },
{ "kill", Kill, kill_params },
{ "pause", Pause,
pause_params },
{ "reloadSources", ReloadSources,
reload_sources_params },
{ "_reloadSources", ReloadSources,
reload_sources_params },
......
};
static void ReloadSources(Thread* thread, JSONStream* js) {
......
IsolateGroup* isolate_group = thread->isolate_group();
if (isolate_group->IsReloading()) {
return;
}
isolate_group->ReloadSources(js, force_reload, js->LookupParam("rootLibUri"),
js->LookupParam("packagesUri"));
......
}
//runtime/vm/isolate.cc
bool IsolateGroup::ReloadSources(JSONStream* js,
bool force_reload,
const char* root_script_url,
const char* packages_url,
bool dont_delete_reload_context) {
......
std::shared_ptr<IsolateGroupReloadContext> group_reload_context(
new IsolateGroupReloadContext(this, shared_class_table, js));
group_reload_context_ = group_reload_context;
const bool success =
group_reload_context_->Reload(force_reload, root_script_url, packages_url,
/*kernel_buffer=*/nullptr,
/*kernel_buffer_size=*/0);
return success;
}
2.5 加载编译增量文件
//runtime/vm/isolate.cc
//root_script_url为dill文件路径
bool IsolateGroupReloadContext::Reload(bool force_reload,
const char* root_script_url,
const char* packages_url,
const uint8_t* kernel_buffer,
intptr_t kernel_buffer_size) {
// Load the kernel program and figure out the modified libraries.
//加载增量文件
intptr_t* p_num_received_classes = nullptr;
intptr_t* p_num_received_procedures = nullptr;
// ReadKernelFromFile checks to see if the file at
// root_script_url is a valid .dill file. If that's the case, a Program*
// is returned. Otherwise, this is likely a source file that needs to be
// compiled, so ReadKernelFromFile returns NULL.
kernel_program = kernel::Program::ReadFromFile(root_script_url);
if (kernel_program != nullptr) {
num_received_libs_ = kernel_program->library_count();
bytes_received_libs_ = kernel_program->kernel_data_size();
p_num_received_classes = &num_received_classes_;
p_num_received_procedures = &num_received_procedures_;
}
......
//编译增量文件
IsolateGroupSource* source = IsolateGroup::Current()->source();
source->add_loaded_blob(Z, external_typed_data);
modified_libs_ = new (Z) BitVector(Z, num_old_libs_);
kernel::KernelLoader::FindModifiedLibraries(
kernel_program.get(), IG, modified_libs_, force_reload, &skip_reload,
p_num_received_classes, p_num_received_procedures);
modified_libs_transitive_ = new (Z) BitVector(Z, num_old_libs_);
BuildModifiedLibrariesClosure(modified_libs_);
......
}
2.6 WidgetsTree重建
Flutter framework中BindingBase注册了名为reassemble的Dart VM服务,用于外部与正在运行的Dart VM通信,能够触发根节点树重建操作。
服务触发后,BindingBase.reassembleApplication-> WidgetsBinding. performReassemble -> BuildOwner.reassemble -> Element.reassemble 由根节点开始一步步实现widgets树重建。
//src/foundation/binding.dart
void initServiceExtensions() {
//注册reassemble服务
registerSignalServiceExtension(
name: 'reassemble',
callback: reassembleApplication,
);
}
Future<void> reassembleApplication() {
//执行performReassemble
return lockEvents(performReassemble);
}
三、开始问题结论
1、混合栈开发的情况下,dart代码通常会被打成平台包(Android:aar,Ios:FrameWork)App进行依赖,此时可以hotReload吗?
可以。有运行的dart vm即可以attach flutter工程成功。但是hotreload时的增量文件生成是依赖与开发机器上的dart工程代码,hot reload可能会不成功。比如aar是版本1,attach时本地代码是版本2,hotreload时的代码时版本3。horrelaod生成的main.dill.incremental.dill文件中包含的时版本3与版本2的差异文件的内容。
2、Flutter HotReload时的增量文件生成的依据是什么?多次HotReload时每次增量文件的生成依赖的第一次还是上一次?
attach或者run flutter app时编译时的文件列表。上一次。
3、重启App,再次Attach时为什么之前HotReload的文件没有生效?
app.dill文件与main.dill.incremental.dill文件只会在内存中进行合并,不会在dill文件中进行合并,再次启动时加载的是main.dill.incremental.dill文件。
4、为什么增量文件中是被修改的dart文件整个文件的内容而不是只有diff内容?
dart vm会加载并编译main.dill.incremental.dill增量文件,然后重新构建widget。不会进行dill文件与main.dill.incremental.dill文件的合并。