-
引言
在 Android 或 iOS 原生开发中,我们常常遇到这样的问题:
-
改了一行 UI 代码 → 编译 → 打包 → 安装 → 运行,可能要几十秒甚至几分钟。
-
一旦需要频繁调整 UI 样式,等待的时间远超写代码的时间。
Flutter 的热重载(Hot Reload) 让这个过程几乎秒级完成:
保存代码 → 立即在设备或模拟器上看到效果 → 状态保持不丢失。
这种体验改写了我们的开发节奏,大大提升了迭代速度。
-
什么是热重载?
简单来说:
- 热重载 是在不重启应用进程的情况下,将修改后的代码增量更新到正在运行的应用,并重新构建 UI。
- Flutter 可以在几十毫秒内完成一次热重载。
不同于 热重启(Hot Restart) :
- 热重载:保留状态,只更新 UI。
- 热重启:重启整个应用,状态丢失。
详细对比: Flutter热重启
-
Flutter 热重载的原理(比喻版)
我们把 Flutter 应用比作一本“书”:
- 每个功能就是书里的一个章节(类/函数)
- 热重载并不是重新印刷整本书,而是只替换我们刚改的那一页内容。
- 读者(应用)继续从上次停留的地方往下读,不重新开头。
这种替换部件而不重启引擎的能力,正是 Flutter 在 JIT 模式下的特性。
-
原理流程(工程版)
开发模式下的步骤
- 运行在 Dart VM 的 JIT 模式
- 支持运行时加载、替换代码。
- 保存文件
flutter_tools检测到文件变更。
- 增量编译
- Dart 编译器只编译修改过的文件,生成新的 kernel 字节码。
- 代码注入
- 通过 VM Service 将新字节码注入到运行中的 Dart VM。
- Widget 树重建
- Flutter 框架调用
reassembleApplication(),遍历所有 State 调用reassemble()
- UI 更新
setState()触发build()方法,UI 重新渲染,旧状态保留。
流程说明
源码地址:packages/flutter\_tools/lib/src/run\_hot.dart
-
触发阶段
热重载从调用 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,
),
);
}
-
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,
),
];
}
-
重组装阶段
代码重载成功后,调用 _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,
);
···
}
流程图
暂时无法在飞书文档外展示此内容
时序图
-
热重载的优势
-
开发速度飞快
- 秒级看到 UI 改动效果。
-
状态保留
- 页面滚动位置、输入内容、变量值都不会丢失。
-
调试友好
- 可以快速尝试不同 UI 样式和逻辑。
-
跨平台一致性
- 无需适配多平台,结果在 Android、iOS、Web 等终端一致。
-
注意事项与潜在风险
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)
- ⚠️ 修改异步操作逻辑
-
小结
Flutter 的热重载是它的“杀手级功能”之一:
- 利用声明式 UI + 状态与渲染分离的设计,让 UI 重建非常轻量。
- 依托 Dart VM 的 JIT 增量编译,在运行时快速替换代码。
- 极大提升了开发迭代速度,是很多团队选择 Flutter 的关键原因之一。