ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

2017阿里客户端附加题:聊天列表页设计背后的核心能力

2017阿里客户端附加题:聊天列表页设计背后的核心能力 2017年秋招客户端岗位的竞争已经相当激烈。我印象最深的不是某道算法题而是最后那轮面试官临时加的“附加题”——一个看似简单的聊天列表页设计。当时心里第一反应是这不就是RecyclerView加个接口嘛。但真开口回答时才发现肚子里没货的人是连框架都搭不起来的。后来我专门复盘过这道“阿里巴巴2017秋招客户端附加题”发现它几乎把客户端开发的核心能力全串起来了UI渲染、数据存储、网络同步、性能优化、异常处理全都藏在一个聊天页面里。今天就把这套东西完整拆开聊一聊我当时怎么答的、后来怎么改的以及准备客户端面试的人可以从里面学到什么。1. 附加题为什么不是“附加”那么简单1.1 附加题的本质是筛选器常规面试题考的是知识量比如“HashMap的底层实现”“TCP三次握手”这些题只要背得够多就能过。但附加题不一样它往往是一个开放式场景没有标准答案甚至没有唯一答案。面试官递出这道题的时候想的不是“这个人能不能说出正确答案”而是“这个人遇到一个真实问题的时候脑子里有没有一套完整的思考路径”。附加题通常放在面试结尾时间不会太长但它的权重往往比前面的题更高。因为前面几轮已经确认了基础能力最后一题考察的是你把这些知识用到真实场景里的能力。换句话说前面的题回答得好能证明你“知道”附加题回答得好才能证明你“会做”。我当时犯的第一个错误就是把附加题当成了普通的设计题想用一个模板糊弄过去。但实际上面试官想听的是很具体的东西消息从服务端推过来以后怎么走到UI、数据库怎么建、列表滑动卡顿怎么办、弱网条件下消息发不出去怎么处理。这些细节一点一点挖下去你的项目经验和技术深度很快就暴露了。1.2 客户端开发的能力模型决定了附加题的考法客户端开发不像后端那样有一条清晰的业务链它更像一个“端到端”的闭环。一个消息列表页涉及的东西远比表面看起来复杂。我后来整理了一份能力模型对照表基本可以解释为什么面试官喜欢用这类题目来试探候选人能力维度常规考察方式附加题中的表现UI渲染RecyclerView的基本用法是否能回答列表复用、卡顿优化、增量更新数据存储SQL语句是否能设计表结构、索引、缓存策略网络通信HTTP协议是否能讲清长连接、消息推送、超时重试异步并发线程池是否能处理消息乱序、状态同步、线程切换稳定性崩溃日志是否能想到异常兜底、重试、幂等性能优化卡顿、内存是否能提出可落地的监控和优化方案一个人如果只是用RecyclerView做过列表没做过真正的IM或消息类应用面对附加题时大概率会在“消息状态同步”或者“本地缓存与服务器一致性”这样的点上卡住。这就像会开车的人很多但能在暴雨天走山路还不心慌的人很少面试官想找的就是后者。所以这道附加题表面上是在问你“聊天列表怎么设计”实际上是在问你你曾经在客户端项目中处理过哪些真实问题有没有把这些问题背后的原理想清楚。2. 题目复盘2017年那道附加题到底长什么样2.1 面试题常见的几种变体严格来说阿里巴巴2017秋招并没有一份公开的完整题库不同岗位、不同面试官出的附加题会有差异。但从当时多个渠道的题目整理来看客户端方向出现频率最高的附加题类型大致有四种设计一个IM消息列表、设计一个电商首页、设计一个视频播放器、设计一个离线日志系统。这四种场景有一个共同点——都是客户端最典型的业务形态能覆盖大多数核心知识点。我遇到的是IM消息列表这也是后来很多人讨论最多的题目。题目描述大概是这样的用户打开一个聊天会话页能看到历史消息新消息可以通过推送实时到达发送消息后需要显示“发送中”“已发送”“已读”等状态弱网或断网时消息不能丢恢复网络后要自动补发。要求用10到15分钟说清整体设计思路并说明关键技术点。这类题目最考验人的地方在于它没有给出任何技术栈限制也没有明确数据量级。候选人需要自己主动确认边界比如是点对点聊天还是群聊消息量级是每天几十条还是几千条用户手机的内存和存储空间有没有限制这些都是决定设计方案的重要因素。2.2 为什么要选IM场景来考面试官选择IM场景绝对不是随手一拍。一个消息列表页几乎是客户端技术点的“全家桶”。列表要流畅涉及渲染优化消息有状态涉及状态管理有新消息实时到达涉及网络长连接和推送需要保存历史记录涉及本地数据库设计消息发送失败要重试涉及网络策略和任务队列。往深处问还能牵扯出更多问题消息到达顺序怎么保证消息ID怎么生成才能避免重复本地数据库和服务器数据不一致怎么办分页加载时如果用页码会不会漏消息这些细节面试官可以顺着你的回答一路追问问到你答不出来为止。所以我要提醒准备面试的朋友不要只盯着“这道题的标准答案”而是要把IM这个场景当成一个完整的小项目亲手做一遍踩一遍坑。只有这样你才能在回答“为什么这么设计”的时候言之有物。3. 我当时的回答从架构设计到数据细节3.1 先分层再谈页面我当时给面试官的框架是四层UI层、状态管理层、数据层、网络层。每一层只做自己该做的事。UI层只负责渲染和用户操作状态管理层负责把数据转换成UI需要的状态数据层负责内存缓存、本地数据库、远程数据之间的协调网络层负责HTTP接口和长连接消息。这个分层的好处是每一层都容易替换和测试。如果不用WebSocket换成轮询网络层改动就行UI层不用管如果本地数据库从SQLite换成Room数据层内部改就行状态管理层不受影响。这正是工程上最看重的“低耦合”思路。具体到消息列表页我会这样设计UI层一个聊天列表ActivityRecyclerView承载消息展示底部输入框负责发送。状态管理层一个Presenter或ViewModel持有消息列表数据负责把数据库和网络源的数据合并后抛给UI。数据层一个MessageRepository对外提供getHistoryMessages、sendMessage、onNewMessage三个核心方法。网络层一个ChatSocketService维护长连接接收服务端推送并封装发送消息的接口。面试官听完这个分层之后通常会追问一句如果用户快速滑动列表数据库读取和网络消息同时到达你怎么处理这个问题其实就是想确认你是否理解“数据源合并”的复杂性。我的回答是所有来自数据库、内存缓存、网络推送的消息都统一进入一个消息队列由状态管理层按消息时间戳顺序处理后再一次性刷新UI。这样UI永远只面对一份有序的数据不会出乱。3.2 消息状态机聊天列表的灵魂IM里最容易被忽略、面试官最爱追问的就是消息状态。一条消息从用户点击“发送”到对方真正看到中间要经历好几种状态。如果状态设计得不对会出现“消息明明发了列表里却消失了”或“消息显示已读对方其实没收到”的问题。我当时画了一个很朴素的状态机消息发送时先进入“发送中”本地把内容写入数据库状态为0服务端收到后返回一个确认消息状态变为“已发送”如果接收方回执已读则状态变为“已读”如果网络异常或超时状态变为“发送失败”用户点击重发后回到“发送中”。这里有一个容易被忽略的点本地写入数据库的时机。有人会等服务端确认后再写库这样做会导致弱网下UI上根本没有这条消息体验很差。正确做法是用户点击发送后立刻把消息写进本地库状态标记为“发送中”同时把消息塞进一个待确认队列。等服务端确认之后再更新本地状态并刷新UI。这既是产品体验的要求也是工程上更合理的方案。状态变化还需要考虑幂等性。如果服务端确认消息因网络问题重复到达你不能把一条已读的消息改回发送中。所以每一条消息都要有一个全局唯一的消息ID本地根据这个ID做去重和更新。3.3 数据库表怎么建才不踩坑消息列表页离不开本地数据库。2017年时SQLite还是主流Room也没有完全普及所以面试时聊SQLite是更稳妥的选择。我记得当时给面试官画了两张表会话表和消息表。会话表存的是每个聊天对象的基础信息包括目标ID、最后一条消息、未读数、更新时间。消息表存的是具体消息内容包括消息ID、会话ID、内容、状态、时间戳。建表SQL大概是CREATE TABLE conversation ( id INTEGER PRIMARY KEY, target_id TEXT NOT NULL, last_message TEXT, unread_count INTEGER DEFAULT 0, updated_at INTEGER NOT NULL ); CREATE TABLE message ( id INTEGER PRIMARY KEY AUTOINCREMENT, msg_id TEXT NOT NULL, conversation_id TEXT NOT NULL, content TEXT, status INTEGER DEFAULT 0, timestamp INTEGER NOT NULL, UNIQUE(conversation_id, msg_id) );最核心的设计是conversation_id msg_id的联合唯一约束。这样即使服务端重复推送同一条消息数据库也不会插入两条。timestamp字段一定要建索引因为历史消息分页基本就是按时间倒序去查。分页查询不要用页码我推荐用“加载更多”时传上次最后一条消息的时间戳SQL里用WHERE timestamp ? ORDER BY timestamp DESC LIMIT 20。这样新消息插入时不会影响历史分页的位置也不会出现翻页重复的问题。数据库的操作还必须放到子线程不能占用主线程否则滑动列表时会有明显卡顿。这个点虽然基础但面试时主动说出来会显得你确实处理过真实项目。4. 代码实现与细节优化能跑到真机上才算数4.1 列表复用与快速滑动的处理既然题目里明确要求聊天列表列表性能就是绕不开的话题。面试时如果只说一句“用RecyclerView”基本等于白说。你要能回答出来为什么不用ListView因为RecyclerView强制了ViewHolder复用、布局管理更灵活、支持局部刷新。你怎么避免列表卡顿核心是保证onBindViewHolder里不出现耗时操作。我当时给面试官举了一个例子不要在Adapter里直接做时间格式化。每条消息的时间戳要显示成“昨天 14:20”这样的格式如果在绑定的时候用SimpleDateFormat格式化快速滑动时会产生大量临时对象造成卡顿。正确做法是在数据层把时间格式化好或者写一个轻量级的缓存按分钟粒度缓存格式化后的结果。图片加载也要注意。聊天里的表情包、头像如果直接用原生方法加载很容易在滑动时出现错位和卡顿。2017年Glide已经比较流行可以在绑定ViewHolder时使用Glide并通过skipMemoryCache()等参数控制缓存策略。更重要的是加载完成后的回调里要判断ImageView是否还在原来的位置避免图片错位。代码上面的核心示范是这样的public class MessageAdapter extends RecyclerView.AdapterMessageViewHolder { Override public void onBindViewHolder(MessageViewHolder holder, int position) { Message msg messages.get(position); holder.contentTv.setText(msg.getContent()); // 不要在 bind 里做耗时格式化 holder.timeTv.setText(TimeCache.format(msg.getTimestamp())); } }很多项目卡顿不是因为RecyclerView本身而是因为你在bind里做了太多的事。提前把耗时工作做好Adapter就只是一个搬运工。4.2 增量更新不要整表刷新聊天列表很容易遇到这样的事收到一条新消息其实只需要在列表尾部插入一行但很多人图省事直接notifyDataSetChanged()。这个操作会把整个列表重新绘制一遍数据量小的时候没问题消息一多就是灾难特别是正在快速滑动时突然刷新直接掉帧。更优的方案是DiffUtil。DiffUtil是Android Support Library 25.1.0加入的工具能计算出新旧列表的差异然后精确到具体item做插入、删除、更新操作。使用起来也不复杂你需要在回调里比较两个对象的id和内容。聊天列表的messageId天然适配这个场景。当然DiffUtil也不是银弹。列表特别大时计算diff本身会消耗一些时间。更极致的做法是如果你能明确知道“新消息永远加在尾部”那就直接用notifyItemInserted(list.size() - 1)连DiffUtil都不需要。这里我想强调一个面试技巧面试官问你怎么优化列表时你不仅要说出方案还要说出“这个方案在什么场景下不适用”。比如DiffUtil适合数据顺序可能变化的场景如果只是尾部追加用notifyItemInserted更轻量。这种对比回答会让面试官觉得你有真实项目经验。4.3 弱网不好时消息状态怎么保证消息状态机设计得再好落到真实网络环境里还是会出问题。最典型的场景是用户点击发送消息进入“发送中”但网络突然断开这个时候如果直接把消息状态改成“失败”用户就要手动重发。但如果网络只是闪断马上恢复其实可以自动重试。我当时的设计是发送队列里维护一个重试次数变量每次发送失败不是直接置为失败而是先检查重试次数是否小于3次。如果小于3次启动一个指数退避的定时器分别隔1秒、2秒、4秒再试如果超过3次消息状态才变为“发送失败”并提示用户点击重试。这种设计还需要考虑服务端幂等。客户端发送消息时带上一个客户端生成的消息ID服务端用这个ID做去重即使客户端重试了多次服务端也只会落一条消息。否则就会出现“用户点一下发送对方收到三条一模一样消息”的事故。在面试里表达这个点时可以顺便说一下消息“乱序”问题如果服务端把消息A和消息B推过来时顺序反了UI不能盲目按到达顺序渲染而是应该按消息时间戳排序。聊天场景下时间戳才是真正的顺序依据而不是网络包的到达顺序。5. 面试现场怎么讲才会让人觉得你“真的做过”5.1 回答设计题的五段式框架当年我面试时虽然设计思路有了但表达串得不好导致面试官总觉得我在堆名词。后来复盘时我总结出一个五段式表达框架基本可以应对所有客户端开放型设计题先确认需求边界不要立刻开答。问数据量级、问场景约束这本身就加分。说明技术选型并解释为什么不用其他方案。比如“这里用长连接而不是轮询因为实时性要求高”。画出整体架构分层讲清楚每一层的职责和层与层之间的数据流向。把核心流程走一遍从一条消息发送到状态更新再到推送给对方从头到尾串一次。讲风险点和你如何处理比如弱网、乱序、数据库膨胀、内存泄漏。框架不复杂但能帮你把散落的知识点串成一条线。5.2 三个最容易失分的细节我后来模拟过很多次面试发现大家在回答这类设计题时容易踩三个坑。第一个坑是“只谈界面不谈数据”。面试官问的是“设计一个消息列表页”你如果从布局文件讲到Adapter再讲到ViewHolder却始终不提数据库怎么存、消息怎么同步、状态怎么更新那在面试官眼里你只是一个“会写UI的”而不是“做客户端的”。第二个坑是“只谈方案不谈取舍”。一上来就丢出一堆名词MVP、MVVM、RxJava、EventBus、组件化。听着很唬人但面试官只要追问“为什么用MVP而不是MVVM”就答不上来。正确做法是每提出一个方案都要说出它解决了什么问题、牺牲了什么。比如用RxJava可以简化异步回调的嵌套但也会增加调试难度和团队学习成本。第三个坑是“忽略边界条件”。面试官特别爱问“如果用户手机存储空间满了怎么办”“如果服务端返回的数据格式错了怎么办”“如果消息有几万条怎么办”。这些问题往往没有标准答案但你要提前想到。你主动提一句“我还会做消息本地存储的清理策略”就会比被动等问要强很多。5.3 对于准备客户端面试我的几点实在建议如果你正在准备客户端岗位不要只刷面试题。建议你拿一个真实的IM demo从数据库表设计开始一路写到RecyclerView展示。每一步都问自己为什么要这样写。这个流程走下来你对“客户端”三个字的理解会完全不一样。另外要把“性能优化”当成习惯而不是加分项。写列表时想想会不会卡顿写网络请求时想想弱网表现写数据存储时想想会不会越存越多。面试官很愿意听到候选人说“我测过卡顿”“我统计过内存”哪怕你没有太多性能调优经验只要表现出有这方面的意识就已经比很多人强了。说到底这道2017年的附加题考的并不仅仅是当时那15分钟里的回答。它撬动的是你对客户端这个领域的完整认知。我现在带项目时依然会拿类似的问题去考团队新人因为一个人面对模糊场景时的思考方式很难伪装。如果你正在准备面试不妨亲手写一个带消息状态机的列表页再对着镜子把整个思路讲一遍。你会发现面试官真正想看的从来不是你背过多少答案而是你遇到问题时候脑子里的那张地图是否清晰。
返回列表