Flutter 202608面试总结
Flutter 202609面试总结
根据202608左右的纯Flutter岗位的面试记录和总结
一、核心结论
Widget是不可变配置;Element是树中的持久实例;RenderObject负责 layout、paint、hit test、semantics。- rebuild、layout、paint、raster 是不同阶段,前者发生不代表后者必然发生。
setState()同步修改State,并将对应Element标记为 dirty;Widget 不会被标脏。- 布局口诀:约束向下传递,尺寸向上传递,父节点决定位置。
- Element 复用条件:新旧 Widget 的
runtimeType与key都相同。 mounted只能保证 State/Context 尚未卸载,不能解决请求乱序或导航栈变化。- 不要在
build()中创建 Future 或 Stream,否则 rebuild 可能重复启动异步任务。 - Isolate 默认不共享可变内存;普通网络等待是异步 I/O,通常不需要 Isolate。
RepaintBoundary是重绘边界,不等于一定离屏绘制或一定缓存。ListView.builder的核心是懒构建,不是缓存全部 item。Get.put立即创建;Get.lazyPut首次查找时创建;Get.create每次查找都新建。debounce是停止变化后执行;interval是持续变化时每个时间窗口最多执行一次。final是只能赋值一次;const是编译期常量并具有深层不可变性。implements不复用实现;mixin通过with横向复用实现。
二、Event Loop 与 Microtask
1 | |
“每个 Event 前先清空 Microtask Queue”在结果上基本正确;更严谨地说,当前同步任务或 Event 结束后形成 microtask checkpoint,Microtask 清空后才进入下一个 Event。
如果 Microtask 持续产生新的 Microtask,Timer、I/O 回调等 Event 可能长期无法执行。
三、三棵树与更新流程
Widget、Element、RenderObject
Widget:轻量、不可变的 UI 配置。Element:树中的持久实例;实现BuildContext;管理父子关系、State 生命周期和 rebuild。RenderObject:负责布局、绘制指令、命中测试和语义,不负责光栅化。
Widget 是不可变配置;Element 负责关联 Widget、State 和 RenderObject,并协调更新;RenderObject 负责 layout、paint、hit test 和 semantics。光栅化由 Engine 的 Raster Thread 完成。
setState 链路
1 | |
build() 是构建阶段,不是绘制阶段。在其中随意 setState() 可能违反构建约束或导致重复构建。
Element 复用
1 | |
位置是父 Element 协调子节点时的匹配基础,但不是 Widget.canUpdate 的直接判断项。
四、布局约束
1 | |
Column + ListView
Column 沿主轴布局非 Flex 子节点时通常给出无界高度;ListView 作为滚动视口需要有限高度来确定可视区域,因此无法完成布局。
不是“父组件满足不了子组件”,而是:
父组件没有提供子组件完成布局所需的有限约束。
1 | |
shrinkWrap: true 可按内容计算高度,但长列表成本较高。
五、Key、const 与 RepaintBoundary
Key
ValueKey:按值的==比较,常用业务 ID。ObjectKey:按对象身份identical比较。UniqueKey:每个实例不同;每次 build 新建会阻止复用。GlobalKey:全局唯一,可访问 State/Context,也可跨父节点移动同一子树。
列表无 Key 时默认按位置和类型匹配。插入、删除、换序可能使旧 State 对应到错误数据,造成输入框、勾选、动画、播放状态错位。
GlobalKey 会增加耦合、全局注册和子树移动成本。父子通信优先回调或 Controller,跨层状态优先状态管理。
const
相同参数的 const 构造会被规范化为同一实例。父组件 rebuild 时:
1 | |
框架可跳过对子 Element 的更新。但 const 子组件仍可能因自身 setState() 或 InheritedWidget 变化而 rebuild;父级约束或绘制环境变化时也仍可能 layout、paint。
RepaintBoundary
它创建绘制边界和独立 Layer,使 markNeedsPaint() 的脏标记传播到最近边界为止。适合高频变化区域与复杂静态区域相邻的场景。
它不保证离屏渲染或 Raster Cache。过多使用会增加 Layer、内存、显存和合成成本。
const 减少 Widget/Element 更新;RepaintBoundary 隔离 paint。
六、InheritedWidget 与生命周期
InheritedWidget
1 | |
该调用找到最近的 InheritedElement,并将当前 Element 注册为 dependent。updateShouldNotify(oldWidget) 只判断是否通知依赖者,不负责建立依赖。
返回 true 后,仅通知已注册的 dependent,触发 didChangeDependencies() 并标记 rebuild。
StatefulWidget 生命周期
1 | |
initState():每个 State 一次,不能声明为async。didChangeDependencies():首次也会执行。didUpdateWidget():Element 复用但 Widget 配置变化时执行。deactivate():暂时离树,不代表销毁。dispose():释放 Controller、FocusNode、Stream、Observer 等资源。
七、异步、mounted 与 BuildContext
BuildContext 本质上是 Element 暴露的接口,表示 Widget 在 Element Tree 中的位置。
1 | |
mounted 只保证 Element 尚未卸载,不能保证:
- 返回的是最新请求;
- 当前路由仍在顶部;
- 页面参数或业务状态未变化。
请求乱序:
1 | |
处理方式:请求序号、取消旧请求、返回时校验参数。
异步后 pop 还要检查路由:
1 | |
八、FutureBuilder 与 StreamBuilder
不要在 build() 中创建 Future,因为 rebuild 会产生新 Future 并可能重复请求。
1 | |
FutureBuilder 切换 Future 不会取消旧 Future 的底层网络任务。
- Future:一次完成结果。
- Stream:多个 data、error、done 事件。
StreamBuilder 内部管理订阅:Widget 移除时取消;Stream 实例变化时取消旧订阅并订阅新实例。取消订阅能否停止数据源取决于 Stream 实现。
不要在 build() 中创建 Stream,否则可能反复取消、重新订阅或重启底层任务。
九、Isolate
每个 Isolate 有独立 Heap、Event Loop、Microtask Queue 和全局状态。Isolate 不等于固定的一条系统线程,Runtime 会负责调度。
Isolate 默认不共享可变内存,通过 SendPort、ReceivePort 传递消息;大块二进制可使用 TransferableTypedData。
适合 CPU 密集任务:大型 JSON 解析、图片处理、加密、压缩、大量计算。
不要用“超过 100ms”作为固定标准。60Hz 一帧约 16.67ms,应结合 Profile 数据和创建、消息传递成本判断。
普通网络等待是异步 I/O,不需要 Isolate;响应后的大型解析可能需要。
十、帧渲染与性能排查
一帧流程
1 | |
每帧不会重新挂载整棵 Element Tree,只处理 dirty 节点。
卡顿分类
UI 卡顿常见原因:
- 大范围或高频 rebuild;
- 昂贵 layout;
- Dart 同步计算、JSON 解析;
- 动画或事件回调耗时。
Raster 卡顿常见原因:
- 大图、模糊、阴影、复杂路径;
saveLayer、离屏渲染;- Shader 编译;
- 过度绘制。
Opacity、裁剪不是必然昂贵,要看是否产生额外 Layer 或离屏渲染。
DevTools
Performance Timeline:区分 UI 与 Raster 阶段耗时。Performance Overlay:实时显示 UI、Raster 两条耗时图。Track Widget Builds:记录 Widget build,定位频繁或耗时构建。Repaint Rainbow:用颜色显示重绘区域,不用于检查 Widget 销毁。
十一、长列表
ListView.builder 按需构建可视区域及 cacheExtent 附近的 item,远离屏幕的 item 通常会被回收。
itemExtent:指定固定高度,减少测量和位置计算。prototypeItem:用原型测量统一高度。cacheExtent:预构建视口前后一定距离;过大会增加内存和构建成本。
状态保留:
- 业务状态,如输入内容、播放位置:优先保存到数据源。
- 短期保留 State:
AutomaticKeepAliveClientMixin。
大量视频播放器不宜全部保活。应在离屏时暂停、释放播放器并保存播放位置,重新进入时恢复。
十二、GetX
GetBuilder 与 Obx
GetBuilder:调用update()手动通知,适合低频或批量刷新。Obx:构建时自动收集其中读取的 Rx 依赖,Rx 变化后自动 rebuild。.obs:把普通值转换为 Rx,不是.obx。update([id]):只刷新对应 id 分组;不传 id 通常通知相关 GetBuilder。
大范围 Obx 会使任意依赖变化重建整个包裹区域,应缩小监听范围。
Controller 注册
Get.put:立即创建并注册,后续通常复用。Get.lazyPut:注册工厂,首次Get.find时创建,之后复用。Get.create:每次Get.find都创建新实例。permanent: true:阻止自动释放,适合应用级服务。fenix: true:实例释放后保留工厂,下次Get.find时重新创建。
Bindings
Bindings.dependencies() 与 Flutter 的 didChangeDependencies() 无关。
1 | |
不要在 build() 中 Get.put():构造表达式可能反复执行,且实例所有权、生命周期、释放时机不明确。
Bindings 的价值:集中依赖、支持懒加载、关联路由生命周期、便于 Mock 和测试。
Controller 生命周期
onInit():初始化数据、Worker 和订阅。onReady():onInit后下一帧,适合导航、弹窗等页面就绪操作。onClose():Controller 删除前释放 Worker、Timer、Stream 等。
页面关闭不一定触发 onClose(),取决于注册方式、SmartManagement、permanent 等配置。
Worker
ever:每次 Rx 变化都执行。once:第一次变化时执行一次。debounce:停止变化一段时间后执行,适合搜索框。interval:持续变化时每个时间窗口最多执行一次,适合滚动上报、防连点。
1 | |
内存泄漏
泄漏不是“持续写数据”,而是对象已不需要却仍被引用,无法被 GC。
常见原因:
permanent滥用;- Worker、Timer、StreamSubscription 未释放;
- Controller、FocusNode 等未释放;
- 静态变量、单例或闭包长期持有页面对象;
- tag 错误导致重复注册。
排查方法:DevTools Memory 的 Heap Snapshot、Diff、Retaining Path,并确认 onClose() 是否执行。
十三、Dart 语言
空安全
T?:允许为 null。!:空值断言,不是保证不为空;值为 null 时运行期报错。late:延迟初始化的非空变量,不一定是懒加载。required:命名参数必须显式传入,不代表值非空,如required String? name。
风险:
- 滥用
!:把编译期问题推迟成运行期异常。 - 未初始化就读取
late:LateInitializationError。 late final重复赋值:运行期报错。
泛型协变
若 Cat extends Animal,Dart 的泛型协变允许:
1 | |
但实际对象仍是 List<Cat>。执行:
1 | |
编译期按 List<Animal> 看似允许,运行期发现实际列表要求 Cat,因此抛出类型错误。
final 与 const
final:变量只能赋值一次,值可在运行时确定。const:编译期常量,对象深层不可变并可被规范化。
1 | |
extends、implements、mixin
extends:单继承,可复用父类字段和方法实现,适合明确的is-a关系。implements:只采用接口契约,不复用实现;需实现全部实例成员,包括方法、getter、setter。mixin:通过with横向复用实现,一个类可混入多个 Mixin;可用on限制适用父类型。
1 | |
extension
extension:不修改原类,为已有类型补充便捷方法,无需with。mixin:将字段和方法实现混入类,成为实例能力的一部分。- 扩展方法采用静态分派,根据表达式的编译期类型选择扩展。
- 原类存在同名实例方法时,实例方法优先。
- 扩展方法不进入原类虚方法表,因此不能真正重写实例方法。
- 接收者为
dynamic时,不会通过静态扩展查找扩展方法。
十四、第 33 题预习:静态分派与动态分派
静态分派
编译期根据表达式的静态类型确定目标,例如 Dart 扩展方法。
动态分派
运行期根据对象的实际类型选择被重写的方法:
1 | |
变量静态类型是 Animal,实际对象是 Cat。实例方法进入虚方法分派,运行时会调用 Cat 的重写实现,这就是多态。
字段访问通常不具有相同的动态多态语义;Dart 的实例变量会隐式产生 getter/setter。面试回答时应区分:
- 方法重写:运行时动态分派。
- 同名字段声明:不是传统意义上的字段重写,避免依赖“字段多态”。
- 若要多态获取属性,应显式定义并重写 getter。
十五、CHECK ME LIST
- 三棵树:Widget 配置、Element 持久实例、RenderObject 布局绘制。
- 更新:
setState → Element dirty → build → 按需 layout/paint/raster。 - 布局:Constraints down,Sizes up,Parents set positions。
- Key:类型和 key 相同才能复用;列表换序用稳定业务 Key。
- 异步:mounted 只防卸载,不防请求乱序和路由变化。
- Builder:Future/Stream 在生命周期中创建,不在 build 中创建。
- Isolate:CPU 密集才考虑;网络等待本身不需要。
- 性能:先用 Profile/Timeline 区分 UI 卡顿与 Raster 卡顿。
- GetX:GetBuilder 手动,Obx 响应式;put 立即、lazyPut 首次、create 每次。
- Dart:
!是断言;late是延迟初始化;final单次赋值;const编译期深层不可变。 - 泛型:
List<Cat>可协变成List<Animal>,错误写入在运行期检查。 - 继承:extends 复用父类,implements 只要契约,mixin 横向复用。
