Flutter HotRealod详解

        

目录

一、原理

二、源码详解

2.1 扫描代码改动

2.2 编译生成增量文件

2.3 推送更新

2.4 触发reload

2.5 加载编译增量文件

2.6 WidgetsTree重建

三、开始问题结论


        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文件的合并。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值