Flutter 面试知识点整理
本文整理 Flutter 高频面试知识点,按「Dart 语言 → Widget 与三棵树 → 状态管理 → 渲染与布局 → 事件机制 → 线程与异步 → 混合开发 → 性能优化 → 引擎架构」的顺序组织,方便系统性复习。
一、Dart 语言基础
Dart 为什么适合 Flutter?
- JIT + AOT 双编译:开发期用 JIT(Just-In-Time)编译,支持 Hot Reload(热重载);发布期用 AOT(Ahead-Of-Time)编译成原生机器码,运行性能好、启动快。
- 单线程事件循环模型:UI 逻辑跑在单一 isolate 上,天然避免多线程访问 UI 的并发问题(对齐主流客户端 UI 框架)。
- AOT 下无需 JVM/桥接,直接调用 Skia 绘制,避免了 RN 那种 JS Bridge 的序列化开销。
- 强类型 + 空安全(Null Safety),编译期就能规避大量空指针问题。
final 与 const 的区别?
final:运行时常量,只能赋值一次,值可以在运行期才确定。const:编译期常量,值必须在编译时就能确定,且是深度不可变的。- Flutter 里大量使用
const构造 Widget,因为constWidget 会被规范化(canonicalize)复用同一个实例,重建时可直接跳过,是重要的性能优化点。
1
2
3
final now = DateTime.now(); // ✅ 运行时才知道值
const pi = 3.14; // ✅ 编译期常量
// const now = DateTime.now(); // ❌ 编译报错
late 关键字的作用?
延迟初始化非空变量:声明时不赋值,但保证使用前一定会赋值;也可用于「懒加载」——被 late 修饰的变量在第一次访问时才执行初始化表达式。
??、?.、??=、! 各是什么?
?.:空感知调用,左侧为 null 时整体返回 null。??:为 null 时取默认值,a ?? b。??=:为 null 时才赋值,a ??= b。!:非空断言,告诉编译器”我确定它不为 null”(错了会运行时抛异常)。
mixin、extends、implements、with 的区别?
extends:单继承,复用父类实现。implements:实现接口,必须重写所有成员(Dart 中任何类都可当接口)。with(mixin):横向复用代码,多个 mixin 线性化混入,解决单继承的复用局限。- 典型场景:
class A extends B with M1, M2 implements I1 {},线性化顺序影响super调用链。
二、Widget、Element、RenderObject(三棵树)
Flutter 三棵树分别是什么?
| 树 | 作用 | 是否可变 | 创建频率 |
|---|---|---|---|
| Widget 树 | UI 的配置/描述,不可变(immutable) | 不可变 | 频繁重建、开销小 |
| Element 树 | Widget 的实例化载体,维护生命周期与关联关系 | 可变、复用 | 尽量复用 |
| RenderObject 树 | 真正负责 布局、绘制、命中测试 | 可变、复用 | 尽量复用 |
- Widget 是”图纸”,Element 是”施工队/中间人”,RenderObject 是”真正的房子”。
- Widget 频繁重建,但 Element / RenderObject 尽可能复用,这是 Flutter 高性能的核心。
为什么需要 Element 这一层?
Widget 不可变且频繁重建,如果每次都重建 RenderObject 会非常昂贵。Element 作为中间层:
- 持有对 Widget 和 RenderObject 的引用;
- 负责在 Widget 变化时判断能否复用已有的 Element / RenderObject(通过
canUpdate); - 维护上下文(
BuildContext本质就是 Element)。
Widget.canUpdate 的判断逻辑?
1
2
3
4
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
即 runtimeType 相同 且 key 相同 → 复用旧 Element(只更新配置);否则销毁旧 Element、创建新的。这也是 Key 存在意义的底层依据。
BuildContext 到底是什么?
BuildContext 就是 Element 的抽象接口。build(BuildContext context) 里的 context 指向当前 Widget 对应的 Element,可用来向上查找祖先(如 Theme.of(context)、context.findAncestorStateOfType)、获取自身在树中的位置信息(size、渲染对象等)。
三、StatelessWidget / StatefulWidget 与 State
两者区别?
- StatelessWidget:无内部可变状态,UI 只由外部传入的配置决定。
- StatefulWidget:持有可变状态,状态放在独立的
State对象里。为什么状态要单独放在 State 里?因为 Widget 本身不可变、会被频繁重建丢弃,而 State 对象由 Element 持有、跨重建保留。
State 的生命周期?
1
2
3
4
5
6
7
8
createState()
→ initState() // 只调用一次,做初始化、订阅
→ didChangeDependencies() // 依赖的 InheritedWidget 变化时也会调用
→ build() // 构建 UI(可多次)
→ didUpdateWidget() // 父 Widget 重建导致配置更新时
→ setState() // 触发重建
→ deactivate() // 从树中移除(可能被重新插入)
→ dispose() // 彻底销毁,释放资源、取消订阅
initState:初始化,此时还不能用context访问 InheritedWidget(应放到didChangeDependencies)。dispose:必须在这里释放 controller、动画、流订阅、定时器等,否则内存泄漏。
setState 的原理?
setState 做两件事:
- 执行传入的回调,修改状态字段;
- 调用
element.markNeedsBuild(),把当前 Element 标记为 dirty,加入BuildOwner的 dirty 列表。
下一帧 WidgetsBinding 的 drawFrame 会重建所有 dirty 的 Element(rebuild),触发 build。
注意:
setState只标记当前 Element 子树需要重建,不会重建整棵树;- 回调里应只做同步的状态修改,异步操作要在
mounted为 true 时才调用setState; - 频繁/大范围
setState是常见性能问题,应尽量下沉状态、缩小重建范围。
四、Key 的作用
什么时候需要 Key?
当同类型 Widget 在列表中顺序发生变化、增删时,仅靠 runtimeType 无法正确匹配新旧 Element,会导致状态错乱(比如带勾选状态的列表项被复用到错误的位置)。此时用 Key 帮助 Element 精确匹配。
Key 的分类?
- LocalKey(局部,用于同一父节点下的兄弟节点区分):
ValueKey:用一个值区分(如 id)。ObjectKey:用对象本身区分。UniqueKey:每次都唯一(强制不复用)。
- GlobalKey(全局唯一):
- 可跨树访问 State / Element / RenderObject(
globalKey.currentState、currentContext); - 用于跨位置保留 State、拿到 Form/子组件的状态;
- 开销较大,滥用会影响性能,非必要不用。
- 可跨树访问 State / Element / RenderObject(
五、状态管理
为什么需要状态管理框架?
setState 只能管理组件自身局部状态。当状态需要跨组件、跨层级共享,靠层层传参(prop drilling)会非常繁琐且难维护,因此引入状态管理方案。
常见方案对比
| 方案 | 核心思想 | 特点 |
|---|---|---|
| InheritedWidget | 官方底层,自上而下高效共享数据 | 是众多方案的基础,直接用较繁琐 |
| Provider | 对 InheritedWidget 的封装 + ChangeNotifier | 官方推荐、轻量、易上手 |
| Riverpod | Provider 作者重写,编译期安全、不依赖 BuildContext | 解决 Provider 的诸多痛点 |
| Bloc / Cubit | 基于事件流(Stream),单向数据流 | 规范、可测试性强、模板代码多 |
| GetX | 状态 + 路由 + 依赖注入一体 | 上手快、侵入性强、生态争议 |
InheritedWidget 的原理?
- 它把数据”注入”到 Element 树中,子孙节点通过
context.dependOnInheritedWidgetOfExactType<T>()获取数据,同时把自己登记为依赖者。 - 当 InheritedWidget 的数据变化(
updateShouldNotify返回 true)时,会精准通知所有依赖它的 Element 重建,而不是重建整棵子树。 Theme.of(context)、MediaQuery.of(context)都是基于它实现的。
六、渲染与布局机制
Flutter 的渲染流水线(一帧做了什么)?
Vsync 信号到来后,SchedulerBinding 触发 drawFrame,依次执行:
- Build:重建 dirty 的 Widget → 更新 Element / RenderObject。
- Layout(布局):约束自上而下传递,尺寸自下而上返回。
- Paint(绘制):生成 Layer 树,记录绘制指令。
- Composite(合成):将 Layer 树合成,交给 Engine。
- Rasterize(光栅化):Engine 用 Skia/Impeller 光栅化,最终上屏。
Flutter 的布局约束原则?
一句话口诀:Constraints go down, Sizes go up, Parent sets position.
- 约束向下:父节点把
BoxConstraints(min/max width/height)传给子节点; - 尺寸向上:子节点在约束范围内确定自己的尺寸,返回给父节点;
- 父定位置:父节点根据自己的布局逻辑决定子节点的位置(offset)。
理解这条原则能解释大量布局问题,比如「为什么 Container 在无约束下会尽可能大/小」「为什么要用 Expanded / Flexible」。
为什么会出现”无边界约束(unbounded constraints)”报错?
当一个可滚动(如 ListView)或弹性布局在主轴方向约束为无限大时,子组件无法确定尺寸就会报错。常见于 Column 里直接放 ListView(Column 主轴高度无限),解决方式是用 Expanded / Flexible 给出有界约束,或给 ListView 指定 shrinkWrap: true。
RepaintBoundary 有什么用?
将子树的绘制隔离到独立的 Layer,使其重绘不影响其它区域、其它区域重绘也不影响它。对于频繁重绘的局部(如动画)能显著减少不必要的 repaint。滥用会增加 Layer 数量和内存,需权衡。
七、事件处理机制
Flutter 的事件分发流程?
- Engine 收到触摸事件(
PointerEvent)传给 Framework。 - 命中测试(Hit Test):从 RenderView 开始自上而下遍历 RenderObject 树,收集命中路径上的所有 RenderObject 到
HitTestResult。 - 事件派发:沿命中结果从内到外(子到父)派发事件(
handleEvent)。
GestureArena(手势竞技场)是什么?
当多个手势识别器同时命中(如同时有点击和滑动),它们会加入”竞技场”竞争:
- 每个识别器根据后续事件”声明胜出(accept)”或”放弃(reject)”;
- 最终由一个胜出者独占该次手势,其余被拒绝;
- 这套机制解决了手势冲突(如 ListView 内的按钮点击 vs 列表滑动)。
八、线程与异步(Isolate / Event Loop)
Dart 是单线程的,怎么处理异步?
Dart 单线程通过事件循环(Event Loop) 实现异步非阻塞。每个 isolate 有两个队列:
- Microtask Queue(微任务队列):优先级高,如
scheduleMicrotask、Future的.then回调。 - Event Queue(事件队列):I/O、定时器、手势、
Future等。
执行顺序:先清空当前微任务队列,再取一个事件队列任务;每处理完一个事件又会先清空微任务队列。微任务永远优先于事件任务。
Future 与 async/await?
Future表示一个异步操作的最终结果(未来完成/失败)。async标记异步函数,返回Future;await挂起当前函数、等待 Future 完成,但不阻塞线程(让出控制权给事件循环)。
1
2
3
4
Future<String> fetch() async {
final data = await http.get(url); // 不阻塞,让出主线程
return data.body;
}
Future 与 Stream 的区别?
- Future:单个异步结果(一次性)。
- Stream:多个异步结果组成的序列(可持续产出),有单订阅流和广播流;配合
StreamBuilder、async*/yield使用。
Isolate 是什么?和线程有什么区别?
- Isolate 是 Dart 的并发单元,各自独立的内存堆、独立的事件循环,不共享内存。
- 与传统线程最大区别:Isolate 之间不共享内存,只能通过
SendPort/ReceivePort消息传递通信,因此没有锁和竞态问题。 - 使用场景:CPU 密集型任务(大 JSON 解析、图片处理、加解密),避免阻塞 UI isolate 造成掉帧。可用
compute()快捷创建。
1
2
// compute 会在新 isolate 中执行 heavyParse,完成后把结果传回
final result = await compute(heavyParse, jsonString);
九、混合开发与原生通信
Flutter 如何与原生(Android/iOS)通信?
通过 Platform Channel,消息在 Flutter 与原生之间以二进制编解码传递,异步:
| Channel | 用途 |
|---|---|
| MethodChannel | 方法调用(最常用),Flutter 调原生方法并拿返回值,反之亦可 |
| EventChannel | 事件流,原生向 Flutter 持续发送数据(如传感器、电量监听) |
| BasicMessageChannel | 传递字符串或半结构化消息,双向 |
- Channel 通过 name(字符串标识) 区分,Flutter 端和原生端 name 必须一致。
- 通信默认在平台主线程处理,注意不要在原生侧做耗时操作阻塞。
1
2
const channel = MethodChannel('com.example/battery');
final level = await channel.invokeMethod<int>('getBatteryLevel');
Flutter 三种混合栈接入方式?
- 纯 Flutter 应用;
- 原生为主、局部嵌入 Flutter(add-to-app,
FlutterEngine/FlutterFragment); - 混合栈路由管理(如闲鱼 FlutterBoost),解决原生页面与 Flutter 页面互相跳转、栈管理与内存问题。
十、性能优化
常见性能优化手段?
- 多用
const构造:命中 const 复用,重建时直接跳过。 - 缩小
setState范围:把状态下沉到最小的 StatefulWidget,避免大范围重建。 - 列表用
ListView.builder/SliverList:按需懒加载,而非一次性构建所有子项。 RepaintBoundary隔离频繁重绘区域(如动画)。- 避免在
build里做耗时操作 / 创建大对象 / 新建闭包,build 会被频繁调用。 - 图片优化:
cacheWidth/cacheHeight控制解码尺寸,及时释放,使用缓存库。 - CPU 密集任务放到 Isolate(
compute),避免阻塞 UI。 - 减少不必要的透明度 / 裁剪 / 阴影(
saveLayer开销大)。 - 用 DevTools 的 Performance / Timeline 定位 jank,观察 Raster 与 UI 线程耗时。
如何定位卡顿(Jank)?
- Flutter DevTools → Performance,查看每帧是否超过 16.6ms(60fps);
- 区分是 UI 线程(build/layout/paint 慢)还是 Raster 线程(光栅化慢,通常是绘制过重);
- Timeline 找出超时帧对应的耗时方法,针对性优化。
十一、引擎架构与其它高频问题
Flutter 的整体架构分层?
- Framework 层(Dart):Material/Cupertino、Widgets、Rendering、Animation、Painting 等,我们平时写的代码。
- Engine 层(C/C++):核心是 Skia / Impeller(图形)、Dart Runtime、文本排版、Platform Channel 等。
- Embedder 层(平台相关):把引擎嵌入到各平台(Android/iOS/Web/桌面),负责线程管理、事件循环、Surface。
Skia 和 Impeller 的关系?
- Skia:Flutter 长期使用的 2D 图形库,跨平台绘制。
- Impeller:Flutter 新一代渲染引擎,预编译 shader,解决 Skia 首次运行时 shader 编译造成的卡顿(shader jank)。iOS 已默认,Android 逐步默认。
Hot Reload 与 Hot Restart 的区别?
- Hot Reload(热重载):把改动的 Dart 代码增量注入正在运行的 Dart VM,保留应用当前状态,重建 Widget 树;不会重跑
main()/initState。适合改 UI。 - Hot Restart(热重启):重启 Dart VM,丢失状态,重跑
main();比重新编译安装快。 - 依赖 JIT 编译,Release(AOT)模式下不支持热重载。
Widget 重建 ≠ RenderObject 重建
面试常考的关键结论:build 被调用、Widget 重新创建,不代表 Element 和 RenderObject 被重建。只要 canUpdate 通过,Element 复用,RenderObject 只是被更新属性(updateRenderObject),这正是 Flutter 频繁 rebuild 却依然高性能的原因。
Flutter 的优缺点?
- 优点:一套代码多端一致(自绘 UI,不依赖平台控件)、性能接近原生、Hot Reload 开发效率高、UI 表现力强。
- 缺点:包体积偏大、部分平台特性仍需原生桥接、动态化能力弱(AOT 限制)、大型混合栈接入有成本。
十二、Dart 进阶
普通构造、命名构造、factory 构造的区别?
- 普通/命名构造:必然创建一个新实例(
Point(1,2)、Point.origin())。 factory工厂构造:不强制创建新对象,可以返回缓存实例、子类实例或已有实例,因此常用于单例、缓存池、根据参数返回不同子类。
1
2
3
4
5
6
7
class Logger {
static final Map<String, Logger> _cache = {};
final String name;
Logger._internal(this.name); // 私有命名构造
factory Logger(String name) => // 工厂构造:命中缓存则复用
_cache.putIfAbsent(name, () => Logger._internal(name));
}
Dart 中如何实现单例?
1
2
3
4
5
class Singleton {
Singleton._(); // 私有构造,外部无法 new
static final Singleton instance = Singleton._();
factory Singleton() => instance; // 让 Singleton() 也返回同一实例
}
static final 保证懒汉/饿汉且线程安全(Dart 单 isolate 无并发问题)。
== 与 hashCode 为什么要一起重写?
判断两个对象「值相等」时需重写 ==;而 Set/Map 依赖 hashCode 分桶,只重写 == 不重写 hashCode 会导致 Set 去重、Map 查找失效。推荐用 Object.hash(a, b, ...) 生成,或直接用 freezed/equatable 自动生成。
extension(扩展方法)?
在不修改/继承原类的前提下给已有类型「加方法」,编译期静态解析:
1
2
3
4
extension StringExt on String {
bool get isEmail => contains('@');
}
'a@b.com'.isEmail; // true
级联操作符 .. 的作用?
对同一对象连续调用多个方法/赋值并返回对象本身,避免重复写变量名:
1
2
3
4
final p = Paint()
..color = Colors.red
..strokeWidth = 2
..style = PaintingStyle.stroke;
深拷贝与浅拷贝?
Dart 没有内置深拷贝。List.from / Map.from / ...spread 都是浅拷贝(元素仍是引用)。深拷贝需手动逐层复制,或 jsonDecode(jsonEncode(obj))(仅适用可序列化对象),或用 freezed 的 copyWith。
sync* / async* 与 yield?
sync*+yield→ 惰性生成 Iterable(同步、按需产出)。async*+yield→ 生成 Stream(异步、可持续产出),yield*委托给另一个序列。
十三、动画机制
隐式动画 vs 显式动画?
- 隐式动画(Implicit):
AnimatedContainer、AnimatedOpacity、AnimatedPadding等,只要改变属性并给duration,框架自动补间,无需管理控制器,简单场景首选。 - 显式动画(Explicit):手动用
AnimationController控制,可暂停/反向/重复/监听进度,复杂动画用它。
显式动画的核心三件套?
- AnimationController:控制动画的开始/停止/时长/方向,值域 0~1;需要
vsync(TickerProvider)。 - Tween:定义补间范围(
Tween<double>(begin: 0, end: 100)),tween.animate(controller)生成Animation。 - AnimatedBuilder / Transition:监听
Animation重建局部 UI(用AnimatedBuilder的child参数缓存不变子树,减少重建)。
1
2
3
4
5
6
class _S extends State<Demo> with SingleTickerProviderStateMixin {
late final _c = AnimationController(vsync: this, duration: 1.seconds);
late final _a = Tween(begin: 0.0, end: 1.0).animate(_c);
@override void initState() { super.initState(); _c.repeat(reverse: true); }
@override void dispose() { _c.dispose(); super.dispose(); } // 必须 dispose
}
Ticker 与 vsync 是什么?为什么要 SingleTickerProviderStateMixin?
- Ticker 在每一帧回调(对齐 Vsync 信号),驱动 AnimationController 更新值。
vsync传入一个TickerProvider,让 Ticker 与页面绑定:页面不可见时 Ticker 暂停,避免离屏动画白白消耗资源。- 单个控制器用
SingleTickerProviderStateMixin,多个用TickerProviderStateMixin。
十四、路由与导航
Navigator 1.0(命令式)?
基于栈,命令式增删页面:
Navigator.push(context, MaterialPageRoute(builder: ...))入栈;Navigator.pop(context, result)出栈并可回传结果;- 命名路由
Navigator.pushNamed(context, '/detail', arguments: ...),在routes/onGenerateRoute注册。
Navigator 2.0(声明式)?
用 Router + Pages 声明式描述整个路由栈(栈由状态驱动),解决 Web 深链、浏览器前进后退、恢复路由状态 等 1.0 难以处理的场景。核心组件:
- RouterDelegate:根据 App 状态构建
Navigator的pages列表; - RouteInformationParser:URL ↔ 应用状态互转。
因为原生 2.0 模板代码多,实际项目多用官方封装 go_router(声明式路由表 + 深链 + 重定向 + 嵌套路由)。
页面间传参与回传结果?
- 传参:构造函数直接传、命名路由
arguments; - 回传:
await Navigator.push(...)拿到pop(result)的返回值。
十五、网络请求与数据序列化
http 与 dio 的选择?
http:官方轻量包,够用但功能少。dio:功能全,支持拦截器(统一加 token/日志/错误处理)、超时、取消(CancelToken)、文件上传下载、FormData、全局配置,中大型项目主流选择。
JSON 序列化的三种方式?
| 方式 | 特点 |
|---|---|
手写 fromJson/toJson | 简单直接,字段多时易错、维护累 |
| json_serializable | 注解 + build_runner 代码生成,减少样板 |
| freezed | 生成不可变数据类 + copyWith + ==/hashCode + union(sealed),配合状态管理很香 |
Dart 没有运行时反射(release 下),所以主流做法是编译期代码生成而非反射解析。
十六、本地存储
| 方案 | 适用 |
|---|---|
| SharedPreferences | 轻量 KV(配置、开关、token),底层是 plist/xml |
| sqflite | 关系型 SQLite,结构化、复杂查询 |
| Hive / Isar | 纯 Dart NoSQL,读写快、无需原生依赖 |
| drift | SQLite 之上的类型安全 ORM,编译期检查 SQL |
| flutter_secure_storage | 加密存储敏感数据(Keychain/Keystore) |
| path_provider | 获取文档/缓存/临时目录路径,配合文件读写 |
十七、App 与页面生命周期
如何监听 App 前后台切换?
实现 WidgetsBindingObserver,注册后重写 didChangeAppLifecycleState:
1
enum AppLifecycleState { resumed, inactive, paused, hidden, detached }
resumed:可见可交互(前台);inactive:非活跃(如来电、下拉通知栏);paused:退到后台、不可见;detached:还在运行但已脱离视图;hidden:所有视图隐藏(较新增加)。
如何感知某个页面被覆盖/回到前台?
用 RouteObserver<PageRoute> + RouteAware,重写 didPush / didPopNext(当前页重新可见)/ didPushNext(被新页覆盖)。
十八、Sliver 与滚动
为什么需要 Sliver?
当页面里多个可滚动区域(AppBar 折叠 + 列表 + 网格)需要共享同一个滚动时,用 CustomScrollView 承载多个 Sliver,统一滚动、按需懒加载。
常见 Sliver:SliverAppBar(折叠头)、SliverList / SliverGrid(.builder 懒加载)、SliverPersistentHeader(吸顶)、SliverToBoxAdapter(把普通 Widget 塞进 Sliver)、SliverFillRemaining。
ListView.builder 的复用原理?
ListView.builder 只构建视口内 + 缓存区(cacheExtent)的子项;滑出屏幕的 Element / RenderObject 会被回收复用给即将进入的项(类似原生 RecyclerView 的复用),因此长列表内存平稳、性能好。列表项顺序会变时记得加 Key。
十九、异步进阶
Completer 有什么用?
手动创建并控制一个 Future 的完成时机:把「基于回调的 API」桥接成 Future。
1
2
3
4
5
Future<T> toFuture(void Function(Function(T)) reg) {
final c = Completer<T>();
reg((v) => c.complete(v)); // 回调里完成
return c.future;
}
并发等待多个异步?
Future.wait([f1, f2]):全部完成才返回(结果列表),任一失败即抛错;Future.any([...]):最先完成的那个;- 需要各自容错时对每个
Future单独catchError。
Zone 是什么?
Zone 是异步执行的「上下文/沙箱」,可用来捕获整个异步调用链中未处理的异常(runZonedGuarded),或提供区域局部变量。常见于 App 入口统一兜底、埋点错误上报。
微任务与事件任务的顺序(经典输出题)
1
2
3
4
5
6
7
print('A');
Future(() => print('B')); // 事件队列
Future.microtask(() => print('C')); // 微任务队列
Future(() => print('D')).then((_) => print('E'));
print('F');
// 输出:A F C B D E
// 同步先跑(A F)→清空微任务(C)→依次取事件任务(B, 然后 D 及其 .then E)
二十、测试
- 单元测试:
test(),纯 Dart 逻辑;依赖用mockito/mocktail打桩。 - Widget 测试:
testWidgets()+WidgetTester,pumpWidget挂载、pump/pumpAndSettle推进帧,find+expect断言。 - 集成测试:
integration_test,真机/模拟器上跑完整流程。
1
2
3
4
5
6
testWidgets('计数器 +1', (tester) async {
await tester.pumpWidget(const MyApp());
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
二十一、构建、打包与包体积优化
Debug / Profile / Release 三种模式?
- Debug:JIT,支持热重载与断言,含调试信息,体积大、性能低。
- Profile:AOT,保留部分性能分析工具,用于性能测试(不能装模拟器上测)。
- Release:AOT,去断言/调试信息,最优性能与体积,上线用。
减小包体积的手段?
- 按 ABI 拆分:
flutter build apk --split-per-abi,或发appbundle让商店按机型下发; - 代码混淆并剥离符号:
--obfuscate --split-debug-info=<dir>; - 图标 tree-shaking(默认对 IconData 生效)、压缩图片、移除无用资源与字体子集化;
- 谨慎引入大依赖,检查
flutter build --analyze-size。
flavor / scheme?
多环境/多渠道(dev/staging/prod、不同包名/图标/配置)用 Android 的 productFlavors 与 iOS 的 scheme/configuration,Flutter 侧用 --flavor + --dart-define 注入环境变量。
二十二、屏幕适配
- MediaQuery:拿屏幕尺寸、像素密度、安全区(
padding/viewInsets,如键盘、刘海)。 - LayoutBuilder:拿到父约束再决定布局,做响应式。
- OrientationBuilder:横竖屏切换。
- 相对布局:
Expanded/Flexible/FractionallySizedBox/AspectRatio。 - 三方:
flutter_screenutil(按设计稿基准尺寸等比缩放.w/.h/.sp)。
二十三、手撕代码 / 原理深挖
为什么 build 方法里不能有副作用?
build 会被频繁、多次调用(每次 rebuild),且顺序、次数不可预期。如果在里面发网络请求、订阅、启动定时器,会造成重复请求、泄漏、状态混乱。副作用应放到 initState / didChangeDependencies / 事件回调里。
手写防抖(Debounce)与节流(Throttle)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 防抖:停止触发 delay 后才执行(搜索框输入)
class Debouncer {
final Duration delay;
Timer? _t;
Debouncer(this.delay);
void call(void Function() action) {
_t?.cancel();
_t = Timer(delay, action);
}
void dispose() => _t?.cancel();
}
// 节流:一段时间内最多执行一次(滑动/按钮防连点)
class Throttler {
final Duration interval;
Timer? _t;
Throttler(this.interval);
void call(void Function() action) {
if (_t?.isActive ?? false) return;
action();
_t = Timer(interval, () {});
}
}
手写一个简易 EventBus(基于广播流)
1
2
3
4
5
6
class EventBus {
final _controller = StreamController.broadcast();
Stream<T> on<T>() => _controller.stream.where((e) => e is T).cast<T>();
void emit(Object event) => _controller.add(event);
void dispose() => _controller.close();
}
setState 之后为什么不是「立即」刷新?
setState 只是把 Element 标记为 dirty,真正的重建发生在下一帧的 drawFrame。所以连续多次 setState 会被合并到同一帧处理,不会触发多次 build——这也是为什么在异步回调里 setState 要先判断 mounted。
const 构造函数为什么能优化性能?
const Widget 在编译期就完成实例化并被规范化(同值只存一份),父级 rebuild 时,const 子 Widget 引用不变,Widget.canUpdate 判定可复用,直接跳过该子树的重建。这是「无脑加 const」能提性能的原理。
以上为核心高频点的速记版。建议对每个点都能「结合源码或原理讲清楚 why」,而不仅是背结论——面试官通常会顺着三棵树、渲染流水线、事件分发、Isolate 这几条主线深挖。