热重载原理研究和探索

1. 前言


Flutter 亚秒级别的热重载是开发者的神兵利器,它能够提供我们快速修改UI、增加功能、修复bug,不需要重新启动应用,就可以看到改动效果。那么Flutter是如何做到这样的功能的呢?下面我们一起探究一下其中的原理。

2. 首先需要知道JIT 和AOT


JIT

JIT(Just In Time),指的是运行时编译,Flutter 在debug模式下采用这种方式,在运行时动态下发和执行代码,启动速度快,但是由于在运行时编译,性能会受影响。

img
AOT

AOT(Ahead Of Time),指的是在运行之前进行编译,Flutter 在Release模式下采用,可以为特定的平台生成稳定的二进制代码、执行性能好、运行速度快,但是每次都需要重新运行编译,开发效率低。

img

所以,Flutter提供的两种编译模式中,AOT 是静态编译,编译成设备直接可执行的二进制代码;而JIT 则是先生成中间代码(Script snapshot),然后通过dart Vm 解释执行。

3. Hot Reload 原理


  1. 扫描修改的文件;
  2. 生成kernal file(app.dill.incremental.dill);
  3. 将文件通过Http协议下发到dartVM;
  4. VM服务通过RPC 调用_reloadSources,进行资源重载;
  5. VM 资源加载成功,将FlutterDevice UI线程重置(uiIsolate),通过RPC调用,触发Flutter树的重建、重绘。

如下图形象说明:

[图片上传失败...(image-1e9489-1597131036977)]

4. 源码分析


​ 通过命令行输入r或者触发闪电按钮,其实会触发flutter_tools的run_hot文件的HotRunner的restart() 方法(位于flutter/pakages/flutter_tools/);

​ flutter_tools 调试步骤:

​ 1)用AS打开Flutter_tools,打开 Run/Debug Configurations; [图片上传失败...(image-b2e623-1597131036977)]

​ 2) 添加Dart Command Line App, Name 设置为Flutter Tools Debugger, Dart file 设置为flutter_tools的文件目录,Working directory 设置为一个测试项目路径

​ 3) 运行Debug,运行完成后,断点打在HotRunner 的restart()方法处。

​ 4)随便修改测试项目的Widget,在flutter_tools 工程的Console输入r回车;发现断点断在了restart方法。下面我们从restart()方法开始逐步分析,热重载的原理。

  1. restart()方法
@override
Future<OperationResult> restart({
  bool fullRestart = false,
  bool pauseAfterRestart = false,
  String reason,
  bool benchmarkMode = false
}) async {
  String targetPlatform;
  String sdkName;
  bool emulator;
  // 判断当前的设备,并且获取当前设备的targetPlatform、sdkName、emulator
  if (flutterDevices.length == 1) {
    final Device device = flutterDevices.first.device;
    targetPlatform = getNameForTargetPlatform(await device.targetPlatform);
    sdkName = await device.sdkNameAndVersion;
    emulator = await device.isLocalEmulator;
  } else if (flutterDevices.length > 1) {
    targetPlatform = 'multiple';
    sdkName = 'multiple';
    emulator = false;
  } else {
    targetPlatform = 'unknown';
    sdkName = 'unknown';
    emulator = false;
  }
  final Stopwatch timer = Stopwatch()..start();
  if (fullRestart) {
        ... 完全重新启动、非热重载部分省略
      return;
  }
  final OperationResult result = await _hotReloadHelper(
    targetPlatform: targetPlatform,
    sdkName: sdkName,
    emulator: emulator,
    reason: reason,
    pauseAfterRestart: pauseAfterRestart,
  );
  if (result.isOk) {
    final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
    printStatus('${result.message} in $elapsed.');
  }
  return result;
}

​ 调用到restart方法之后,会获取当前设备的一些信息,然后根据是否是fullRestart执行不同流程,我们研究的是Hot reload不是fullRestart所以我们走的是下边的流程,执行_hotReloadHelper();

  1. _hotReloadHelper

     Future<OperationResult> _hotReloadHelper({
        String targetPlatform,
        String sdkName,
        bool emulator,
        String reason,
        bool pauseAfterRestart = false,
      }) async {
        final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
        final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
        Status status = logger.startProgress(
          '$progressPrefix hot reload...',
          timeout: timeoutConfiguration.fastOperation,
          progressId: 'hot.reload',
        );
        OperationResult result;
        try {
          result = await _reloadSources(
            targetPlatform: targetPlatform,
            sdkName: sdkName,
            emulator: emulator,
            pause: pauseAfterRestart,
            reason: reason,
            onSlow: (String message) {
              status?.cancel();
              status = logger.startProgress(
                message,
                timeout: timeoutConfiguration.slowOperation,
                progressId: 'hot.reload',
              );
            },
          );
        } on rpc.RpcException {
          HotEvent('exception',
            targetPlatform: targetPlatform,
            sdkName: sdkName,
            emulator: emulator,
            fullRestart: false,
            reason: reason).send();
          return OperationResult(1, 'hot reload failed to complete', fatal: true);
        } finally {
          status.cancel();
        }
        return result;
      }
    

    这个方法呢,其实也是对于核心逻辑方法_reloadSources()方法的封装。

  2. _reloadSources()

     Future<OperationResult> _reloadSources({
        String targetPlatform,
        String sdkName,
        bool emulator,
        bool pause = false,
        String reason,
        void Function(String message) onSlow
      }) async {
       // 遍历所有Device的FlutterView, 并且uiIsolate 不存在的话,执行失败
        for (FlutterDevice device in flutterDevices) {
          for (FlutterView view in device.views) {
            if (view.uiIsolate == null) {
              return OperationResult(2, 'Application isolate not found', fatal: true);
            }
          }
        }
        bool shouldReportReloadTime = !_runningFromSnapshot;
       // 开启计时器
        final Stopwatch reloadTimer = Stopwatch()..start();
            // 判断FlutterView的uiIsolate是否为null或者处于暂停状态,如果是刷新所有view
        if (!_isPaused()) {
          printTrace('Refreshing active FlutterViews before reloading.');
          // 触发所有设备的vmService RPC 发送_flutter.listViews消息
          await refreshViews();
        }
    
        final Stopwatch devFSTimer = Stopwatch()..start();
        // 同步修改的文件
        final UpdateFSReport updatedDevFS = await _updateDevFS();
        // Record time it took to synchronize to DevFS.
        _addBenchmarkData('hotReloadDevFSSyncMilliseconds', devFSTimer.elapsed.inMilliseconds);
        if (!updatedDevFS.success) {
          return OperationResult(1, 'DevFS synchronization failed');
        }
        String reloadMessage;
        final Stopwatch vmReloadTimer = Stopwatch()..start();
        Map<String, dynamic> firstReloadDetails;
        try {
          final String entryPath = fs.path.relative(
            getReloadPath(fullRestart: false),
            from: projectRootPath,
          );
          final List<Future<DeviceReloadReport>> allReportsFutures = <Future<DeviceReloadReport>>[];
          for (FlutterDevice device in flutterDevices) {
            if (_runningFromSnapshot) {
              // Asset directory has to be set only once when we switch from
              // running from snapshot to running from uploaded files.
              await device.resetAssetDirectory();
            }
            final Completer<DeviceReloadReport> completer = Completer<DeviceReloadReport>();
            allReportsFutures.add(completer.future);
            // 触发RPC 调用_reloadSources 重新加载资源
            final List<Future<Map<String, dynamic>>> reportFutures = device.reloadSources(
              entryPath, pause: pause,
            );
            // 处理RPC 调用_reloadSources 返回结果
            unawaited(Future.wait(reportFutures).then(
              (List<Map<String, dynamic>> reports) async {
                // TODO(aam): Investigate why we are validating only first reload report,
                // which seems to be current behavior
                final Map<String, dynamic> firstReport = reports.first;
                // Don't print errors because they will be printed further down when
                // `validateReloadReport` is called again.
                await device.updateReloadStatus(
                  validateReloadReport(firstReport, printErrors: false),
                );
                completer.complete(DeviceReloadReport(device, reports));
              },
            ));
          }
          final List<DeviceReloadReport> reports = await Future.wait(allReportsFutures);
          for (DeviceReloadReport report in reports) {
            final Map<String, dynamic> reloadReport = report.reports[0];
            if (!validateReloadReport(reloadReport)) {
              // Reload failed.
              HotEvent('reload-reject',
                targetPlatform: targetPlatform,
                sdkName: sdkName,
                emulator: emulator,
                fullRestart: false,
                reason: reason,
              ).send();
              return OperationResult(1, 'Reload rejected');
            }
            // Collect stats only from the first device. If/when run -d all is
            // refactored, we'll probably need to send one hot reload/restart event
            // per device to analytics.
            firstReloadDetails ??= reloadReport['details'];
            final int loadedLibraryCount = reloadReport['details']['loadedLibraryCount'];
            final int finalLibraryCount = reloadReport['details']['finalLibraryCount'];
            printTrace('reloaded $loadedLibraryCount of $finalLibraryCount libraries');
            reloadMessage = 'Reloaded $loadedLibraryCount of $finalLibraryCount libraries';
          }
        } on Map<String, dynamic> catch (error, stackTrace) {
          printTrace('Hot reload failed: $error\n$stackTrace');
          final int errorCode = error['code'];
          String errorMessage = error['message'];
          if (errorCode == Isolate.kIsolateReloadBarred) {
            errorMessage = 'Unable to hot reload application due to an unrecoverable error in '
                           'the source code. Please address the error and then use "R" to '
                           'restart the app.\n'
                           '$errorMessage (error code: $errorCode)';
            HotEvent('reload-barred',
              targetPlatform: targetPlatform,
              sdkName: sdkName,
              emulator: emulator,
              fullRestart: false,
              reason: reason,
            ).send();
            return OperationResult(errorCode, errorMessage);
          }
          return OperationResult(errorCode, '$errorMessage (error code: $errorCode)');
        } catch (error, stackTrace) {
          printTrace('Hot reload failed: $error\n$stackTrace');
          return OperationResult(1, '$error');
        }
        // Record time it took for the VM to reload the sources.
        _addBenchmarkData('hotReloadVMReloadMilliseconds', vmReloadTimer.elapsed.inMilliseconds);
        final Stopwatch reassembleTimer = Stopwatch()..start();
        // Reload the isolate.
        final List<Future<void>> allDevices = <Future<void>>[];
       // 触发所有的FlutterView 的uiIsolate 刷新
        for (FlutterDevice device in flutterDevices) {
          printTrace('Sending reload events to ${device.device.name}');
          final List<Future<ServiceObject>> futuresViews = <Future<ServiceObject>>[];
          for (FlutterView view in device.views) {
            printTrace('Sending reload event to "${view.uiIsolate.name}"');
            futuresViews.add(view.uiIsolate.reload());
          }
          final Completer<void> deviceCompleter = Completer<void>();
          unawaited(Future.wait(futuresViews).whenComplete(() {
            deviceCompleter.complete(device.refreshViews());
          }));
          allDevices.add(deviceCompleter.future);
        }
        await Future.wait(allDevices);
        // We are now running from source.
        _runningFromSnapshot = false;
        // Check if any isolates are paused.
        final List<FlutterView> reassembleViews = <FlutterView>[];
        String serviceEventKind;
        int pausedIsolatesFound = 0;
        // 添加 ressembleViews
        for (FlutterDevice device in flutterDevices) {
          for (FlutterView view in device.views) {
            // Check if the isolate is paused, and if so, don't reassemble. Ignore the
            // PostPauseEvent event - the client requesting the pause will resume the app.
            final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
            if (pauseEvent != null && pauseEvent.isPauseEvent && pauseEvent.kind != ServiceEvent.kPausePostRequest) {
              pausedIsolatesFound += 1;
              if (serviceEventKind == null) {
                serviceEventKind = pauseEvent.kind;
              } else if (serviceEventKind != pauseEvent.kind) {
                serviceEventKind = ''; // many kinds
              }
            } else {
              reassembleViews.add(view);
            }
          }
        }
        if (pausedIsolatesFound > 0) {
          if (onSlow != null)
            onSlow('${_describePausedIsolates(pausedIsolatesFound, serviceEventKind)}; interface might not update.');
          if (reassembleViews.isEmpty) {
            printTrace('Skipping reassemble because all isolates are paused.');
            return OperationResult(OperationResult.ok.code, reloadMessage);
          }
        }
       //删除dirty 资源
        printTrace('Evicting dirty assets');
        await _evictDirtyAssets();
        assert(reassembleViews.isNotEmpty);
        printTrace('Reassembling application');
        bool failedReassemble = false;
        final List<Future<void>> futures = <Future<void>>[];
       // rpc触发所有的FlutterView uiIsolate ressemble
        for (FlutterView view in reassembleViews) {
          futures.add(() async {
            try {
              await view.uiIsolate.flutterReassemble();
            } catch (error) {
              failedReassemble = true;
              printError('Reassembling ${view.uiIsolate.name} failed: $error');
              return;
            }
          }());
        }
        final Future<void> reassembleFuture = Future.wait<void>(futures).then<void>((List<void> values) { });
        await reassembleFuture.timeout(
          const Duration(seconds: 2),
          onTimeout: () async {
            if (pausedIsolatesFound > 0) {
              shouldReportReloadTime = false;
              return; // probably no point waiting, they're probably deadlocked and we've already warned.
            }
            // Check if any isolate is newly paused.
            printTrace('This is taking a long time; will now check for paused isolates.');
            int postReloadPausedIsolatesFound = 0;
            String serviceEventKind;
            for (FlutterView view in reassembleViews) {
              await view.uiIsolate.reload();
              final ServiceEvent pauseEvent = view.uiIsolate.pauseEvent;
              if (pauseEvent != null && pauseEvent.isPauseEvent) {
                postReloadPausedIsolatesFound += 1;
                if (serviceEventKind == null) {
                  serviceEventKind = pauseEvent.kind;
                } else if (serviceEventKind != pauseEvent.kind) {
                  serviceEventKind = ''; // many kinds
                }
              }
            }
            printTrace('Found $postReloadPausedIsolatesFound newly paused isolate(s).');
            if (postReloadPausedIsolatesFound == 0) {
              await reassembleFuture; // must just be taking a long time... keep waiting!
              return;
            }
            shouldReportReloadTime = false;
            if (onSlow != null)
              onSlow('${_describePausedIsolates(postReloadPausedIsolatesFound, serviceEventKind)}.');
          },
        );
        // Record time it took for Flutter to reassemble the application.
        _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'],
          syncedLibraryCount: firstReloadDetails['receivedLibraryCount'],
          syncedClassesCount: firstReloadDetails['receivedClassesCount'],
          syncedProceduresCount: firstReloadDetails['receivedProceduresCount'],
          syncedBytes: updatedDevFS.syncedBytes,
          invalidatedSourcesCount: updatedDevFS.invalidatedSourcesCount,
          transferTimeInMs: devFSTimer.elapsed.inMilliseconds,
        ).send();
    
        if (shouldReportReloadTime) {
          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 ((reassembleViews.length == 1) && !failedReassemble && shouldReportReloadTime)
          flutterUsage.sendTiming('hot', 'reload', reloadDuration);
        return OperationResult(
          failedReassemble ? 1 : OperationResult.ok.code,
          reloadMessage,
        );
      }
    

​ 这个方法主要干了 5件事情

​ 1. 扫描修改的文件,生成dill文件,并且通过Http服务下发到设备资源文件;

2. RPC 调用_reloadSources 触发VM重新加载修改后的文件
3. RPC 调用 flutter View的uiIsolate refreshView
4. 删除dirty文件
5. rpc触发所有的FlutterView uiIsolate ressemble

5. 不会发生Hot Reload的场景


  1. 应用被杀死

  2. 编译错误

    当代码更改导致编译错误时,热重载会生成类似于以下内容的错误消息:

    Hot reload was rejected:
    '/Users/obiwan/Library/Developer/CoreSimulator/Devices/AC94F0FF-16F7-46C8-B4BF-218B73C547AC/data/Containers/Data/Application/4F72B076-42AD-44A4-A7CF-57D9F93E895E/tmp/ios_testWIDYdS/ios_test/lib/main.dart': warning: line 16 pos 38: unbalanced '{' opens here
      Widget build(BuildContext context) {
                                         ^
    '/Users/obiwan/Library/Developer/CoreSimulator/Devices/AC94F0FF-16F7-46C8-B4BF-218B73C547AC/data/Containers/Data/Application/4F72B076-42AD-44A4-A7CF-57D9F93E895E/tmp/ios_testWIDYdS/ios_test/lib/main.dart': error: line 33 pos 5: unbalanced ')'
        );
    

    在这种情况下,只需更正上述代码的错误,即可以继续使用热重载

    1. CupertinoTabView's builder

    在修改了CupertinoTableView 的builder内容时,Hot reload 不会生效

    1. 枚举类型改变成class 类型

    当一个枚举类型,改成一个类时,Hot reload 不会生效

    1. 字体修改

    2. 泛型修改

    修改前

    class A<T> {
      T i;
    }
    

    修改后

    class A<T, V> {
      T i;
      V v;
    }
    
7. Native code 

   更改原生代码, hot reload 不会生效

8. statelessWidget 和 statefulWidget 的互改

9. 静态变量和全局变量的改变

参考

Flutter Hot Reload doc

深入理解flutter的编译原理与优化

揭秘Flutter Hot Reload(原理篇)

底层原理 - Flutter Hot Reload 详解

美团-Flutter原理与实践

头条开发攻城狮-深入理解Dart虚拟机启动

Dart VM 简介

深入浅出RPC原理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容