赞
踩
目录
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地址、设备中增量文件写入路径等
attach 或者run Flutter工程时flutter_tools会运行以下命令compile当前Flutter工程并将文件列表并缓存在flutterDevice.devFs.sources中,hotReload时依据sources中的文件列表逐个判断每个文件是否有更改。每次horReload时会重新运行以下命令,并更新flutterDevice.sources列表。
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;
- }
- }
- ......
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
增量的dill文件通过http(deice->devfs中有记录http地址)发送至device
- // devfs.dart
- Future<UpdateFSReport> update(...) async {
- //推送更新至设备
- if (dirtyEntries.isNotEmpty) {
- await (devFSWriter ?? _httpWriter).write(dirtyEntries, _baseUri!, _httpWriter);
- }
- }
推送更新成功后,如果有更新的文件,会通过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;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
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;
-
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
- //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_);
- ......
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
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文件的合并。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。