Flutter热重载:让开发效率飞起来🚀

38 阅读8分钟
  1. 引言

在 Android 或 iOS 原生开发中,我们常常遇到这样的问题:

  • 改了一行 UI 代码 → 编译 → 打包 → 安装 → 运行,可能要几十秒甚至几分钟。

  • 一旦需要频繁调整 UI 样式,等待的时间远超写代码的时间。

Flutter 的热重载(Hot Reload) 让这个过程几乎秒级完成:

保存代码 → 立即在设备或模拟器上看到效果 → 状态保持不丢失

这种体验改写了我们的开发节奏,大大提升了迭代速度。

  1. 什么是热重载?

简单来说:

  • 热重载 是在不重启应用进程的情况下,将修改后的代码增量更新到正在运行的应用,并重新构建 UI。
  • Flutter 可以在几十毫秒内完成一次热重载。

不同于 热重启(Hot Restart)

  • 热重载:保留状态,只更新 UI。
  • 热重启:重启整个应用,状态丢失

详细对比: Flutter热重启

  1. Flutter 热重载的原理(比喻版)

我们把 Flutter 应用比作一本“书”:

  • 每个功能就是书里的一个章节(类/函数)
  • 热重载并不是重新印刷整本书,而是只替换我们刚改的那一页内容。
  • 读者(应用)继续从上次停留的地方往下读,不重新开头。

这种替换部件而不重启引擎的能力,正是 FlutterJIT 模式下的特性。

  1. 原理流程(工程版)

开发模式下的步骤

  1. 运行在 Dart VM JIT 模式
  • 支持运行时加载、替换代码。
  1. 保存文件
  • flutter_tools 检测到文件变更。
  1. 增量编译
  • Dart 编译器只编译修改过的文件,生成新的 kernel 字节码。
  1. 代码注入
  • 通过 VM Service 将新字节码注入到运行中的 Dart VM。
  1. Widget 树重建
  • Flutter 框架调用 reassembleApplication(),遍历所有 State 调用 reassemble()
  1. UI 更新
  • setState() 触发 build() 方法,UI 重新渲染,旧状态保留。

流程说明

源码地址:packages/flutter\_tools/lib/src/run\_hot.dart

  1. 触发阶段

热重载从调用 restart() 方法开始,参数 fullRestart=false 表示执行热重载而非热重启 run_hot.dart:775-780。

    @override
      Future<OperationResult> restart({  // 775
        bool fullRestart = false,
        String? reason,
        bool silent = false,
        bool pause = false,
      }) async {
        if (flutterDevices.any((FlutterDevice? device) => device!.devFS == null)) {
          return OperationResult(1, 'Device initialization has not completed.');
        }
        await _calculateTargetPlatform();
        final timer = Stopwatch()..start();

        // Run source generation if needed.
        await runSourceGenerators();

        if (fullRestart) {
          final OperationResult result = await _fullRestartHelper(
            targetPlatform: _targetPlatformName,
            sdkName: _sdkName,
            emulator: _emulator,
            reason: reason,
            silent: silent,
          );
        if (!silent) {
            globals.printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.');
          }
          for (final FlutterDevice? device in flutterDevices) {
            unawaited(device?.handleHotRestart());
          }
          return result;
        }
        final OperationResult result = await _hotReloadHelper(
          targetPlatform: _targetPlatformName,
          sdkName: _sdkName,
          emulator: _emulator,
          reason: reason,
          pause: pause,
        );
        if (result.isOk) {
          final String elapsed = getElapsedAsMilliseconds(timer.elapsed);
          if (!silent) {
            if (result.extraTimings.isNotEmpty) {
              final String extraTimingsString = result.extraTimings
                  .map((OperationResultExtraTiming e) => '${e.description}: ${e.timeInMs} ms')
                  .join(', ');
              globals.printStatus('${result.message} in $elapsed ($extraTimingsString).');
            } else {
              globals.printStatus('${result.message} in $elapsed.');
            }
          }
        }
        return result;
      }

2. ##### 源码重载阶段

_reloadSources() 是核心方法,负责协调整个热重载过程 run_hot.dart:1002-1009 。首先调用 _updateDevFS() 检测和同步修改的文件 run_hot.dart:1027-1032 。

  Future<OperationResult> _reloadSources({    //1002
    String? targetPlatform,
    String? sdkName,
    bool? emulator,
    bool? pause = false,
    String? reason,
    void Function(String message)? onSlow,
  }) async {
    ···
    // 获取所有 Flutter 视图 run_hot.dart:1011-1020
    final viewCache = <FlutterDevice?, List<FlutterView>>{};  // 1011
    for (final FlutterDevice? device in flutterDevices) {
      final List<FlutterView> views = await device!.vmService!.getFlutterViews();
      viewCache[device] = views;
      for (final view in views) {
        if (view.uiIsolate == null) {
          return OperationResult(2, 'Application isolate not found', fatal: true);
        }
      }
    } // 1020
    // 更新 DevFS 1027-1033
    final devFSTimer = Stopwatch()..start(); // 1027
    UpdateFSReport updatedDevFS;    
    try {
      updatedDevFS = await _updateDevFS(targetPlatform: _targetPlatform);
    } finally {
      hotRunnerConfig!.updateDevFSComplete();
    }
    ···
    // 如果有源文件修改,调用重载助手 1048-1064
    if (updatedDevFS.invalidatedSourcesCount > 0) {    // 1048
      final OperationResult result = await _reloadSourcesHelper(
        this,
        flutterDevices,
        pause,
        firstReloadDetails,
        targetPlatform,
        sdkName,
        emulator,
        reason,
        globals.analytics,
      );
      if (result.code != 0) {
        return result;
      }
      reloadMessage = result.message; // 1064
    ···
    // 重组装 UI 1075-1085
    final ReassembleResult reassembleResult = await _reassembleHelper( //1075
      flutterDevices,
      viewCache,
      onSlow,
      reloadMessage,
    );
    shouldReportReloadTime = reassembleResult.shouldReportReloadTime;
    if (reassembleResult.reassembleViews.isEmpty) {
      return OperationResult(OperationResult.ok.code, reloadMessage);
    } //1085
    ···
    // 收集和发送分析数据 1103-1146
    HotEvent(
    ···
  }
// 更新开发文件系统
Future<UpdateFSReport> _updateDevFS({
    bool fullRestart = false,
    required TargetPlatform targetPlatform,
  }) async {
    final bool isFirstUpload = !assetBundle.wasBuiltOnce();
    final bool rebuildBundle = assetBundle.needsBuild();
    // 构建资源包(如果需要)
    if (rebuildBundle) { // 492行
      globals.printTrace('Updating assets');
      final int result = await assetBundle.build(
        flutterHookResult: await dartBuilder?.runHooks(
          targetPlatform: targetPlatform,
          environment: environment,
          logger: _logger,
        ),
        packageConfigPath: debuggingOptions.buildInfo.packageConfigPath,
        flavor: debuggingOptions.buildInfo.flavor,
        targetPlatform: targetPlatform,
      );
      if (result != 0) {
        return UpdateFSReport();
      }
    }
    final Stopwatch findInvalidationTimer = _stopwatchFactory.createStopwatch('updateDevFS')
      ..start();
    final DevFS devFS = flutterDevices[0].devFS!;
    // 查找失效的源文件。ProjectFileInvalidator 获取器用于识别哪些源文件被修改
    final InvalidationResult invalidationResult = await projectFileInvalidator.findInvalidated(
      lastCompiled: devFS.lastCompiled,
      urisToMonitor: devFS.sources,
      packagesPath: packagesFilePath,
      asyncScanning: hotRunnerConfig!.asyncScanning,
      packageConfig: devFS.lastPackageConfig ?? debuggingOptions.buildInfo.packageConfig,
    );
    ···
    // 更新每个设备的 DevFS
    for (final FlutterDevice? device in flutterDevices) {
      results.incorporateResults(
        await device!.updateDevFS(
          mainUri: entrypointFile.absolute.uri,
          target: target,
          bundle: assetBundle,
          bundleFirstUpload: isFirstUpload,
          bundleDirty: !isFirstUpload && rebuildBundle,
          fullRestart: fullRestart,
          pathToReload: getReloadPath(resetCompiler: fullRestart, swap: _swap),
          invalidatedFiles: invalidationResult.uris!,
          packageConfig: invalidationResult.packageConfig!,
          dillOutputPath: dillOutputPath,
        ),
      );
    }
    
  1. VM 重载阶段

如果有源文件被修改(invalidatedSourcesCount > 0),会调用_reloadDeviceSources()调用链为: _reloadSourcesHelper()defaultReloadSourcesHelper()_reloadDeviceSources()该方法遍历所有 isolate 并调用 VM Service 的 reloadSources API run_hot.dart:1357-1372 。

    // 重载单个设备的源码
    Future<List<Future<vm_service.ReloadReport>>> _reloadDeviceSources(    // 1357
      FlutterDevice device,
      String entryPath, {
      bool? pause = false,
    }) async {
      final 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!)
          device.vmService!.service.reloadSources(
            isolateRef.id!,
            pause: pause,
            rootLibUri: deviceEntryUri,
          ),
      ];
    }
    
  1. 重组装阶段

代码重载成功后,调用 _reassembleHelper() 重新组装 Widget 树,但保留应用状态 run_hot.dart:1075-1080 。

  Future<OperationResult> _reloadSources({    //1002
    String? targetPlatform,
    String? sdkName,
    bool? emulator,
    bool? pause = false,
    String? reason,
    void Function(String message)? onSlow,
  }) async {
    ···
    final ReassembleResult reassembleResult = await _reassembleHelper(    // 1075
      flutterDevices,
      viewCache,
      onSlow,
      reloadMessage,
    );
    ···
  }

流程图

暂时无法在飞书文档外展示此内容

时序图

  1. 热重载的优势

  1. 开发速度飞快

    1. 秒级看到 UI 改动效果。
  2. 状态保留

    1. 页面滚动位置、输入内容、变量值都不会丢失。
  3. 调试友好

    1. 可以快速尝试不同 UI 样式和逻辑。
  4. 跨平台一致性

    1. 无需适配多平台,结果在 Android、iOS、Web 等终端一致。
  5. 注意事项与潜在风险

6.1 热重载不是万能更新

  • 底层配置文件、依赖库升级、构建脚本 的修改仍需全量重启。
  • 结构性变更(数据模型大改、全局变量修改)可能无法安全热替换。

例如:修改了 build.gradle 的依赖库和 AndroidManifest.xml 的权限

6.2 状态保留可能导致隐性bug

  • 热重载保留旧状态,有时会让旧逻辑或缓存残留,造成错误在热重载模式下表现与真实启动情况不同。
  • 建议在重要功能上线前进行一次从冷启动的全量测试。

示例1:lib/viewmodel/ai/ai_viewmodel.dart

// Session identifier: set when entering page (timestamp-based)
String? _sessionId;
String? get sessionId => _sessionId;
final int maxSubscribeNum=3;
// 修改前
_sessionId ??= DateTime.now().millisecondsSinceEpoch.toString();

// 你修改了 session ID 生成逻辑,想加上设备 ID
_sessionId ??= '${deviceId}_${DateTime.now().millisecondsSinceEpoch}';

// ❌ 热重载后:_sessionId 已经有值了,不会执行新逻辑

// ✅ 冷启动后:会使用新的格式

示例2:lib/ui/widget/setting/device_setting_extension.dart

extension DeviceSettingViewModel on DeviceShadowViewModel {
  // 加载状态控制
  static bool _isLoading = false;
  // 格式化状态控制
  static bool _isFormatting = false;
  
  // 使用私有静态变量存储OTA版本信息
  static String _cachedOtaVersion = '';
  static bool _hasNewOtaVersion = false;
// 场景1: 正在格式化 SD 卡
_isFormatting = true; // 开始格式化
// 你热重载修改了格式化进度的 UI
// ❌ _isFormatting 依然是 true,但实际格式化可能已完成
// 界面会一直显示"格式化中"

// 场景2: 缓存了 OTA 版本信息
_cachedOtaVersion = '2.1.0';
_hasNewOtaVersion = true;
// 你修改了版本检测逻辑
// ❌ 热重载后依然使用缓存的旧值,不会重新检测

6.3 内存泄漏风险

  • 部分热重载机制不会完全释放旧对象,长时间使用可能造成占用增加。
  • 在长时间调试的场景中,需定期执行全量重启以释放内存。

示例:lib/ui/widget/home/live_video_widget.dart

@override
void onPause() {
  IotLog.d('FROM_FLUTTER_IPC', 'LiveVideoWidget onPause - 页面失去焦点');
  VideoViewModel videoViewModel = context.read<VideoViewModel>();
  videoViewModel.disconnectLive();
  Future.delayed(const Duration(milliseconds: 300), () {
    if (mounted) {
      setState(() {
        _isPageActive = false;
      });
    }
  });
}

@override
void onResume() {
  IotLog.d('FROM_FLUTTER_IPC', 'LiveVideoWidget onResume - 页面获得焦点');
  setState(() {
    _isPageActive = true;
  });
  VideoViewModel videoViewModel = context.read<VideoViewModel>();
  DeviceShadowViewModel deviceViewModel = context.read<DeviceShadowViewModel>();
  if (videoViewModel.videoLiveStatus != VideoStatus.connected &&
      deviceViewModel.getDeviceProperty(Identifier.cameraSwitch)?.data == CameraSwitch.on) {
    final deviceId = DeviceApiHandler.instance.localDeviceInfo.deviceId;
    videoViewModel.connectLive(deviceId, reason: 'onResume');
  }
}
// 用户正在观看实时视频
videoViewModel.connectLive(deviceId);
// 视频流连接已建立,正在传输数据

// 修改了视频播放相关的 UI 代码,执行热重载
// ❌ 问题:
// 1. 旧的视频流连接可能没有完全释放
// 2. 新的热重载可能创建了新的视频播放器实例
// 3. 多次热重载后,可能有多个视频解码器在运行

// 表现:内存占用持续上升,视频卡顿

6.4.边界场景

  • 多线程/并发场景:热替换可能导致线程锁状态与新代码不一致。
  • 热重载对 持久连接(WebSocket、长轮询) 的切换较敏感,需要保证不会断开或混淆新旧逻辑。

示例:lib/viewmodel/device_shadow_viewmodel.dart

@override
Future<bool> init() async {
  IotLog.d('runApp', 'DeviceShadowViewModel init begin');

  IoTSdk.instance.init(
    showLog: true,
    versionName: '1.0',
    defaultMap: defaultDataMap,
    logHandler: IotLogHandlerImpl(),
    cacheRootDirectory: await getApplicationDocumentsDirectory(),
    ioTInterceptor: IpcInterceptor(),
    testLocalDeviceInfo: LocalDeviceInfo(...)
  );

  // 建立页面事件通道
  // 设备信息更新通道
  // 音视频事件接口通道
  // sd卡播放、文件下载接口通道
  // ...
  
  return true;
}
// 应用启动时
IoTSdk.instance.init(...);
// MQTT 连接已建立,正在接收设备状态更新

// 修改 UI 代码,执行热重载
// ❌ 问题:
// 1. IoTSdk.instance 是单例,不会重新初始化
// 2. MQTT 连接状态是旧的,但代码逻辑是新的
// 3. 如果修改了 MQTT 消息处理逻辑,热重载后使用的还是旧逻辑
// 4. IpcDeviceInfoUpdateApi、IpcFlutterVideoApi 等 setup 不会重新执行

// 场景:修改设备属性变化的处理逻辑
@override
bool onDeviceNameUpdate(String newName) {
  // 你添加了新的处理逻辑
  IotLog.d('FROM_FLUTTER_IPC', 'onDeviceNameUpdate===>: $newName');
  // ❌ 热重载:这个方法已经注册到旧的事件系统,不会更新
  notifyListeners();
  return true;
}

6.5 ****团队规范

  • 建议在团队开发文档中说明哪些模块适合用热重载测试,哪些变更必须冷启动。
  • 在提交代码前,自己至少冷启动一次以减少上线问题。

🔥 热重载安全清单

可以安全使用热重载:

  • ✅ 纯 UI 布局调整 (不涉及状态)
  • ✅ 颜色、字体、间距等样式修改
  • ✅ 文本内容修改
  • ✅ 简单的条件渲染逻辑
  • ✅ 无状态 Widget 的修改
  • ✅ 静态资源路径修改

热重载有风险,建议冷启动:

  • ⚠️ 修改 ViewModel 的状态变量
  • ⚠️ 修改网络请求逻辑
  • ⚠️ 修改数据解析逻辑
  • ⚠️ 修改生命周期方法 (initState, dispose)
  • ⚠️ 修改异步操作逻辑
  1. 小结

Flutter 的热重载是它的“杀手级功能”之一:

  • 利用声明式 UI + 状态与渲染分离的设计,让 UI 重建非常轻量。
  • 依托 Dart VM 的 JIT 增量编译,在运行时快速替换代码。
  • 极大提升了开发迭代速度,是很多团队选择 Flutter 的关键原因之一。