欢迎关注微信公众号:FSA全栈行动 👋
一、核心架构:从三层模型看 Flutter 的底座
大部分跨平台框架解决"如何用一套代码控制两个平台"的问题时,思路基本一致:它们通过"套壳"的方式,试图远程控制平台原生的 UI 组件。
但 Flutter 走了一条完全相反的路。它压根不用平台自带的 UI 组件。你的 App、Flutter 框架以及整个 Widget 库全部都是 Dart 代码,在发布模式下会被直接编译成原生的 ARM 或 x64 机器码。这些代码驱动着 Flutter 自己的 C++ 引擎,再由引擎通过 Impeller 渲染器直接把每一帧画到 GPU 上。
这个决策其实解释了 Flutter 几乎所有的特性:为什么 UI 在不同设备上看起来一模一样;为什么 Hot Reload 能在保持应用状态的情况下热更新代码;为什么性能表现如此稳定;以及为什么想要让它看起来像"原生"应用,需要额外下功夫。在 Flutter 眼里,平台提供的不是按钮或列表,而仅仅是一个窗口、一个 GPU 表面和一串输入事件流。
1、三层架构图解
对于操作系统来说,一个 Flutter 应用其实挺"无聊"的------它只是一个普通的应用程序进程中的全屏原生视图。真正好玩的机制都藏在视图内部,分为三层:
| 层级 | 实现语言 | 核心职责 |
|---|---|---|
| Framework | Dart |
提供 Material / Cupertino 设计语言、Widget 库、Rendering、Animation 等 |
| Engine | C++ |
Impeller 渲染器、Dart 运行时、文本布局、帧调度、Platform Channel 管道 |
| Embedder | 平台相关 | Android (Activity)、iOS (ViewController)、桌面端窗口管理、vsync 信号、输入事件 |
2、各层角色详解
-
Embedder :这是极其轻量且针对平台定制的"外壳"。在
Android上它是承载FlutterView的Activity;在iOS上是FlutterViewController。它的工作虽然不显眼但必不可少:创建渲染表面、同步vsync信号、转发触摸/键盘/鼠标事件、处理App生命周期(后台运行、旋转、销毁等)。 -
Engine :这是整个系统的核心,以
libflutter.so(Android) 或Flutter.framework(iOS) 的形式存在。它托管了Dart运行时(开发模式用JIT,发布模式用AOT)、Impeller渲染器、文本布局、图像解码以及Platform Channel的原生部分。 -
Framework :这是我们平时直接撸代码的地方,全是纯
Dart。它内部也是层层叠加的:底层是foundation,往上是Animation/Painting/Gestures,再往上是Rendering,然后是Widgets,最顶层才是Material和Cupertino。这意味着大部分 "Flutter" 的逻辑其实都是可以阅读、断点调试和单步执行的Dart源码。
二、编译与执行:为什么会有 Hot Reload?
Flutter 根据编译模式的不同,运行 Dart 的方式完全不同。理解这一点,你就能明白为什么 Hot Reload 能这么丝滑,以及为什么发布版性能这么强。
1、Debug 模式:应用内嵌了一个虚拟机
当你运行 flutter run 时,Flutter 工具会解析包、生成插件注册表,然后调用 Gradle 或 Xcode 构建外壳。你的 Dart 源码会被编译成一种叫 kernel 的中间表示格式(存储在 .dill 文件中),然后由 Engine 里的 Dart VM 加载并进行 JIT (即时编译)。
这就是 Hot Reload 的原理:
-
工具只重新编译发生变化的代码库,生成一个增量
kernel。 -
这个增量被通过
Service Protocol推送到正在运行的VM中。 -
VM直接在内存中"打补丁",替换掉旧的类和函数,应用不需要重启。 -
框架调用
reassemble(),标记组件为"脏"并触发重建。
关键点在于: Element Tree 和 State 对象并没有被丢弃,只是新旧配置进行了一次 Diff。所以你的导航栈、输入框里的文字、计数器的数字都能留下来。
2、Release 模式:抛弃虚拟机,直接上机器码
而 flutter build apk 或 ipa 走的是另一条路。gen_snapshot 工具会将你的 App 和 Framework 一起进行 AOT (预编译) 转换成原生的机器码,并顺便做掉没用的代码(Tree-shaking)。
在 Android 的 APK 里,你最终会看到:
-
libapp.so:你的应用 +Framework(原生机器码)。 -
libflutter.so:C++引擎。 -
flutter_assets/:图片、字体等资源。
这里没有 JIT,没有解释器,也没有运行时反射,这就是为什么 dart:mirrors 在 Flutter 里用不了。
三、渲染流水线:从 Widget 到 GPU 像素
这是最容易让新手困惑的地方:为什么"每一帧都在重建 Widget"却不会卡顿?
1、三棵树的协同
在 Flutter 里,按钮、内边距、主题、甚至手势检测,本质上都是可组合的 Dart 对象。但请记住:Widget 不是视图。 它没有尺寸,也不负责绘图,它只是一个临时的"蓝图"。
真正干活的是另外两棵树:
| 树类型 | 特性 | 职责 |
|---|---|---|
| Widget Tree | 不可变,频繁重建 | 提供 UI 的描述(蓝图) |
| Element Tree | 可变,持久存在 | 管理 State,负责连接 Widget 和 RenderObject |
| Render Tree | 昂贵,复用为主 | 负责布局 (Layout)、绘制 (Paint) 和点击测试 (Hit-test) |
当你调用 setState() 时,Widget Tree 会产生一堆新的 Widget 对象。Framework 会拿着这些新 Widget 去和现有的 Element Tree 做对比(通过 canUpdate 检查 runtimeType 和 key 是否一致):
-
一致 :复用现有的
Element和RenderObject,只更新一下配置。 -
不一致:把整个子树拆掉,重新创建一套。
所以重建并不可怕,因为昂贵的 RenderObject 几乎是不动的。
2、渲染管线:一帧的诞生
当 User 点击屏幕,Embedder 转发事件,Engine 找到对应的 RenderObject 并触发回调(如 onTap)。接着 setState() 被调用,标记 Element 为"脏",并向 Engine 申请一个新帧。
在下一个 vsync 信号到来时,UI 线程会按顺序执行以下阶段:
-
Animate:动画数值更新。
-
Build :重建被标记为"脏"的
Widget树。 -
Layout :约束向下传递 (
Constraints go down),尺寸向上汇报 (Sizes go up)。 -
Paint:记录绘制指令。
-
Composite :将层级树合成
Scene交给Engine。
这里有个很重要的原则:约束向下传递,尺寸向上汇报 。这就是为什么如果你在一个 Column 里面套一个 ListView,就会报"无限高度"错误------因为 Column 给出的约束是无限的,而 ListView 试图占据无限的高度,两者冲突了。
3、从渲染到像素:Impeller 的意义
以前 Flutter 使用 Skia 引擎,它会在运行时动态编译 Shader(着色器)。这意味着当你第一次运行某个动画时,可能会因为编译 Shader 而产生明显的掉帧(Jank)。
现在 Flutter 默认使用了 Impeller。它的思路很直接:不再运行时编译,而是把需要的 Shader 在构建引擎时就预先编译好。这样,渲染线程只需把指令丢给 Metal (iOS) 或 Vulkan (Android) 即可,从根本上解决了首帧动画卡顿的问题。
四、总结与权衡
Flutter 的架构给了开发者极大的自由,但也并非没有代价。
1、优势与劣势对比
| 维度 | Flutter 方案 |
原生方案 |
|---|---|---|
| UI 一致性 | 极高,一套代码像素级复现 | 取决于各平台组件实现差异 |
| 性能上限 | 高且可预测,无通信桥接开销 | 最高,直接调用系统能力 |
| 包体积 | 较大(需要自带引擎) | 较小 |
| 原生交互 | 需通过 Channel 或 FFI 桥接 |
原生支持 |
2、最后聊两句
理解了这套架构,你就不会再纠结为什么 Widget 重建这么快,也不会再奇怪 Hot Reload 为什么能保持状态。
Flutter 的本质是一个带渲染引擎的响应式框架 。它跳过了系统的 UI 逻辑,自己掌握了像素的控制权。这种"我全都要"的霸气,虽然带来了包体积变大的代价,但也换来了跨平台开发中极其珍贵的------高度一致的开发体验。
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~