# Flutter 面试题精选
覆盖
Dart、Flutter Framework、渲染、状态、并发、平台集成、性能、测试、发布、工程实战,以及Flutter/React Native/uni-app高频对比。每题均含速记、代码、详细解析和追问;核心流程可逐帧交互。
# 1. 架构与选型
# 1. Flutter 的整体架构分哪几层?各层干什么?
30 秒速记
- 三层:
Framework(Dart)→Engine(C++)→Embedder(各平台原生代码),越往下越贴近系统 Framework内部再分:foundation→animation/painting/gestures→rendering→widgets→Material/Cupertino- 三棵树:
Widget是配置(轻、不可变、随时重建),Element是实例和身份(持有State),RenderObject管布局和绘制 build被调用 ≠ 重新布局 ≠ 重新绘制,中间有Element做比对,只有属性真变了才往下标脏- 业务代码几乎只碰
widgets及以上,自定义布局/绘制才下到rendering层
Flutter 自上而下是 Framework、Engine、Embedder 三层,业务写的 Dart 代码全在 Framework 里。 Framework 负责把你的 Widget 变成一棵渲染树,算出布局、生成绘制指令;Engine 是 C++ 写的,带着 Dart 运行时、文字排版和渲染后端(Skia 或 Impeller),把绘制指令真正变成像素;Embedder 是每个平台的壳,负责建窗口、接 vsync、转发触摸和生命周期。面试我会顺带讲三棵树:Widget 只是一份配置,重建很便宜,Element 才是真正挂在树上的身份,RenderObject 干重活,所以「频繁 build」本身不一定是性能问题。
# 先用大白话把三层讲清楚
可以把 Flutter 应用想成一个自带画笔的游戏:Framework 是游戏逻辑,决定画什么;Engine 是画笔和画布,负责真的画出来;Embedder 是游戏机的外壳,管屏幕、按键和开关机。原生 Android 的按钮是系统画的,Flutter 的按钮是 Engine 自己一笔一笔画的,系统只提供一块空白画布。
| 层 | 语言 | 主要内容 | 业务会不会碰 |
|---|---|---|---|
Framework |
Dart |
widgets、rendering、手势、动画、Material / Cupertino 组件库 |
天天碰 |
Engine |
C++ |
Dart VM、Skia / Impeller、文字排版、platform channel 的底层通道 |
基本不碰 |
Embedder |
Java/Kotlin、ObjC/Swift、C++ |
窗口/Surface、线程、vsync、输入事件、插件注册 |
写插件、混合开发时碰 |
# 三棵树是 Framework 层的核心
Widget 是不可变的配置对象,每次 setState 都会生成一批新的;Element 是框架根据 Widget 创建的长期实例,它拿新旧 Widget 比较 runtimeType 和 key,一样就复用自己,只把新配置递给下面的 RenderObject;RenderObject 收到新属性后,只有值真的变了才调用 markNeedsLayout 或 markNeedsPaint。所以一帧里 build 可能跑了很多次,但真正重新布局、重新绘制的节点可能很少。
下面这段能直接跑,看清 BuildContext 其实就是 Element,以及同一个 Element 在重建后不变:
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: Counter()));
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int n = 0;
@override
Widget build(BuildContext context) {
// context 就是当前 Element,每次重建打印的 hashCode 不变
debugPrint('element=${identityHashCode(context)} widget=${identityHashCode(widget)}');
return Scaffold(
body: Center(
child: Builder(builder: (ctx) {
// 拿到 Text 下面那个 RenderObject(RenderParagraph)
WidgetsBinding.instance.addPostFrameCallback((_) {
debugPrint('render=${ctx.findRenderObject().runtimeType}');
});
return Text('$n');
}),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => n++),
child: const Icon(Icons.add),
),
);
}
}
// 点几次按钮:element 的 hashCode 一直不变,Text widget 每次都是新对象
# 容易说错的地方
很多人说「Flutter 是 Dart 写的」,其实只有 Framework 是,Engine 是 C++,所以排查渲染卡顿时要看 DevTools 里 UI 线程和 Raster 线程两条线:UI 线程慢是你的 build / layout 重,Raster 线程慢是画的东西重(比如大量 saveLayer、模糊、裁剪)。另一个是渲染后端的版本差异:iOS 从 Flutter 3.10 起默认 Impeller,Android 从 3.27 起在支持 Vulkan 的设备上默认 Impeller,老设备会回落,讲渲染层时别只说 Skia。
面试官追问
追问 1setState 之后整个页面的 build 都跑了,是不是整页都重绘了?
不是。build 只生成新配置,Element 比对后复用节点,RenderObject 只有属性变了才 markNeedsPaint。想确认就开 debugRepaintRainbowEnabled = true,颜色变了的区域才是真重绘的。
追问 2同样一个列表,UI 线程很闲,Raster 线程一直超 16ms,问题在哪一层?
在 Engine 的光栅化,不在你的 build。常见是 Opacity、ClipRRect、BackdropFilter 触发了 saveLayer,或者图片解码尺寸太大。先用 DevTools 的 Raster 面板找贵的 layer,图片记得配 cacheWidth。
追问 3什么时候需要自己写 RenderObject,而不是组合现有 Widget?
组合实现不了、或者组合层级深到影响性能的布局才写,比如按特殊规则排的标签流、需要在一次 layout 里拿到子节点尺寸再决定位置的场景。99% 的业务用 CustomPaint、CustomMultiChildLayout 就够了,直接写 RenderBox 维护成本不小。
追问 4Element 复用的判断依据是什么?列表项状态串了跟它有关系吗?
看 runtimeType 和 key 是否都相同。列表删掉第一项后,没有 key 的话框架按位置匹配,第二项的 Widget 会复用第一项的 Element 和 State,于是状态串位。给列表项加 ValueKey(item.id) 就好。
# 2. Flutter 为什么能一套代码在多个平台上画出一样的 UI?
30 秒速记
- 根本原因是自绘:不用系统控件,
Engine自己用Skia/Impeller往一块画布上画像素 - 平台只需要提供三样:一块
Surface、vsync信号、输入事件,这部分由Embedder适配 - 代价:长得不像原生(要自己做
Cupertino/ 适配)、无障碍和输入法要靠Semantics和平台桥接补 - 嵌原生控件(地图、
WebView)要走PlatformView,有额外合成开销 Web端渲染器:CanvasKit/skwasm,旧的HTML renderer已在3.29移除
Flutter 能多端一致,是因为它压根不用系统的 UI 控件,所有东西都是 Engine 自己画的。 每个平台对 Flutter 来说只是「给我一块画布、告诉我什么时候该画下一帧、把用户的点击传给我」,这些由很薄的 Embedder 适配,剩下的布局、绘制、动画、手势全在跨平台的 Dart 和 C++ 里完成,所以同一份代码出来的像素基本一样。代价也很明确:按钮长得不会自动像 iOS 原生,系统控件的新特性要等 Flutter 自己实现,地图和 WebView 这种只能嵌原生控件,嵌进来有性能成本。
