IM · 移动端 / Flutter 面试题
B Flutter 域已有 44 篇系统文章 + 3 份速查(Riverpod / GoRouter / Freezed), 本篇绝大部分题目直接链过去,只补充域内没有的(Dart 手写、跨端原理对比、Isolate 通信、热更新)。
本项目技术栈:Riverpod 2.6 + GoRouter 14 + Freezed 2.5 + Retrofit 4 + Dio 5。
📌 章序:Dart → 渲染 → 布局 → 状态 → 路由 → 数据持久化 → 性能 → 平台能力 → 测试 → 监控 → 架构 → 工程化 → 跨端对比。
高频 Top 10
| # | 题目 | 去哪补 |
|---|---|---|
| 1 | Flutter 三棵树各自职责 | B03 |
| 2 | setState 之后发生了什么 | B03 |
| 3 | Widget 为什么不可变、Key 有什么用 | B02 |
| 4 | "约束向下、尺寸向上" 怎么理解 | B04 |
| 5 | RenderFlex overflowed 怎么解 | B04 / SB01 |
| 6 | 状态管理怎么选(Provider/Bloc/GetX/Riverpod) | B27 |
| 7 | Riverpod 的 watch/read/listen 区别 | 速查 06 |
| 8 | 列表卡顿怎么优化 | B22 |
| 9 | 为什么要用 const 构造函数 | B22 |
| 10 | Platform Channel 有哪几种 | B21 |
一、Dart 语言
Q:mixin 和 extension 的区别?(内联)
A:mixin 是可复用的类成员注入(with 混入,可 on 限定宿主类型,解决单继承的限制); extension 是给已有类型加语法糖方法(不改原类,编译期静态解析,不能被 override)。 典型:状态复用用 mixin(如 AutomaticKeepAliveClientMixin),工具方法用 extension。
Q:Future 和 Isolate 的区别?什么时候必须用 Isolate?
A:Future 是单线程内的异步(靠事件循环,不并发); Isolate 是真正的并行(独立内存堆,靠 ReceivePort/SendPort 消息通信,不共享内存)。 CPU 密集任务(大 JSON 解析、图片处理、加密)必须用 Isolate,否则会卡掉 UI 线程。
→ 详见 B06 · Dart 异步并发
Q:Isolate 之间怎么通信?(内联)
final receivePort = ReceivePort();
await Isolate.spawn(worker, receivePort.sendPort); // 把主 isolate 的 sendPort 传进去
receivePort.listen((msg) => print('收到 $msg'));
void worker(SendPort sendPort) {
final result = heavyCompute();
sendPort.send(result); // 只能传**可序列化**的值(基础类型/List/Map)
}💡 Flutter 提供了更简单的
compute(fn, message)(内部即 Isolate),适合一次性重任务。
Q:空安全里 late、?、! 怎么用?
A:Type? 可空;! 强制解包(不确定时别用,会抛异常); late 延迟初始化(声明时不赋值,首次访问才初始化,适合依赖注入/异步赋值的非空字段)。
→ 详见 B01 · Dart 语言基础
Q:手写 Dart 单例(内联)
class ApiClient {
ApiClient._();
static final ApiClient instance = ApiClient._(); // ✅ 静态字段天然懒加载 + 线程安全
factory ApiClient() => instance;
}二、渲染原理
Q:Flutter 的三棵树分别是什么?
A:Widget 树(配置/蓝图,不可变、频繁重建)→ Element 树(中间层,负责 diff 与复用) → RenderObject 树(真正布局与绘制,创建成本高、尽量复用)。 Widget 廉价重建,Element/RenderObject 尽量复用,这是 Flutter 性能模型的核心。
→ 详见 B03
Q:setState 之后发生了什么?
A:标记对应 Element 为 dirty → 下一帧重建 Widget → Element 树 diff(能复用就复用) → RenderObject 重新布局/绘制 → 合成上屏。 注意:setState 只重建该 State 对应的子树(不是整棵树)。
→ 详见 B03
Q:Key 有什么用?什么时候必须加?
A:Key 让框架在 Widget 重建时正确匹配旧的 Element/State。 列表中有状态组件且顺序会变时必须加(否则状态会跟错对象)——与前端 key 同理。 ValueKey(按值)/ ObjectKey(按引用)/ GlobalKey(跨树访问 State)。
→ 详见 B02
Q:Flutter 的生命周期有哪些?
A:initState → didChangeDependencies → build → didUpdateWidget → deactivate → dispose; 另有 didUpdateWidget、reassemble(热重载)。dispose 里必须清理:控制器、监听、定时器、订阅。
→ 详见 B03
三、布局
Q:怎么理解"约束向下、尺寸向上"?
A:父级给子级约束(min/max 宽高)→ 子级在约束内决定自己的尺寸 → 父级根据子级尺寸决定位置。 所以子级不能自己想要多大就多大;想突破约束要用 UnconstrainedBox/OverflowBox。
→ 详见 B04
Q:RenderFlex overflowed 怎么排查?
A:本质是子组件想要的尺寸超出父约束。 常见解法:给 Row/Column 里的长文本包 Expanded/Flexible;列表改 ListView; Row 里的 TextField 要包 Expanded;开启 DevTools 的 "Show Guidelines" 看约束。
Q:Sliver 是什么?什么时候用?
A:Sliver 是可被滚动容器按需懒加载的一块区域(SliverList/SliverGrid/SliverAppBar)。 需要"多个滚动区域协同、折叠头部、自定义滚动效果"时用 CustomScrollView + Slivers。
四、状态管理
Q:状态管理方案怎么选?
A:setState(局部、简单)→ Riverpod(本项目选择,编译安全、无 Context 依赖、易测试) → Bloc(强约束、事件驱动、大团队)→ GetX(轻量但易滥用、不推荐大型项目)。 判断维度:团队规模、是否需要强可追溯、测试要求、学习成本。
→ 详见 B27 · 状态管理选型与横向对比
Q:Riverpod 的 watch / read / listen 区别?
A:watch 值变了重建(用在 build);read 只取一次、不订阅(用在回调); listen 变化时不重建、只触发副作用(弹窗、跳转)。 回调里误用 watch 是高频错误。
→ 详见 速查 06 · Riverpod / B08
Q:AsyncValue 怎么处理异步三态?
A:when(data/loading/error) 全量处理;maybeWhen 只处理部分并用 orElse 兜底。 想"刷新时保留旧数据":先存旧值再置 AsyncLoading,或用 AsyncValue.guard。
→ 详见 速查 06
Q:Riverpod 里怎么减少无谓重建?
A:ref.watch(p.select((s) => s.field)) 只订阅用到的字段; 把消费者下沉到最小子树(用 Consumer 包局部而不是整个页面); 列表项抽成独立 Widget。
→ 详见 速查 06
五、路由
Q:go 和 push 的区别?
A:go 替换整个路由栈(适合底部 Tab 切换、登录后进主页); push 压栈可返回(适合列表进详情)。另有 replace(替换当前)、pop(出栈,可带返回值)。 出栈前建议判 context.canPop()。
→ 详见 速查 07 · GoRouter
Q:登录守卫怎么写?为什么登录后不自动跳转?
A:在 GoRouter.redirect 里判断登录态,未登录返回 /login。 必须配 refreshListenable(把登录状态传进去),否则登录成功后 redirect 不会重新触发。 同时要判断"当前已在登录页"以防无限重定向。
六、数据层与持久化
Q:本地存储怎么选?
| 方案 | 适用 | 特点 |
|---|---|---|
SharedPreferences | 少量 KV(配置、token、开关) | 简单;不适合大量数据,注意 iOS 上是异步 |
文件(path_provider) | 大块数据、图片/日志缓存 | 灵活,自己管格式与清理 |
SQLite(drift/sqflite) | 结构化、需要查询/关联/事务 | 强查询能力,drift 有编译期 SQL 校验 |
| Hive / Isar | 高频读写的对象存储 | 快、纯 Dart、无需 SQL;Isar 功能更强 |
安全存储(flutter_secure_storage) | Token、密钥等敏感数据 | 走 Keychain / Keystore |
选型判断:配置类 → SharedPreferences;结构化 + 复杂查询 → SQLite/drift; 对象缓存 + 高频读写 → Isar;任何密钥类一律走安全存储,别图省事放 SharedPreferences。
→ 详见 B18 · 本地存储与持久化
Q:数据层怎么分层?Repository 该做什么?
A:典型三层——DataSource(远程 API / 本地 DB)→ Repository(统一出口:缓存策略、合并、错误转换)→ 领域/状态层。 Repository 的职责是屏蔽数据来源:上层不知道数据来自网络还是缓存。 常见策略:先返回缓存再刷新(stale-while-revalidate)、失败回落缓存、写操作先更新本地再同步。
→ 详见 B09 · 数据层链路
Q:离线与缓存怎么设计?(内联)
A:四件事:① 缓存优先 + 过期策略(TTL 或版本号);② 写操作进待同步队列,恢复网络后重放; ③ 冲突处理(时间戳 / 版本号 / 服务端裁决);④ UI 要能表达"这是本地未同步的数据"。
七、性能
Q:Flutter 的帧预算是多少?掉帧怎么定位?
A:60fps → 16.67ms/帧;UI 线程(构建布局)与 Raster 线程(光栅化)都要在一帧内完成。 用 profile 模式(不是 debug)+ DevTools 的 Performance 与 "Track Widget Builds" 定位到具体 Widget。
→ 详见 B22
Q:常见的性能手段有哪些?
A:① 多用 const 构造(减少重建);② 把重建范围下沉(拆分 Widget、Consumer 局部消费); ③ RepaintBoundary 隔离重绘;④ 长列表用 ListView.builder 懒加载; ⑤ 图片指定 cacheWidth 控制内存;⑥ 避免在 build 里做重计算。
→ 详见 B22
Q:常见的内存泄漏点?
A:未 dispose 的 TextEditingController/AnimationController/ScrollController; 未取消的定时器与 Stream 订阅;全局缓存持有 BuildContext;图片缓存过大。
→ 详见 B22
八、原生与平台能力
Q:Platform Channel 有哪几种?
A:MethodChannel(方法调用,最常用)、EventChannel(原生主动推流,如传感器)、 BasicMessageChannel(双向消息)。注意线程:原生侧回调要回到主线程。
→ 详见 B21
Q:混合开发(Add-to-App)要注意什么?
A:引擎复用与预热、路由栈与原生导航协调、内存占用(多引擎 vs 单引擎)、 生命周期同步、包体积增量、调试链路。
→ 详见 B26
Q:权限与设备能力怎么申请?有哪些坑?(内联)
A:流程:在清单里声明(Android AndroidManifest.xml / iOS Info.plist 的用途说明)→ 运行时申请 → 处理"永久拒绝"。 坑:① iOS 没写用途描述会直接崩溃;② Android 13+ 细化了通知与媒体权限; ③ 被永久拒后系统不再弹框,要引导去设置页;④ 定位/相册有"仅使用时允许"与"仅限本次"两档。
→ 详见 B15 · 权限与设备能力
Q:推送与深链(Deep Link)怎么落地?(内联)
A:推送分 FCM/APNs 通道与厂商通道(国内必须接小米/华为/OPPO/vivo,否则收不到); 注意冷启动点击的处理、通知权限、token 上报与失效刷新。 深链:App Links(Android) / Universal Links(iOS) 需要服务端放 assetlinks.json / apple-app-site-association 做域名归属校验; Scheme 方式兼容好但会被劫持与弹窗询问。冷启动时深链要延后到首页就绪再跳转,否则路由栈未初始化会丢。
→ 详见 B24 · 推送通知与深链接
九、测试与质量
Q:Flutter 的三层测试怎么写?
| 类型 | 测什么 | 速度 |
|---|---|---|
| 单元测试 | 纯函数、Model、状态管理(Riverpod 可脱离 UI 测) | 最快 |
| Widget 测试 | 单个组件的渲染与交互(testWidgets + pumpWidget) | 中 |
| 集成测试 | 完整流程,真机/模拟器跑(integration_test) | 最慢 |
要点:Riverpod 用 ProviderContainer 做 override,可以不依赖 BuildContext 测逻辑; Widget 测试用 Finder 找组件,交互用 tester.tap + pumpAndSettle; 网络层用 mockito/mocktail 打桩,别在单测里打真实接口。
→ 详见 B19 · 测试实战
Q:覆盖率有意义吗?多少合适?(内联)
和前端同理:覆盖率是发现盲区的工具,不是 KPI。 优先保证纯逻辑与状态层的高覆盖,UI 层靠少量关键路径的 Widget/集成测试。 追求 100% 会把测试写成"实现快照",一改就红,反而拖垮迭代。
十、线上监控与崩溃
Q:Flutter 的错误怎么捕获?为什么有些错误抓不到?
A:三层都要接,缺一层就会漏:
| 层 | 做法 |
|---|---|
| Flutter 框架层 | FlutterError.onError(构建/布局/绘制异常) |
| Dart 异步层 | runZonedGuarded 的 onError(未捕获的异步异常) |
| 原生层 | iOS 的 NSException/信号、Android 的 Thread.UncaughtExceptionHandler(原生崩溃 Dart 层看不见) |
另外要把 ErrorWidget.builder 换成友好页面,避免线上红屏。
→ 详见 B20 · 异常捕获与错误监控
Q:崩溃上报后怎么定位?(内联)
A:关键是符号化——Release 包必须开 --obfuscate 并保留符号表文件, 拿到崩溃堆栈后用 flutter symbolize 还原成真实方法名。 再加:用户操作面包屑、日志、版本/机型/系统维度聚合。 没有符号表的混淆包,崩溃日志基本没法看——这是最容易漏的一步。
十一、架构分层与演进
Q:项目怎么分层?什么时候该重构?
A:常见演进路径:"全写在 Widget 里" → 抽 Provider/Notifier → 分数据层 → 分领域层。 分层原则:依赖向内、UI 不直接碰数据源、跨层走接口。 判断该重构的信号:一个 Widget 文件超过 300 行且混着网络请求、业务逻辑无法单测、改一处要在 5 个地方同步改。
→ 详见 B31 · 架构模式与分层演进 / B07 · 项目架构总览
Q:Clean Architecture 在 Flutter 里要不要上?(内联)
A:看项目规模。小项目全套 Clean(entity/usecase/repository/datasource 四层)会产生大量样板代码,得不偿失。 务实做法:先把"UI / 状态 / 数据"三层切干净,等业务复杂到出现"多个页面复用同一段业务规则"时,再抽 usecase 层。 判断标准:有没有"跨页面复用的业务规则",没有就别上。
Q:包与插件怎么抽?什么时候该做插件?(内联)
A:抽包时机:第三处用到才抽(Rule of Three)。 插件 plugin(含原生代码)vs 纯 Dart package(无原生)。 发布前要有:示例工程、README、CHANGELOG、单元测试、flutter analyze 干净、平台声明。
→ 详见 B30 · 包与插件开发
十二、工程化与发布
Q:Freezed 的 when 和 maybeWhen 区别?
A:when 必须穷尽所有分支(新增分支时编译报错,安全); maybeWhen 用 orElse 兜底(新增分支不会报错,注意漏改)。状态机建议用 when。
→ 详见 速查 08 · Freezed
Q:build_runner 报冲突怎么办?
A:build --delete-conflicting-outputs;仍不行就 clean 后重建。 嵌套对象序列化报 Converting object to an encodable object failed → 开 explicitToJson。
→ 详见 速查 08
Q:包体积怎么优化?
A:--split-per-abi/--obfuscate + 符号表、移除未用资源、字体子集化、压缩图片(WebP)、 --tree-shake-icons、延迟加载组件(deferred components)。
→ 详见 B23
Q:国际化(i18n)怎么做?有哪些坑?(内联)
A:主流用 flutter_localizations + ARB 文件(intl / flutter_intl 生成), 语言切换要重建顶层 Widget 并持久化选择。 坑:① 复数/性别/日期与数字的本地化格式(用 intl 的 NumberFormat/DateFormat,别手写); ② 文案里有参数要用占位符,别字符串拼接(语序在不同语言里不同); ③ 从右到左语言(RTL)需要 Directionality 与布局双向测试。
→ 详见 B14 · 国际化
Q:发布流程有哪些必须卡的点?(内联)
清单:版本号与 build number 递增 → 混淆并归档符号表 → 分 ABI 包 → 多机型回归(至少 iOS + 一个低端安卓)→ 灰度(按百分比/渠道)→ 监控崩溃率与关键指标 → 保留回滚能力。 国内安卓还要各应用商店的渠道包与合规(隐私清单、权限说明)。
十三、跨端对比
Q:Flutter 和 RN / uni-app 的本质差异?
A:Flutter 自绘(Skia/Impeller 直接渲染,不依赖原生控件)→ 跨端一致性最好、性能好,但包体积大、生态需自建; RN 用原生控件(JS 桥接/JSI)→ 体积小、接近原生观感,但跨端一致性差、升级易踩坑; uni-app 是 WebView/小程序方案,生态与上手成本低,性能与体验上限较低。
→ 详见 A01 · 跨端方案横向对比
Q:Flutter Web 有什么限制?为什么不适合做需要 SEO 的站点?
A:编译产物是 JS + canvas/DOM 渲染器,天然只能 CSR,没有服务端 HTML 生成, 爬虫与首屏体验都不友好。另外有 CORS、路由 fallback、首屏体积三个必踩的坑。
十四、进阶补充主题(新题)
Q:Flutter 的无障碍是什么?为什么"视觉还原"不等于"无障碍达标"?
A:Flutter 每一帧会从 Widget 树推导一棵语义树(Semantics Tree),读屏只读这棵树。 内建 Text / IconButton 自带部分语义,但纯图标按钮、图片、自定义绘制、手势都不会自动有语义—— 要用 Semantics / MergeSemantics / excludeSemantics 手动补。视觉还原只证明"看起来对",不代表读屏用户能用。
→ 详见 B35 无障碍
Q:App 被系统杀掉进程后,状态怎么恢复?
A:默认不恢复,要用 RestorationMixin + RestorableProperty(RestorableInt / RestorableTextEditingController 等) 在 RestorationScope 下注册。注意 Riverpod 的 Provider 状态在内存里,进程被杀就没了,需要恢复的是用户可见关键态 (Tab、输入、滚动),跨会话数据仍走本地存储。
→ 详见 B36 状态恢复
Q:Flutter 怎么做后台任务?定时器能在后台一直跑吗?
A:不能。三层能力要分清:Isolate(App 活着时并行计算)、workmanager(系统调度,Android 受 Doze、iOS 由 BGTask 决定时机)、 推送(实时唤醒)。"每天提醒"应走本地通知/服务端推送,而不是后台轮询;保活要靠厂商推送通道,不是前台 Service 硬扛。
→ 详见 B37 后台任务
Q:埋点的关键设计点有哪些?
A:先定事件字典(统一 schema)→ 用事件总线 + Provider 统一入口 → 批量上报 + 离线补偿 + 去重 → 曝光不等于进页面(要判定可见)→ 隐私合规(先授权后采集、最小化、可关闭)。
→ 详见 B38 埋点
Q:Flutter 怎么和 IoT 设备通信?能源 App 怎么选?
A:三条链路:BLE(近场配网,flutter_blue_plus,注意 Android 权限分裂与并发写入排队)、 MQTT(云端遥测,QoS / 遗嘱 / retain,配退避重连)、Modbus(工业设备,寄存器映射 + CRC)。 统一抽象 DeviceChannel,离线判定靠心跳 + 遗嘱,告警走服务端推送。
→ 详见 B39 IoT 协议
Q:Flutter 图表选型怎么定?
A:常规折线/柱状/饼图用 fl_chart;数据量大/视觉要完全控制用 CustomPainter 自绘; 只有超复杂图表(桑基/3D/地图)才考虑 WebView + ECharts(桥接成本高)。配色要接 AppTokens,空态要占位。
→ 详见 B40 图表可视化
Q:Flutter 支持 Shader 吗?什么时候用?
A:支持 FragmentShader + GLSL(.frag 资源),适合图表水波、背景动效等增强;但它是逐像素计算, 性能敏感,全屏常驻要慎重。注意 Skia / Impeller / Web 的 Shader 差异,必须真机验证。
→ 详见 B41 Shader
Q:移动 App 安全纵深有哪些层?
A:传输层(SSL Pinning + 证书轮换)→ 存储层(flutter_secure_storage,密钥不进 Dart 源码)→ 代码层(R8 混淆 + 加固,混淆不是安全边界)→ 运行时(root/越狱检测、反调试,关键判定下沉原生)。
→ 详见 B42 移动安全
Q:Flutter Monorepo 是什么?什么时候不该用?
A:Melos / pub workspace 管理"App + 内部包"在同一仓库协同演进(melos bootstrap 链接、melos run 批量命令)。 但单 App 无复用诉求、团队无 CI/发版规范时,硬拆多包反而增加成本。
→ 详见 B43 Monorepo
Q:Flutter 能线上热更新吗?
A:没有官方一键热更新。iOS 禁止动态下发可执行代码,Android 技术上可行但链路成本高。 绝大多数"不想发版"的需求用远程配置 + 灰度解决(缓存 / 兜底 / 灰度 / 埋点四要素),代码热更新是最后选项。
→ 详见 B34 动态化与热更新
自检清单
- [ ] 能讲清三棵树与
setState之后的完整流程 - [ ] 能解释"约束向下、尺寸向上"并会解溢出报错
- [ ] 能说清 Riverpod 的
watch/read/listen与select优化 - [ ] 能列出 5 条性能手段与 4 个内存泄漏点
- [ ] 能说清 Flutter 与 RN / uni-app 的本质差异,以及 Flutter Web 的限制
- [ ] 能说清本地存储的选型(SharedPreferences / SQLite / Isar / 安全存储)与数据分层
- [ ] 能说清 Flutter 错误的三层捕获与符号化还原
- [ ] 能给出测试金字塔与"覆盖率不是 KPI"的判断
- [ ] 能说清分层的演进路径,以及什么时候不该上 Clean Architecture
- [ ] 能说清无障碍语义树、状态恢复、后台任务与埋点的核心设计
- [ ] 能给出移动安全纵深与 IoT / 图表 / Shader / Monorepo 的选型结论
相关文档
- I00 · 面试题总览
- B 域 · Flutter 学习文档 —— 44 篇(本篇的链源)
- 速查 06 Riverpod · 07 GoRouter · 08 Freezed
- IX · 综合场景题 —— 系统设计 + 排障
- IH · 软技能与行为面试 —— 项目深挖与技术决策