本节摘要:"一套代码多端运行"不是一句魔法,而是一条可以拆开看的转化链。本节沿这条链走一遍:Dart 代码如何变成各端产物、界面像素由谁绘制、事件如何回流,以及热重载为什么因此成为可能。建立这条链的直觉,后面章节里所有"为什么"都有了落点。
上一节的对比表里,"自绘"是 Flutter 与桥接方案的分水岭,这里把它说透。传统跨平台方案里,你的代码描述"这里放一个按钮",真正的按钮由系统控件渲染;Flutter 里,你的代码描述"这里放一个按钮",但渲染这个动作从头到尾由 Flutter 自己完成:排版引擎算出按钮上文字的每一处折行,图形引擎把圆角、阴影、文字逐像素画进一块内存画布,最后这块画布被整体提交给系统合成显示。
这件事之所以可行,是因为现代操作系统都暴露了"给一块内存画像素并合成上屏"的能力:Android 有 Surface,iOS 有 CALayer,桌面与浏览器有各自的位图接口。Flutter 的引擎层把这些差异封装掉,向上暴露统一的画布。于是"落地到新平台"这项工作,从"逐个适配系统控件"变成了"实现一个宿主壳"——宿主负责开窗口、喂事件、交画布,界面内容完全由 Flutter 接管。
这也顺带解释了一个初学者常见疑惑:为什么 Flutter 应用在 Android 上不叫"原生控件",看起来却很顺?因为顺滑感来自帧率和响应,不来自控件是谁画的。Flutter 的目标是每帧稳定在 16 毫秒上下完成(60Hz 屏),这个预算怎么花,第 2 章的渲染管线会逐帧拆解。

问:自绘既然逐像素一致,为什么两台设备上看起来还是略有差别? 一致的是"绘制结果",不一致的是"输入条件":屏幕像素密度、系统字体回退、暗色模式的系统开关都会改变画布的输入。框架一致性与设备差异是两回事——同一台设备上两端产物必然逐像素相同,跨设备则要靠主题与适配(第 3 章)抹平输入差异。
问:Web 版首次打开白屏很久,是坏了还是慢? 是转化链的代价结账了:转译出的代码加画布引擎要在首帧前下载完毕,弱网下这段时间被放大。工程上有三件事可做:路由级代码拆分让首屏只拉首屏的份、加载页给用户明确的"正在装填"信号、内容型页面干脆换技术栈(第 7 章的决策表)。理解了链条,才知道该优化链条的哪一环,而不是对着白屏干瞪眼。
同一份 Dart 代码,在不同阶段会以不同形态运行,这是 Flutter 工程里最容易混淆的知识点之一。
开发调试期走 JIT(即时编译)。代码以中间字节码形态运行,Dart 虚拟机在运行时编译与优化。JIT 换来的是热重载:改完代码保存,框架把新的字节码注入运行中的应用,Widget 树按新代码重建,屏幕立刻刷新,而页面状态不丢。开发体验因此从"改一次编译三分钟"变成"改一次看一眼"。
发布期走 AOT(提前编译)。打包时 Dart 代码被整体编译成目标平台的机器码:Android 上是 so 库里的 arm 指令,iOS 上是框架内的 arm64 指令,桌面上是可执行文件的一部分。没有虚拟机解释,没有桥转发,这就是 Flutter 性能接近原生的基础。Web 是特例:浏览器跑不了 Dart,Dart 编译器把代码转译成 JavaScript,渲染则通过 CanvasKit(Skia 的 WebAssembly 版)画到画布上,或者用 HTML 元素模式——两条 Web 路线的取舍放在第 7 章细讲。
| 阶段 | 编译方式 | 产物形态 | 换来的能力 |
|---|---|---|---|
| 调试 | JIT | 字节码,运行时编译 | 热重载、热重启 |
| 发布(移动与桌面) | AOT | 本机机器码 | 无解释开销的性能 |
| 发布(Web) | 转译 | JavaScript 加画布 | 浏览器直接运行 |
落地链条不只是"代码变成像素"的单向流,输入事件沿反方向回流:手指按下屏幕,系统宿主收到触摸,把它喂给引擎,引擎打包成事件分发给框架里的手势系统,最终落到你写的回调上。第 2 章的手势一节会沿这条回流线再走一遍,你会看到一次"点击"从物理层到 Dart 函数的完整旅程。
热重载则站在转化链的开发期一侧。它的流程值得记住,因为排障时用得上:保存文件后,编译器把改动编译成增量字节码,通过 Dart 虚拟机的服务协议注入运行中的进程,随后框架标记 Widget 树需要重建。重建走的是"配置不变"路径——所以热重载改不了 main 函数里的初始状态,也改不了全局静态初始化;遇到改不动的情况,热重启(重建整个状态)是后手。
下面的会话是一个真实的落地闭环,从建工程到看到第一帧:
$ flutter create light_ledger $ cd light_ledger $ flutter devices Found 3 connected devices: sdk gphone arm64 (mobile) · emulator-5554 iPhone 15 (mobile) · sims Chrome (web) · chrome $ flutter run -d emulator-5554 Launching lib/main.dart on sdk gphone arm64 in debug mode... Running Gradle task 'assembleDebug'... 18.3s Connecting to VM Service... 2.1s Flutter run key commands: r Hot reload R Hot restart An Observatory debugger and profiler is available at: 127.0.0.1:端口 重启应用前,把计数器首页换成轻记账的占位首页后按 r: Performing hot reload... 0.6s
同一条命令换一个设备参数(chrome、macOS、Windows),同一个 main 函数就能在浏览器或桌面上跑起来——这就是"宿主壳"路线的红利:新平台的接入成本集中在一个薄壳上,界面代码原地不动。
原理链已经立起来。下一节补齐最后一块勘察材料:写这条链上的代码需要哪些 Dart 语法,以及把工具链真正架起来。