面试备战:客户端混合架构、AI 助手与复杂业务性能优化项目复盘
面试备战:客户端混合架构、AI 助手与复杂业务性能优化项目复盘
这篇不是单纯罗列项目经历,而是把简历里的几个核心项目整理成面试时真正能讲清楚的版本。
面试官看项目经历,通常不会只问“你做了什么页面”。更高频的问题是:
- 这个项目的业务背景是什么?
- 你在里面承担了什么角色?
- 为什么选择这个技术方案?
- 这个方案相比其他方案有什么优势和弊端?
- 性能问题怎么定位?优化前后有什么变化?
- 如果重做一次,你会怎么改?
所以项目介绍不要讲成流水账,而要讲成一条清晰的技术主线:
我在 Flutter + Native 混合架构下,负责业务模块从 0 到 1 落地、AI 流式能力接入、复杂列表性能优化、Native 能力复用和端侧工程化治理。我的价值不是单纯写 Flutter 页面,而是能在 Flutter、Native、容器、路由、网络、通信、图片、硬件协议和性能监控之间做技术取舍。
1. 总体开场话术
如果面试官让你“介绍一下最近几年做的项目”,可以先用 30 秒建立整体印象:
我近几年主要做 Flutter + Native 混合架构下的客户端业务和性能治理。贝壳阶段主要负责贝壳精工、HOMEHD、贝壳找房等多端产品的 Flutter 业务落地和架构演进,比如工地管理 AI 助手“小师父”、预勘报告复杂表单、工地筛选动态化、HOMEHD iPad 展厅经理带看助手,以及贝刻手环这类 Native + Flutter + BLE 硬件项目。我的工作重点不是单纯开发页面,而是解决混合工程里的页面路由、网络复用、Native 通信、流式渲染、复杂列表定位、大图内存和硬件协议适配问题。
这段话的重点有三个:
- 先把自己定位成“混合架构客户端工程师”,而不是“Flutter 页面开发”。
- 主动抛出项目关键词,让面试官有追问入口。
- 把项目和能力绑定起来:AI、性能、Native、通信、工程化。
2. 贝壳精工:Flutter + Native 混合架构
贝壳精工可以作为你整套项目经历的主线项目。
可以这样讲:
贝壳精工是一个典型的 Native 壳 + Flutter Portal + Flutter Package 的混合架构项目。Native 壳负责 App 生命周期、登录态、网络、安全、埋点、系统能力和历史业务;Flutter Portal 负责 Flutter 引擎初始化、页面注册、性能监控和基础能力注入;业务能力沉淀在 Flutter Package 里。这样设计的好处是,Flutter 可以快速承接新业务页面,但登录、网络、安全、路由、埋点这些成熟能力仍然复用 Native 基建。
对应到工程结构,可以抽象成:
Native App
-> Flutter Portal
-> 注册多个业务 Package 的 urlPageMap
-> Flutter 业务页面
Flutter 页面
-> RequestTools
-> MethodChannel
-> Native 网络层
-> 网关、鉴权、签名、Cookie、环境、监控
这个项目的关键技术决策有三个:共享 Runner/Engine 接入,不用 go_router,不直接用 Dio。
2.1 为什么不用 go_router?
可以这样讲:
我们项目不是一个纯 Flutter App,而是一个多业务线、多容器、多页面来源的混合工程。页面可能来自 Native、Flutter、H5,也可能来自后端下发的 URL。我们使用的是 URL Scheme + FlutterRunner 容器 + urlPageMap 的路由方式。Flutter Package 只负责把业务 URL 映射到 PageBuilder,跨端跳转统一交给 Native/Runner 容器处理。
内部 Flutter 页面跳转可以理解为:
业务 URL
-> urlPageMap[url]
-> PageBuilder
-> Navigator.push(MaterialPageRoute)
跨 Native/Flutter 容器跳转可以理解为:
业务 URL
-> 包装为 flutter container URL
-> FlutterRunnerHelper.openPage
-> Native 容器打开 Flutter 页面
这套方案的优势:
- 和公司已有 Native/H5/Flutter 路由体系兼容。
- 后端、Native、Flutter 可以共享 URL 语义。
- Flutter Package 不需要关心完整 App 页面栈,只要注册业务页面。
- 适合历史包袱较重的大型混合工程做渐进式 Flutter 化。
这套方案的弊端也要主动说:
- 字符串路由缺少编译期检查。
- 参数多用 Map,类型安全弱。
- unknown route、参数校验、路由可观测性需要额外治理。
- 相比 go_router,声明式嵌套路由、Web URL 和 typed route 能力弱。
面试追问时可以补一句:
如果是一个新的纯 Flutter App,我会优先考虑 go_router;但在这个项目里,核心问题不是 Flutter 内部导航,而是 Native、Flutter、H5 和后端 URL 的统一路由语义,所以自研 URL 路由更符合当时的工程约束。
2.2 为什么不用 Dio?
可以这样讲:
我们 Flutter 侧没有直接接 Dio,而是通过 RequestTools 调 MethodChannel,把请求交给 Native 网络层。原因是 Native 壳已经有成熟的登录态、token、签名、Cookie、网关域名、环境切换、统一错误码、安全策略和监控。如果 Flutter 再单独接 Dio,就会形成两套网络栈,登录态和风控参数很容易不一致。
架构可以概括为:
Flutter RequestTools.get/post
-> HomeFlutterPluginAdapter.netGet/netPost
-> MethodChannel
-> Native Plugin
-> Native Network Service
-> 业务网关
优势:
- 登录态、鉴权、签名、Cookie 统一。
- Native、H5、Flutter 网络行为一致。
- Flutter 业务包更轻,不重复建设网络基础设施。
- 安全、环境、监控、错误码可以在 Native 侧统一治理。
弊端:
- MethodChannel 多一层桥接。
- 请求取消、拦截器、重试、单测不如 Dio 直接。
- JSON 字符串解析和调试链路更长。
- SSE、上传、下载这类复杂请求需要额外设计。
面试里要讲清楚这是“取舍”,不是“不懂 Dio”:
Dio 很适合纯 Flutter 或 Flutter 主导的业务工程。但在强 Native 基建的大型混合 App 里,把网络收敛到 Native,可以减少重复建设和安全风险。我的判断标准是:谁拥有登录态、安全和网关治理能力,网络主链路就应该优先收敛到谁。
2.3 我们项目是多 Engine 还是单 Engine?
这个问题不能简单回答“单 Engine”或“多 Engine”,更准确的说法是:
我们主业务 Flutter 页面走的是共享 Runner / 共享 Engine 模式,不是每打开一个 Flutter 页面就创建一个独立 Engine。但从整个 App 角度看,少数特殊页面会走非共享容器,例如包含 Flutter Web 插件的页面会把
isShare设为NO,同时壳工程里还存在LJWBFlutterRunner这类 Web 插件 Runner。所以面试里我会说:主业务是单共享 Engine 思路,局部特殊场景允许非共享。
从代码链路看,Flutter Portal 的 main 入口只负责初始化基础能力、注册页面和 runApp(AppHome())。Native 侧通过 BKRunnerShared sharedInstance 配置插件注册和入口:
Native AppDelegate
-> LJBeikeFlutter didFinishLaunching
-> BKRunnerShared configRegisterBlock(entryPoint: "main")
-> GeneratedPluginRegistrant
-> DWLifeFlutterPlugin / DWHomeFlutterPlugin
Native 打开 Flutter 页面
-> BKRunnerShared.sharedInstance.router.container/open
-> Flutter Portal
-> FlutterRunnerHelper 注册的 urlPageMap
-> 业务 Package PageBuilder
这说明业务侧并没有自己维护一个 FlutterEngine 池,也没有在每个页面里手动 new Engine。普通业务页面通过共享 Runner 创建 BKFlutterViewController,并通过 URL 找到 Flutter 侧注册的页面。
但 DWHomeFlutterHelper、DWLifeFlutterHelper 里还有一个关键参数:
BOOL isShare = ![self isContainsFlutterWebPlugin:pageUrl];
BKFlutterViewController *flutterViewController =
[[BKRunnerShared sharedInstance].router container:pageUrl
urlParams:result
exts:@{}
isShare:isShare];
这里的设计含义是:大多数普通 Flutter 业务页 isShare = YES,复用共享容器;但如果页面包含 Flutter Web 插件,就把 isShare 设为 NO,避免 Web 插件、页面状态或特殊生命周期污染主业务共享 Engine。
这套设计的优势:
- 内存更可控。相比每个页面一个 Engine,共享 Engine 可以显著减少 Dart VM、Skia、插件实例和资源缓存的重复开销。
- 首开体验更稳定。Engine 预热后,后续 Flutter 页面打开成本更低。
- 插件注册统一。登录、网络、埋点、路由、Native 能力通过同一套 Runner 注入,避免多 Engine 下插件状态不一致。
- 页面协同更方便。Flutter Package 只注册 URL 到 PageBuilder,页面打开、关闭、回调和 Native 栈交互由 Runner 统一处理。
弊端也要主动说明:
- 隔离性弱。多个业务页共享同一个 Dart 运行环境,全局单例、缓存、事件总线如果治理不好,会互相影响。
- 生命周期更复杂。页面 push/pop、Native 容器释放、Flutter route 状态需要 Runner 做统一治理。
- 故障影响面更大。共享 Engine 中出现严重异常,可能影响同一 Engine 内其他 Flutter 页面。
- 特殊能力要单独处理。比如 Web 插件、强隔离页面、重资源页面,就需要通过非共享容器或特殊 Runner 兜底。
面试回答可以收束成一句:
我们不是典型的多 Engine 架构,也不是绝对意义上整个 App 只有一个 Engine。主业务 Flutter 是共享 Runner / 单共享 Engine 的接入方式,用来换取内存、首开和插件一致性;但对包含 Web 插件这类特殊页面,会通过
isShare = NO走非共享容器,牺牲一部分资源换隔离性。
3. 工地管理 AI 助手“小师父”
这是最能体现你“AI 客户端集成能力”的项目。
可以这样讲:
小师父是贝壳精工 App 内的工地管理 AI 助手,我负责端侧架构设计和关键模块落地。这个项目的难点不是普通聊天 UI,而是要把服务端 SSE 流式响应稳定地转成端侧可渲染的多类型消息,包括文本、思考过程、进度卡、列表卡、工单卡、评分卡、猜你想问等,并且要处理弱网、断流、重连、历史消息和交互卡片状态。
端侧架构可以拆成四层:
SseService
-> 负责会话初始化、业务接口、SSE 入口
LittleMasterHttpClient
-> 自定义 text/event-stream 解析
-> 处理 id/event/data/retry
LittleMasterConnectionProvider
-> 管连接状态、断流重连、错误流、手动重连
SseConversationManager + SseJsonParserRegistry
-> 按 sseId 维护会话
-> 按事件类型和消息类型解析为 UI 模型
3.1 为什么自定义 SSE Client?
可以这样讲:
一开始也可以考虑直接用第三方 SSE 库,但 AI 助手场景对连接状态、POST body、token header、断流恢复、业务 close 事件和消息解析控制要求比较高。我们最终做了自定义 SSE Client,直接用 HttpClient 建立长连接,按 SSE 协议解析行数据,把
id、event、data、retry转成统一的 SseEvent。
关键点:
- 支持 POST SSE,不只是 GET。
- 明确设置
Accept: text/event-stream。 - 请求体使用 UTF-8 JSON 编码。
- 支持
Last-Event-ID的扩展空间。 - 接收到空行时认为一个事件结束。
- 对服务端异常行、非 200、非 event-stream content-type 做日志和错误处理。
3.1.1 自研 SSE Client 的优势是什么?
可以这样讲:
自研 SSE Client 的核心优势不是“自己造轮子”,而是把 AI 流式场景里最关键的连接控制权、协议解析权和业务状态权拿回来。第三方库通常能完成基础订阅,但很难完全贴合我们这种 POST SSE、Native token、业务 close 事件、多类型卡片、弱网重连和端侧埋点监控的组合场景。
具体优势可以拆成六点:
- 连接生命周期可控:可以明确区分连接中、已建立、已收到首包、正常关闭、异常断开、手动取消和重连中,而不是只拿到一个泛化的 stream。
- 请求协议可控:支持 POST body、业务 header、token、
Content-Type、Accept: text/event-stream,也能按公司网关要求调整请求格式。 - 解析行为可控:逐行解析
id/event/data/retry,可以处理多行 data、空行结束事件、异常行日志、非 200 错误、非 event-stream 响应等边界。 - 重连策略可控:底层 Client 可以禁用自动重连,把重连交给 ConnectionProvider。因为业务层更清楚这次请求是否已经收到数据、是否正常 close、是否用户主动 stop。
- 业务观测可控:可以在连接建立、首包到达、事件解析、断流、重连、关闭等节点打日志和埋点,方便定位线上弱网和服务端异常。
- 模型演进可控:底层只产出标准化
SseEvent,上层 Parser Registry 再扩展卡片类型。协议层和业务层解耦,后续新增消息类型不会反向污染连接层。
面试时可以补一句:
如果只是简单的服务端推送文本,第三方库足够;但小师父不是纯文本流,而是 AI 会话、思考过程、多卡片协议、用户交互、断流恢复和 Native 登录态共同组成的业务流。所以自研 Client 的价值在于可控性和可观测性,而不是重复实现基础 API。
3.1.2 为什么不用 WebSocket?
可以这样讲:
小师父这个场景更适合 SSE,而不是 WebSocket。因为它的通信模型本质是“客户端发起一次问题,服务端持续返回一段生成结果”。主要数据流是服务端到客户端的单向流式输出,客户端并不需要在同一条连接上高频双向通信。SSE 正好适合这种 HTTP 请求驱动的服务端单向流。
SSE 更适合这个项目的原因:
- 通信模型匹配:AI 问答是 request -> streaming response,不是实时多人协同或高频双向消息。
- HTTP 基建复用好:SSE 基于 HTTP,可以复用现有网关、鉴权、日志、代理、超时和监控体系。
- 服务端实现更简单:服务端按
text/event-stream持续 flush 数据即可,不需要维护 WebSocket 会话状态和自定义双向协议。 - 移动端链路更容易排查:HTTP 状态码、header、body、网关日志都比较标准,问题定位比 WebSocket 自定义帧协议更直观。
- 业务关闭语义清晰:服务端可以通过
close/end/error事件表达生成结束或异常,客户端据此更新会话状态。 - 更符合当前 Native 网络体系:项目整体网络基建以 HTTP 网关为主,SSE 更容易接入现有鉴权、token 和环境配置。
WebSocket 的优势也要主动承认:
WebSocket 更适合真正高频双向通信,比如 IM、多人协同、在线游戏、实时交易行情或客户端需要持续向服务端发送控制指令的场景。它的优势是全双工、低延迟、连接复用。但我们的 AI 助手主要是单次提问后服务端连续下发 token、reason 和卡片,SSE 的复杂度更低,和 HTTP 基建也更一致。
可以用一句话收束:
不是 WebSocket 不好,而是这个业务不需要全双工。我们要的是稳定、可观测、能复用 HTTP 基建的服务端单向流,SSE 更贴合。
3.2 流式消息怎么维护?
可以这样讲:
我们用
sseId作为一次问答回合的唯一标识,用有序 Map 维护sseId -> ConversationTurn。这样既能保证消息顺序,又能做到 O(1) 查找和更新。每个 Turn 里包含用户问题、图片、流式片段、卡片消息、完成状态、禁用按钮集合、sequence 和错误信息。
数据结构可以抽象为:
ConversationTurn
- sseId
- question
- fragments
- reason
- message
- error
- cardMessages
- suggestion
- evaluation
- order list
- fixed format card
- isComplete
- disabledActionIds
- sequence
这个设计的价值:
- 文本流和卡片流可以放在同一个回合里。
- 思考过程和正式回答可以独立渲染。
- 按
sseId精准更新,不需要全量遍历。 - 历史消息可以 prepend,当前会话可以 append。
- 评价、按钮禁用、卡片交互可以绑定到具体回合。
3.3 多类型消息怎么扩展?
可以这样讲:
我们没有把所有消息解析写在一个大 if-else 里,而是做了 Parser Registry。先根据 eventType 区分 reason、message、suggestion、close、error,再通过 detector 判断具体 message 类型,最后找到对应 parser 转成 UI 模型。
这样做的优势:
- 新增卡片类型时,只加 detector 和 parser。
- 业务场景扩展不会污染主流程。
- 解析失败可以 fallback,不影响整条消息流。
- 请问、约工、下单等场景可以共享底层 SSE 能力。
面试追问“为什么支持 15+ 消息类型还能维护住”,可以答:
核心是把协议事件、业务消息类型和 UI 组件拆开。SSE Client 不认识业务,只产出 SseEvent;Parser Registry 负责协议到模型;ConversationManager 负责状态;Widget 层只关心模型渲染。这样新增消息类型不会影响连接层和状态层。
3.3.1 多类型适配为什么没有用 DSL?
可以这样讲:
DSL 不是没考虑,而是当前阶段不适合作为第一选择。小师父的消息类型虽然多,但大部分不是纯展示卡片,而是和业务动作、端能力、按钮禁用、评价提交、约工选择、工单状态、埋点、滚动行为、历史消息恢复绑定在一起。它们需要的不只是“根据 JSON 画 UI”,还需要强业务语义和稳定的端侧状态模型。
如果使用 DSL,大概会变成:
服务端下发 UI Schema
-> 端侧解释布局
-> 解释组件
-> 解释动作
-> 绑定状态
-> 执行业务 API / Native 能力
这套方案的优势很明显:
- 服务端可以动态调整卡片布局。
- 新卡片可以减少客户端发版。
- 多端可以共享一套 UI schema。
- 适合营销活动、低交互信息流、表单搭建器、运营卡片等强动态场景。
但它在小师父当前阶段的问题也很明显:
- 端侧复杂度会上升:需要维护 DSL 解释器、组件协议、动作协议、版本兼容、降级逻辑和调试工具。
- 类型安全会下降:强类型模型变成动态 schema 后,很多错误从编译期变成运行期。
- 交互状态更难治理:按钮禁用、评价状态、约工选择、流式更新、历史恢复都要抽象成 DSL 状态协议,成本很高。
- 排查链路更长:问题可能出在服务端 schema、端侧解释器、组件实现、动作绑定或版本兼容任一环节。
- 收益不够确定:当卡片类型还在快速验证时,过早做 DSL 容易把不稳定业务固化成一套复杂平台。
所以当时更合理的选择是:
先用
messageType + Parser Registry + 强类型 CardModel + 对应 Widget承接业务复杂度,把连接、解析、状态和渲染边界拆清楚。等卡片形态稳定、重复模式足够多、服务端确实需要动态编排时,再把高频稳定卡片抽象成 DSL 或 schema 化组件。
这也是一个架构节奏问题:
第一阶段:强类型卡片
-> 快速支撑业务
-> 保证可维护和可调试
第二阶段:抽公共协议
-> 识别重复布局和动作
-> 收敛组件模型
第三阶段:DSL / Schema 化
-> 服务端动态编排
-> 多端一致渲染
-> 灰度和降级治理
面试时可以用一句话收束:
DSL 适合解决“形态高度动态、端侧只负责解释”的问题;小师父当时更核心的问题是“AI 流式协议、多业务卡片和端侧状态稳定落地”。所以我优先选择强类型解析和注册机制,而不是一开始就上 DSL。
3.4 断流重连怎么做?
可以这样讲:
我们把连接重连放在 ConnectionProvider,而不是放在底层 Client 里。因为业务层更清楚这次请求的 body、url、是否已经收到过数据、是否是正常 close、是否应该继续重试。异常断开后会按次数做延迟重连,成功收到数据后重置状态;超过最大次数就停止自动重连,提示用户手动重试,避免弱网下无限请求。
这比简单“断了就重连”更稳:
- 正常 close 不重连。
- 用户主动 stop 不重连。
- 未收到任何数据时可以重连。
- 收到部分数据后异常断开,也可以按业务策略重连。
- 超过最大次数停止自动重连,避免请求风暴。
4. 预勘报告:千级检查项定位优化
预勘报告适合讲“复杂列表性能”和“数据结构优化”。
可以这样讲:
预勘报告是一个四层嵌套的复杂表单:一级目录、二级目录、一级标题、二级检查项。在千级检查项场景下,如果每次点击目录、搜索结果或定位未完成项都遍历整棵树,会导致定位复杂度高、滚动卡顿和目标不准。我做的优化是把嵌套数据预处理成多张索引表,再配合虚拟列表和两段式滚动,实现从 O(n) 遍历定位到 O(1) 映射定位。
核心索引可以概括为:
twoTitleMap
-> 检查项 ID 到检查项模型
twoTitleToDirectoryMap
-> 检查项 ID 到所属目录信息
catalogIndexMap
-> 目录 key 到右侧列表 index
indexToCatalogId
-> 右侧列表 index 到目录 key
twoTitleToCatalogMap
-> 检查项 ID 到目录 key
4.1 为什么不能直接 ensureVisible?
可以这样讲:
因为右侧内容是虚拟化列表,目标检查项如果还没渲染出来,就没有 BuildContext,直接
ensureVisible找不到目标。我的方案是两段式:先通过目录索引滚到目标所在二级目录,让目标区域被构建出来;再通过 GlobalKey 精确滚动到具体检查项。
流程是:
点击搜索结果 / 未完成项
-> twoTitleId
-> twoTitleToCatalogMap 找到目录 key
-> catalogIndexMap 找到二级目录 index
-> ScrollablePositionedList 滚到二级目录
-> 等待目标检查项 build
-> GlobalKey + ensureVisible 精确定位
这个方案解决了两个问题:
- 大列表下快速定位目标区域。
- 虚拟化场景下目标 Widget 尚未构建的问题。
4.1.1 使用 GlobalKey 滚动有没有性能问题?为什么一定要有 GlobalKey?
可以这样讲:
GlobalKey 是有性能成本的,所以我不会把它当成列表定位的主方案。这个项目里的主定位能力不是靠 GlobalKey,而是靠
twoTitleToCatalogMap和catalogIndexMap做 O(1) 索引定位。GlobalKey 只用在第二阶段,也就是目标二级目录已经滚进视口、具体检查项已经 build 出来之后,用它拿到目标 Widget 的 context,再做最后一步ensureVisible精确校准。
这里要先承认成本:
- GlobalKey 会进入全局 key 注册表,创建和维护成本高于普通
ValueKey。 - GlobalKey 参与元素匹配时更复杂,不适合大规模滥用。
- 如果列表频繁 rebuild、GlobalKey 频繁创建销毁,确实会增加 Element 复用和布局阶段负担。
- 在超大列表里给每一个 item 长期挂 GlobalKey,并且用它做全量查找,是不合理的。
但这个项目里使用 GlobalKey 的边界比较克制:
- 粗定位不靠 GlobalKey:从检查项 ID 找目录、从目录找列表 index,全部走 Map 索引。
- GlobalKey 只做精定位:目录级滚动完成后,目标检查项已经进入构建范围,再用 GlobalKey 拿 context。
- 不是每次滚动都遍历 key:定位入口是
twoTitleId,直接从_itemGlobalKeys[twoTitleId]取 key,不做全量扫描。 - 和 ValueKey 分工明确:
ValueKey(twoTitleId)保证列表 item 状态复用;GlobalKey只服务ensureVisible。 - 有 fallback:如果目标 key 还没 context,就退回目录级定位或备用位置索引,不阻塞主流程。
为什么还需要 GlobalKey?
因为
ScrollablePositionedList只能稳定滚到“二级目录 item”的 index,而一个二级目录 item 内部还有多个一级标题和多个检查项。检查项高度又不是固定的,里面有选项、备注、图片、错误态、展开态,靠预估 offset 很容易不准。GlobalKey 能在目标 Widget 真正渲染后拿到准确 context,让Scrollable.ensureVisible基于真实布局做校准。
也就是说,两段式定位里两种工具解决的问题不同:
目录索引 + ScrollablePositionedList
-> 解决远距离、虚拟列表、O(1) 粗定位
GlobalKey + ensureVisible
-> 解决目标已渲染后的真实布局精确校准
如果面试官继续问“能不能不用 GlobalKey”,可以这样答:
可以,但要付出其他成本。比如把每个检查项拆成独立的列表 item,这样可以直接按 item index 定位,但会破坏当前按二级目录组织的 UI 结构,列表 item 数量也会暴涨;或者预先计算每个检查项 offset,但动态高度、备注输入、展开收起和图片加载都会让 offset 失效。所以在当前结构下,目录索引负责性能,GlobalKey 负责精度,是一个比较务实的折中。
最后一句话收束:
我不是用 GlobalKey 解决大列表定位性能问题,而是用索引解决性能问题,用 GlobalKey 解决动态高度下的精确定位问题。只要控制使用范围,它的收益大于成本。
4.2 面试官问 O(n) 到 O(1) 怎么来的?
可以这样答:
原来从一个检查项 ID 找所属目录,需要遍历所有一级目录、二级目录、一级标题和检查项,复杂度接近 O(n)。优化后在模型解析阶段单次遍历构建
twoTitleId -> directoryInfo和twoTitleId -> model,后续定位时直接 Map 查找,所以查询阶段是 O(1)。整体不是消灭遍历,而是把多次运行时遍历前置成一次索引构建。
这句话很关键:不要说“完全没有遍历”,而是说“把重复遍历前置成一次索引构建”。
5. 工地筛选:动态化能力建设
工地筛选适合讲“业务动态化”和“状态协调”。
可以这样讲:
工地筛选不是写死几个按钮,而是后端动态下发多 Tab、多筛选维度和嵌套筛选项。客户端要根据协议动态渲染筛选 UI,并且把进度、红黄灯、计划排程、其他多选项、搜索关键词、状态 count 等统一组装成后端请求参数。
状态结构可以概括为:
ProgressFilterState
LightFilterState
ScheduleFilterState
OtherFilterState
|
v
FilterMediator
-> requestParams
-> pagination
-> countData
-> searchKeyword
-> notifyListeners
5.1 为什么用 Mediator?
可以这样讲:
如果让每个筛选 Widget 直接互相调用,状态会很快变成网状依赖。我们用 Mediator 模式,把每类筛选状态拆成独立对象,每个对象只处理自己的选择逻辑;最终由 FilterMediator 监听这些状态变化,统一合并请求参数、重置分页、处理状态码清理和通知 UI。
优势:
- 筛选维度扩展时,不需要改大段 UI 状态逻辑。
- 各筛选模块职责清晰。
- 请求参数统一出口,便于排查。
- Tab 切换、级联清理、分页重置可以集中治理。
项目里还有一个细节值得讲:
高频筛选操作下,不是每个状态变化都立即 notify,而是通过 microtask 做一次延迟通知,把一次同步操作里的多次状态变化合并成一次 UI 刷新,减少重复 rebuild 和重复请求。
5.2 动态筛选的风险
主动说风险会显得更成熟:
- 后端协议必须稳定,否则客户端动态渲染会出错。
- 默认值、空数据、字段缺失要有兜底。
- 参数结构要可观测,否则线上排查困难。
- 动态化不能无限放权,复杂交互仍需要端侧规则约束。
6. HOMEHD:iPad 展厅经理带看助手
HOMEHD 可以讲“iPad 大屏业务 + 大图内存治理 + Native/Flutter 容器”。
可以这样讲:
HOMEHD 是 iPad 端展厅经理带看助手,业务场景是展厅经理带客户逛店、看套餐、看案例、看工艺和服务方案。这个项目的特点是大屏展示、图片密集、Native 能力多,同时逐步引入 Flutter 承接业务页面。
架构上可以这样讲:
Native 侧负责 iPad App 容器、路由、图片、分享、扫码、WebView、隐私和系统能力;Flutter 页面通过 Native 创建 Flutter 容器承载。Native 路由里有打开 Flutter 页面、打开 Flutter 弹窗、H5 调 Flutter 等能力,Flutter 页面作为业务模块嵌入到整体 Native 页面栈里。
6.1 大图为什么会成为问题?
可以这样讲:
iPad 大图场景最容易出内存问题,因为图片文件大小不等于解码后内存。比如一张 4000x3000 的 RGBA 图片,解码后大约是 4000 * 3000 * 4,也就是 48MB。带看场景经常一屏多图、预加载、轮播、背景模糊、缩放查看、缓存叠加,内存峰值很容易上去。
项目中可讲的优化方向分两层。
第一层是图片下载和缓存治理:
- 预加载时过滤图片,避免把上千张图全部下载。
- 优先缓存大图和中图,减少无效 small 图请求。
- 使用避免提前解码和大图缩放策略。
- 限制图片下载并发。
- 下载后落盘缓存,减少重复网络请求。
- 在生命周期节点清理内存缓存。
第二层是大图渲染链路治理:
对 Flutter 大图场景,如果普通 Flutter Image 或纹理库在 iPad 上内存峰值过高,可以把大图解码、降采样、缓存和释放交给 Native,使用
CVPixelBuffer管理像素内存,再通过 Flutter External Texture 交给 Flutter 合成。这样能减少 Dart 层和 Flutter Image 层对大图的重复持有,让 Native 更精确地控制内存生命周期。
面试时可以强调结果:
这个专项的目标不是单点改一行代码,而是从图片下载、解码、缓存、渲染和释放链路整体治理,最终让套餐大图浏览场景的内存峰值下降约 40%。
6.2 如果被问 External Texture 为什么能降内存?
可以这样答:
External Texture 的核心价值是 Flutter 不直接拥有完整图片对象,而是通过 textureId 引用 Native 侧提供的纹理数据。Native 可以用更适合平台的方式管理像素缓冲、降采样和释放策略。它不是天然降低所有内存,而是给了我们绕开 Flutter Image 默认解码缓存路径、精细控制大图生命周期的机会。
7. 贝刻手环:BLE 硬件协议与 Flutter 页面协同
贝刻手环适合讲“Native 底层能力 + Flutter 页面 + 硬件协议”。
可以这样讲:
贝刻手环是一个 Native + Flutter + BLE 的硬件项目。Native 侧基于 CoreBluetooth 管理扫描、连接、服务发现、特征读写、通知和断连;协议层在 BLE ATT 之上实现了 L1/L2 私有协议;Flutter 侧主要承接设置页面、升级页面、排行榜等业务 UI。
7.1 协议层怎么讲?
可以这样讲:
BLE ATT 层的 MTU 比较小,项目协议在 ATT 之上做了 L1 传输层,用来实现拆包、组包、sequence id、CRC 校验、ACK、错误 ACK 和重传。L1 解决可靠传输问题,L2 再定义具体业务命令,比如设置、绑定、提醒、运动数据和控制命令。
抽象为:
BLE ATT
-> L1 可靠传输层
-> 拆包 / 组包 / ACK / CRC / sequence
-> L2 业务命令
-> 设置 / 绑定 / 提醒 / 运动数据 / 控制
这个项目里可以重点讲两个复杂点:
- 一代、二代手环硬件规格不同,需要根据设备类型和固件版本分支处理。
- Flutter 页面发起的是业务设置,Native 要把它转成具体硬件命令和云端上报。
7.2 Flutter 和 Native 怎么协同?
可以这样讲:
Flutter 页面负责展示和交互,比如勿扰模式、消息提醒、亮度、表盘、固件升级、微信联系人提醒、快捷回复等。Flutter 通过路由 action 把
type + data传给 Native,Native 根据EFlutterToNativeType分发到具体设置逻辑,再调用手环协议对象或二代手环 manager 下发命令。
链路可以概括为:
Flutter 设置页
-> type + data
-> Native Router Action
-> Notification / ViewModel
-> bandSetAction
-> WB_BAND.cmdObj / BandManager
-> BLE 写入
-> 手环固件
优势:
- Flutter 负责 UI,迭代快。
- Native 负责 BLE 和硬件协议,稳定性高。
- 一代、二代兼容逻辑留在 Native,更容易复用已有 SDK。
- 云端上报、本地缓存、错误日志可以和 Native 基建结合。
弊端:
- 链路长,调试复杂。
- Flutter、Native、协议、固件、云端字段必须保持一致。
- BLE 弱连接场景多,需要完善日志和状态机。
面试追问“你怎么保证硬件操作可靠”,可以答:
我会从三层保证:协议层有 sequence、CRC、ACK 和重传;连接层监听蓝牙状态、连接状态和特征可写性;业务层对关键设置做本地状态、云端上报和错误日志。硬件项目不能只看 UI 成功,还要看指令是否真正下发、固件是否响应、云端状态是否一致。
8. Gate.io:AI 工程化研发链路
Gate.io 这段不要讲成“我用了 AI 写代码”,而要讲成“我把 AI 引入研发流程”。
可以这样讲:
Gate.io 阶段我负责 AI 工程化研发链路建设,包括 PRD2Code、需求澄清、技术规格生成和 AI Code Review。这个项目的目标不是让 AI 随便生成代码,而是把自然语言需求转成可评审、可追踪、可落地的工程产物,再通过评审和质量校验控制 AI 代码风险。
可以拆成四步:
PRD
-> 需求澄清
-> 技术规格
-> 代码生成
-> AI Code Review
-> 人工确认和质量门禁
你可以强调:
- AI 适合加速重复性和结构化工作。
- 需求不清时不能直接写代码,必须先澄清。
- 技术规格是 AI 生成代码的约束边界。
- Review 要关注可维护性、边界条件、测试和安全风险。
面试追问“AI 生成代码占比 40%,怎么保证质量”,可以答:
我不会用生成占比证明质量,而是用约束和评审证明质量。前面有需求澄清和技术规格,生成后有 AI Review 和人工 Review,再结合项目已有规范、测试和静态检查。AI 代码必须进入正常工程质量体系,而不是绕过质量体系。
9. 百度文学:阅读器和 iOS 基础能力
百度文学可以作为你 iOS 基础能力的补充项目。
可以这样讲:
百度文学阶段我主要做熊猫看书、纵横小说等阅读类 App。阅读器项目看起来是业务功能,但其实很考验 iOS 基础能力,包括 epub/txt/pdf 解析、中文排版、分页缓存、仿真翻页、平移翻页、上下滚动、夜间模式、字体管理、笔记、划线、书签、本地导入和云同步。
阅读器难点可以这样讲:
- 长文本不能一次性全部排版,否则内存和耗时都高。
- 分页结果要缓存,否则翻页会卡。
- 字体、字号、行距、屏幕尺寸变化会导致分页失效。
- 翻页动画要和手势、渲染、状态同步。
- 本地书籍和云端书架要处理一致性。
这一段的价值是证明:
我不是只会 Flutter,对 iOS Runtime、RunLoop、内存、绘制、数据库、网络和复杂业务组件也有长期实践。
10. 高频追问速答
10.1 你最能代表技术深度的项目是哪个?
可以答:
如果看 AI 和混合架构,我会讲小师父;如果看性能和数据结构,我会讲预勘报告;如果看 iOS 底层和内存,我会讲 HOMEHD 大图优化;如果看硬件和 Native 能力,我会讲贝刻手环。
10.2 Flutter 和 Native 的边界怎么划?
可以答:
我一般按能力归属划边界。UI、业务交互、跨端复用适合 Flutter;登录态、安全、网络网关、系统能力、硬件、图片底层优化更适合 Native。边界不是固定的,要看谁更接近资源、谁拥有现有基建、谁更容易保证稳定性。
10.3 为什么你们很多能力都走 Native?
可以答:
因为这是一个历史 Native 基建很强的大型混合 App。网络、安全、登录、埋点、图片、分享、BLE 等能力 Native 已经成熟。如果 Flutter 重建一套,短期看自由度高,长期会带来双栈维护、行为不一致和安全风险。我们选择让 Flutter 聚焦业务 UI,把基础能力收敛到 Native。
10.4 这种混合架构最大的风险是什么?
可以答:
最大风险是边界失控。比如 Channel 协议膨胀、字符串路由失控、参数没有类型约束、Flutter 和 Native 生命周期不同步、排查链路变长。所以要做统一路由入口、统一网络入口、统一 Channel 协议、统一错误码和可观测性。
10.5 如果让你重构,会优先改什么?
可以答:
我会优先做三件事:第一,路由参数类型化,减少字符串和 Map 带来的运行时错误;第二,Channel 协议收敛,统一 method、参数、错误码和日志;第三,网络和 SSE 增加更好的可测试边界,比如 mock transport、请求取消、链路追踪和错误注入。
11. 最终面试收束
最后可以用这段收束:
我这几段经历的共同点是,业务都不是单纯页面开发,而是在复杂客户端工程里做技术取舍。贝壳精工让我沉淀了 Flutter + Native 混合架构、路由和网络复用;小师父让我系统做了 AI SSE 流式接入和多类型消息渲染;预勘报告让我把复杂嵌套表单从遍历定位优化到索引定位;HOMEHD 让我处理了 iPad 大图内存峰值问题;贝刻手环让我接触了 BLE 私有协议和硬件状态机。我的优势是能把业务问题拆到架构、协议、状态、渲染和 Native 能力边界里解决,而不是只停留在 UI 层。