面试备战:客户端混合架构、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 侧注册的页面。

DWHomeFlutterHelperDWLifeFlutterHelper 里还有一个关键参数:

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 协议解析行数据,把 ideventdataretry 转成统一的 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-TypeAccept: 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,而是靠 twoTitleToCatalogMapcatalogIndexMap 做 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 -> directoryInfotwoTitleId -> 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 层。