腾讯搜狗客户端一面二面面经:MVP架构与字段混淆排查
- 轮次
- 一面+二面
- 结果
- 二面挂
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一面
- 请简单做一下自我介绍。
- 从快手实习中选择一个独立负责的模块,说明问题、技术栈和验收方式。
- 开发过程中最耗时的技术难点是什么?是否需要求助?
- 为什么使用 MVP,而不是当前更主流的 MVVM?
- ArrayList、LinkedList 和 HashMap 分别适合什么业务场景?
- HashMap 如何查找元素?
- 页面请求尚未返回时用户离开,结果如何保存和派发?
- MVP 中 Presenter 如何感知 Activity/Fragment 已离开?
- 什么是接口幂等?哪些接口需要特别关注?
- 实现一个最多三个并发任务、异常隔离且按输入顺序返回的调度函数。
- 如何保证最大并发数确实为 3?
- 审查多线程余额扣减代码。
- 审查返回局部字符串
c_str()的代码。
二面
- 请简单做一下自我介绍。
- 介绍一下快手实习中负责的工作。
- 这些难点是业务难点还是技术难点?
- Release 包中头像和用户名不显示,具体是什么问题?
- 这个数据类在完整链路中有什么作用?
- 最终如何解决字段混淆问题?
- 为什么添加
@SerializedName后就能解决? - 除了逐字段添加注解,还有什么方案?
- 为什么不直接对整个类使用
@Keep? - 如何防止以后其他开发者再次漏写字段映射?
- IM 消息乱序的具体场景和原方案是什么?
- 为什么是 900 ms,而不是 800 或 1000 ms?这个数值由谁决定?
- 如果重新设计,多接口结果聚合怎样做得更优雅?
- 日常开发中如何使用 AI?
- 有没有主动使用 AI 解决问题的具体案例?
- 算法题:执行
m次取数操作,怎样使数组元素乘积之和最小? - 为什么样例没有通过?正确的选择和配对方式是什么?
《参考解析》
架构选型:为什么是 MVP,以及 Presenter 怎么面对 View 消失。 MVP 把 View 抽象成接口,Presenter 持有这个引用、负责逻辑,View 只做渲染回调;MVVM 靠可观察数据驱动界面,ViewModel 不直接持有 View,配置变更时还能活下来。答「为什么不用更主流的 MVVM」不能只说「MVP 简单」,要给场景和代价:存量代码与团队熟悉度、逻辑集中便于单元测试、改造量可控,代价是接口和样板代码多、View 与 Presenter 强耦合、生命周期要手动管。请求还没回来用户就离开,问题在于回调里直接操作已销毁的 View 会崩溃或泄漏:正确做法是回调中先判断 View 是否仍然 attached,没 attached 就把结果暂存,等 View 重新关联时再派发;更稳的是把结果放在 ViewModel 这类存活于配置变更之上的容器里,销毁重建后由新的订阅者取最新值。Presenter 感知 View 离开靠约定:View 在 onDestroyView 调 detachView() 把引用置空(或用弱引用),之后所有 UI 回调先判 isViewAttached();但要注意旋转屏幕引发的销毁重建不该丢掉结果,这也是纯 MVP 的短板。
集合选型、HashMap 查找与接口幂等。 ArrayList 是连续数组,按下标随机访问 O(1)、尾部追加摊还 O(1),中间插删要搬元素,适合读多写少;LinkedList 头尾增删 O(1),但随机访问 O(n)、每个元素多一个 Node 对象且缓存不友好,做队列通常用 ArrayDeque 更快;HashMap 靠哈希做均摊 O(1) 的键值查找,适合去重、计数、对象索引,需要顺序遍历时应换 TreeMap 或 LinkedHashMap。HashMap 的查找链路是:先对 key 求哈希(JDK8 用 h ^ (h >>> 16) 让高位参与运算),再 (n - 1) & hash 定位桶,桶内先比哈希值再比 equals;链表长度到 8 且容量到 64 会转红黑树,把最坏查找从 O(n) 降到 O(log n)。前提是 key 正确实现了 hashCode/equals,用可变对象当 key 改完字段就再也查不到。幂等的定义是同一请求重复执行多次与执行一次效果一致;需要重点关注有副作用又会被重试或回调的接口——支付扣款、下单、库存扣减、退款、消息投递、第三方回调。落地一般是「业务唯一号 + 唯一索引/幂等表」先落记录,重复请求直接返回上次结果,或者用带状态条件的 CAS 更新,而不是「先查再写」。
并发调度函数与两段代码审查。 调度函数的骨架是「固定并发度 + 结果按下标落位」:用核心数与最大数都设成 3 的线程池,或 Semaphore(3) 限流;每个任务自己 try/catch 把异常包装成结果对象,这样单点失败不会炸掉整批(异常隔离);结果写进预先分配的数组对应下标,最后按输入顺序收集返回。追问「怎么保证最大并发确实是 3」时,别答「用一个计数器统计」——要用可证明的机制:固定大小且队列有界的线程池天然不会超过 3,若用信号量则 acquire 必须在提交之前、release 放在 finally,否则任务抛异常就永久占坑。余额扣减的代码审查,典型 bug 是「先查余额再更新」这种 check-then-act 不原子,且锁的作用域没同时盖住读和写;修法是用 update account set balance = balance - ? where id = ? and balance >= ? 这类带条件的原子更新并按影响行数判断成败,或让完整读改写都在同一把锁内,同时检查锁对象是否一致、异常路径是否漏解锁。c_str() 那题是函数返回了局部 std::string 的 c_str() 指针,函数一返回缓冲区就销毁,调用方拿到悬垂指针;应改成返回值语义的 std::string,或者在注释里明确生命周期约束,不把裸指针交出去。
Release 包字段丢失:混淆、映射与防回归。 debug 正常、release 里头像和用户名不显示,几乎可以断定是 JSON 反序列化后字段全为 null:Gson 默认拿「字段名」当 JSON 的 key,而 release 开启了代码混淆,字段名被改写成 a、b 这类短名,键自然对不上,null 一路传到界面。@SerializedName 之所以能修好,是因为注解值是字符串字面量,混淆不会改字符串常量,映射关系被显式固定下来、与字段名无关。替代方案有几档:配置 FieldNamingPolicy 统一命名规则、在混淆规则里 keep 掉 DTO 的字段名,或者干脆换成编译期生成的序列化方案(如 kotlinx.serialization、Moshi 的代码生成),不再依赖反射和字段名,代价是引入插件与迁移成本。不用 @Keep 保住整个类,是因为它粒度太粗——这个类里只要有一个字段要保,全类成员都不混淆,既失去混淆收益、可读性也更高;而且它只治「名字被改」,没解决「映射靠字段名隐式约定」这个根因。防回归要落在机制上:把显式映射写进编码规范与 Review 清单,在 CI 里对 release 变体跑一条真实 JSON 的解析冒烟用例,再补一条自定义 lint 规则扫缺注解的 DTO,让漏写变成流水线能拦下的错。
IM 消息乱序与多接口聚合。 乱序的场景通常是消息经多条通道、多次回调陆续到达,客户端按「到达顺序」直接追加渲染,于是旧消息盖住新消息或出现跳序。可用的解法是引入一个很短的缓冲窗口:先按服务端序号或时间戳重排,窗口内等齐再批量上屏,超过窗口就按现有顺序先渲染,避免卡住界面。900 ms 这种参数是权衡值——窗口太小压不住乱序,太大用户能明显感到消息延迟;合理的回答是说明它由「分片与多通道到达间隔的实际分布 + 端到端延迟预算」共同决定,而且要和产品、服务端一起定,能讲清依据比背出数字更重要。重新设计多接口聚合,方向是用 CompletableFuture.allOf 或协程的 async + awaitAll 做结构化并发,用一个统一的结果类型显式区分成功与失败,为每个来源设超时和降级(哪一路挂了就补占位值,而不是整屏空白),并把聚合抽成可复用函数,让调用方只面对最终模型。
算法题与 AI 协作怎么答。 「执行 m 次取数使乘积之和最小」这类题面本身就有歧义——每次取几个数、取出的数是否放回、乘积如何累加进总和,都会改变答案;第一步应该是澄清操作定义,再看符号与绝对值的影响(两个负数相乘为正会推高总和,异号相乘为负更能压低总和),据此定排序加贪心的配对策略,最后用小规模暴力枚举对拍验证。作者「样例没通过」多半出在没先对齐题意或贪心漏了符号边界(0、异号的处理),面试里把「确认定义 → 给策略 → 对拍验证」这条线讲清楚,比硬写代码更得分。AI 那两问考的是判断力与验证意识:日常可以用它解释陌生代码、写单测与脚本、查报错和文档、生成骨架,但结论要自己复核;讲主动案例时要说具体卡点、给了什么上下文、它哪里不对、自己怎么改的,把它当搜索引擎或整段照抄都是减分项。