2024春招小红书Android笔试复盘:考点、算法与源码细节
开头直接进入状态刚考完2024年春招小红书Android开发岗的第三批笔试趁记忆还热乎把这次笔试的完整复盘整理出来。如果你正准备投Android开发岗尤其是内容社区类App的公司这篇东西应该能帮你少走不少弯路。这次笔试整体给我的感觉是考察范围不算偏但挖得很深。算法题不会恶心到要现场发明数据结构Android基础题也基本都是高频考点但它不满足于你知道这个API怎么调用而是反复追问你知不知道底层是怎么实现的换个场景这个方案还成不成立。我是在牛客网上做的笔试总时长90分钟题型分布是10道单选题、5道多选题、2道编程题。90分钟说实话不算宽裕尤其编程题还要在有IDE和没有IDEA的编辑器之间切换思路选择题里又有好几道需要反复斟酌的题目。我大概用了25分钟做选择题剩65分钟全力写两道编程题最后一道题只完成了核心逻辑边界情况没来得及全部处理。文章的读者定位很明确准备参加Android校招或实习笔试的同学以及想系统检验自己Android知识体系的开发者。我会把这次笔试涉及的考点、我做题时的思路推导、失分的地方以及事后查资料总结的完整知识点都写出来希望能给你提供一份可以照着对照复习的清单。1. 笔试全景题量、时长与考察重心的整体判断1.1 笔试基本信息与做题节奏这次笔试的题量属于典型的基础选择题算法题组合。10道单选覆盖了Java基础、Android四大组件、Handler机制、View绘制、网络协议这些方向5道多选明显更偏原理和源码比如Binder通信的数据拷贝次数、Activity启动流程里涉及哪些系统服务、线程池执行逻辑这些多选是真的很考验功底少选多选都不得分。2道编程题一道偏数据结构、一道偏业务场景。我个人的做题节奏是先快速浏览一遍所有题目把一眼就会的题先做掉把需要思考的标记出来。这样做的好处是避免在一道难题上卡太久导致后面会做的题没时间写。实际上我单选中有3道题是花了较长时间的比如有一道关于Activity启动模式与Intent Flag组合效果的题需要结合taskAffinity和栈的情况综合判断多选里有1道关于Handler同步屏障的题我需要回忆Looper中消息循环的具体实现才能确认。1.2 从题目分布看考察重心把这次笔试的题目归类之后重心其实很明显算法与数据结构大约占30%但并不是纯粹考查竞赛型算法而是偏向数据结构的灵活应用Android四大组件与消息机制大约占25%这是选择题的主力View体系与事件分发大约占15%重点在invalidate和requestLayout的区别、事件分发拦截逻辑三方框架与原理大约占20%OkHttp、Glide、RxJava都有涉及性能优化与工程化大约占10%比如R8混淆、启动耗时优化一个值得注意的倾向是题目几乎不会直接问请解释MVP和MVVM的区别这种概念题而是给你一个具体的业务场景让你判断用哪种方案最合适。比如有一道题给了一个列表页需要展示不同位置、不同尺寸、不同优先级的图片应该怎么设计加载策略这就是典型的Glide和图片加载框架的实战场景题。这反映出小红书的笔试风格比较务实它不关心你背了多少概念而关心你在真实业务压力下能不能做出合理的架构决策。对于准备笔试的同学来说这意味着复习的时候不仅要懂原理还要能把原理对应到具体场景里。2. 编程题复盘两道算法题的完整做题思路2.1 第一道题有序数组归并的变体题目大概描述是这样给定两个严格递增的整数数组A和BA的长度足够容纳合并后的所有元素A的有效元素长度为n数组总容量为nmB的有效长度为m要求把B合并到A中保证合并后A仍然严格递增且不能使用额外数组空间。乍一看这就是经典的合并两个有序数组问题直接套用从后往前遍历的解法就行但题目设了一个小陷阱它强调了严格递增也就是说数组中可能存在重复元素合并时需要去重。很多人在笔试时会忽略这个条件直接用普通合并逻辑处理导致输出结果中出现重复元素被隐藏测试用例卡住。我当时看到严格递增这个描述时停顿了一下在验证用例时特意构造了一个边界场景A {1, 3, 5}B {3, 5, 7}如果不做去重合并结果会是{1, 3, 3, 5, 5, 7}这就不满足严格递增了。正确的输出应该是{1, 3, 5, 7}。我的思路是仍然从后往前处理但比较的时候增加一个skip标记。因为A原始大小为nB大小为m合并后的有效长度不会超过nm。从尾部开始每次取A和B当前指针指向的较大值放到最终位置如果该值与当前已放置的前一个位置的值相同就跳过不放入。核心代码如下public int mergeArrays(int[] A, int n, int[] B, int m) { int i n - 1; int j m - 1; int writeIndex n m - 1; int lastVal Integer.MIN_VALUE; while (i 0 || j 0) { int val; if (i 0) { val B[j--]; } else if (j 0) { val A[i--]; } else { if (A[i] B[j]) { val A[i--]; } else { val B[j--]; } } if (val ! lastVal) { A[writeIndex--] val; lastVal val; } } return writeIndex 1; // 返回合并后的有效长度 }这个解法的关键在于把去重和合并两个操作在一次遍历中完成。如果不做去重直接用后往前放时间复杂度是O(nm)空间O(1)做了去重后时间复杂度仍然是O(nm)但需要注意A和B各自内部已经是严格递增的所以相同值只会出现在A和B之间不会出现在单一数组内部这一点利用上之后处理起来很干净。2.2 第二道题直播场次安排的贪心问题第二道编程题更贴近业务。题目大意是某平台有若干直播场次要安排在同一天每场直播给定开始时间start[i]和结束时间end[i]同一个频道同一时间只能播一场直播问最少需要多少个频道才能把所有直播场次都安排下。这是一道经典的会议室II变体也是面试里经常出现的贪心优先队列题。看到题的时候我第一反应就是用最小堆维护当前正在直播的场次的结束时间。思路是先把所有场次按开始时间排序然后遍历每一场直播如果当前最早结束的直播已经播完也就是堆顶的结束时间小于等于当前直播的开始时间就把这个频道复用弹掉堆顶把当前直播的结束时间压入堆否则就需要在堆里新增一个结束时间代表新开一个频道。最终堆的大小就是最少需要的频道数。public int minChannels(int[][] sessions) { if (sessions null || sessions.length 0) { return 0; } Arrays.sort(sessions, (a, b) - a[0] - b[0]); PriorityQueueInteger minHeap new PriorityQueue(); for (int[] session : sessions) { if (!minHeap.isEmpty() minHeap.peek() session[0]) { minHeap.poll(); } minHeap.offer(session[1]); } return minHeap.size(); }这道题本身的算法并不难如果刷过力扣的类似题目基本5分钟内就能写出核心逻辑。但笔试的隐藏难点在输入处理上题目给出的时间段可能没有排序且存在跨天的场次比如开始时间23:00结束时间次日01:00。如果直接用结束时间是否小于等于开始时间判断复用跨天场次会导致错误。我当时在草稿纸上推演了一下发现如果存在跨天场次严格来说应该把跨天场次拆成两段处理否则贪心策略会失效。由于时间紧张我先采用了把时间统一转换成分钟数、再对跨天做加24小时处理的方案并通过了样例不确定隐藏测试是否完全覆盖。2.3 笔试中写算法题的节奏与取舍两次编程题做的过程中我自己的体会是笔试不是竞赛目标不是解出最优雅的解而是在有限时间内拿到尽量多的分数。比如第二道题如果我先花大量时间处理跨天场次的边缘case很可能导致代码没有写完。我当时的选择是先保证main逻辑正确、通过示例然后留出时间做边界处理。这种先主干后分支的做题节奏在笔试中非常实用。另外想提醒一点牛客网这类在线判题平台编译器不会帮你输出额外的debug信息一旦提交报错只能靠自己的代码和输出日志去猜问题。所以平时刷题时最好养成提交前先构造2到3个边界测试用例的习惯而不是写完就立刻提交。3. 选择题里的隐藏考点Android基础逐题回顾3.1 Activity启动模式与Intent Flag的组合判断单选里有一道题我记得很清楚给出的场景是一个App中有A、B、C三个ActivityA启动B时Intent设置了FLAG_ACTIVITY_NEW_TASKB的launchMode是singleTask。在B已经存在于某个任务栈中的情况下再次通过这个Intent启动B问发生什么。这题考察的是Activity启动模式与Intent Flag之间的优先级关系。这里有一个关键知识点Intent中的Flag优先级高于manifest中声明的launchMode。也就是说如果Intent设置了FLAG_ACTIVITY_NEW_TASK那么即使Activity在manifest里声明的是standard模式这次启动仍然会走NEW_TASK的逻辑。但同时singleTask本身就带有NEW_TASK的语义两者叠加时遵循的还是singleTask的行为。具体到问题上singleTask的Activity在启动时系统会查找是否存在一个包含该Activity的任务栈且栈顶是否就是该Activity。如果已存在会调用该Activity的onNewIntent并把该Activity之上所有Activity出栈。这个过程中如果该Activity已经处于栈顶则更简单只是回调onNewIntent。我当时在选项之间犹豫了很久因为有一个选项说得比较有迷惑性会重新创建一个新的实例。如果只看FLAG_ACTIVITY_NEW_TASK可能真的会误以为创建一个新任务栈但实际上singleTask已经限制了该Activity在整个系统中最多只有一个实例。最终我选了调用已有实例的onNewIntent并将此Activity以上的所有Activity移除这个方向应该是对的。3.2 Handler与消息机制同步屏障和IdleHandler多选里有一题考察Handler机制选项里出现了同步屏障异步消息IdleHandler这些概念。普通开发中用到这些场景不算多但面试笔试却非常喜欢考。我回忆一下这道题的核心在Looper的loop()中MessageQueue.next()会先判断是否有同步屏障如果有就只取异步消息执行直到移除同步屏障如果队列中没有异步消息就会阻塞在这里。IdleHandler则是在消息队列空闲时执行的任务也就是说当消息队列暂时没有消息需要处理时会去执行IdleHandler队列中的任务。这道题的考点在于同步屏障的用途主要是为了保证UI渲染这类高优先级任务能及时执行比如在ViewRootImpl中scheduleTraversals()之前会先postSyncBarrier()确保DoFrame消息不被阻塞。理解了这一点才知道Choreographer和Handler之间的关系也才能答对什么时候会使用同步屏障这类选项。我当时在多选里勾了同步屏障在消息队列中没有异步消息时会阻塞和IdleHandler在消息队列空闲时执行有一个选项同步屏障一旦插入就会一直存在直到显式移除没有勾因为它在没有异步消息且屏障存在时会一直阻塞但最后在移除屏障或队列为空且没有空闲消息时会被处理不能说一直存在这个说法有歧义。3.3 View绘制与事件分发requestLayout和invalidate的区别有一道单选问的是调用View的requestLayout()之后在下一帧会执行哪些方法而调用invalidate()之后又会执行哪些方法。这道题如果在项目里只做UI开发可能不会想得很深但如果读过View的源码答案就很清晰。requestLayout()会从当前View一直向上传递到ViewRootImpl触发performTraversals()也就是完整的measure、layout、draw流程。注意requestLayout并不一定会触发重绘如果布局没有变化draw阶段可能不会执行。而invalidate()只会在当前View的ViewGroup上执行invalidateChild重绘该View所在的脏区域不会触发measure和layout。第三个方法postInvalidate()是invalidate()的线程版本允许在非UI线程中调用但其底层仍然是post一个消息到UI线程本质还是在UI线程执行invalidate()。这道题的一个易错点是invalidate()会调用ViewGroup的invalidateChild最终触发ViewRootImpl的scheduleTraversals()所以在某些情况下invalidate()也会导致measure和layout重新执行比如View的尺寸发生了变化。这个并不绝对的思维是笔试里真正的考点。读源码不能只记住结论还要理解结论的适用条件。3.4 并发、存储与组件从使用到原理的跨度选择题里还出现了一些更偏基础、但又容易被忽略的内容。比如有一道题考察了ThreadPoolExecutor的核心参数corePoolSize、maximumPoolSize、keepAliveTime和workQueue之间的关系。题目给了一个场景核心线程数5最大线程数10阻塞队列长度为8当同时提交20个任务时实际会有多少任务正在执行、多少任务在排队。这类题不算是Android专属而是Java并发基础。解题的关键在于理解线程池的任务提交流程核心线程满 - 入队 - 队列满 - 创建非核心线程 - 最大线程数满 - 执行拒绝策略。如果队列没有满任务是先排队而不是立刻创建新线程这点很多人会搞反。另一道题考察了ContentProvider的内容URI匹配规则还提到了FileProvider在Android 7.0及以上版本通过content:// Uri共享文件的机制。这类题在项目里用得多但在笔试中反而容易被忽视。出这道题的意图大概是考察你是否理解ContentProvider是跨进程组件、它的onCreate调用时机、以及URI匹配时host和path的校验顺序。还有一道跟Kotlin协程相关的题问你withContext(Dispatchers.IO)和async {} await()的区别。这题在Java岗可能不会出现但Android岗现在Kotlin已经是主流考察协程是合理的。我当时的理解是withContext是挂起函数会把代码块切到指定调度器执行执行完再切回来整个代码块是一个suspend函数可以从代码块中返回值而async await则更偏向并行执行多个任务然后统一等待结果。它们的共同点是都会切换调度器区别在于async更适合并发场景。这道题还有两个选项是withContext会阻塞当前线程和async一定会创建新的协程这两个明显是错的withContext是挂起而非阻塞async在特定情况下可以复用父协程的调度器不一定会创建新线程。4. 框架与系统机制题问的是你读过源码吗4.1 Binder与一次拷贝的秘密多选里有一道Binder相关的题选项里出现了Binder相对于共享内存的优势一次拷贝发生在哪里mmap在这其中起什么作用。这道题如果只停留在知道Binder是一次拷贝的层面是拿不准选项的。Binder的底层逻辑是这样的当进程A发起一个Binder调用时数据要从A的用户空间拷贝到A的内核空间然后目标进程B通过映射关系直接读取这块内核空间里的数据而不需要再从内核空间拷贝到B的用户空间。因为Binder驱动在初始化时会通过mmap把内核缓冲区映射到进程B的用户空间所以从内核空间到B进程的拷贝其实是通过内存映射完成的不再有真正的数据复制。这就是Binder一次拷贝说法的来源。另外Binder还有一个共享内存方案不具备的优势Binder协议本身支持调用者UID/PID校验这在Android的权限控制中非常关键比如系统服务需要确认调用方是否有权限。而传统共享内存本身不带这种校验机制。所以Binder可以同时解决数据传输出跨进程权限校验生命周期管理三个问题这也是Android选择Binder作为IPC核心的原因。我在笔试时把Binder传输数据大小有限制这个选项也勾了因为BinderTransactionBuffer大小一般是1MB左右超过就会抛TransactionTooLargeException。后来想想这道题如果问的是优势这个选项其实不算优势而是一个限制可能不该勾。这也是多选题容易失分的地方选项本身表述正确但答非所问。4.2 AMS与Activity启动链路一道口述题化身的单选有一道选择题问的是冷启动一个Activity不涉及进程创建的情况下从startActivity()到Activity被创建经历的系统服务调用顺序是什么。这道题本质上是一道口述源码流程题但出成选择题之后选项里全都是顺序打乱的调用链。我当时回忆的链路是startActivity() - ActivityTaskManager.getService()通过AIDL调用AMS - AMS检查调用者权限和Activity的Info - AMS通过ApplicationThread.scheduleLaunchActivity()通知应用进程 - ApplicationThread通过H类切换到UI线程 - 在UI线程执行ActivityThread.handleLaunchActivity() - 依次执行Activity.attach() - Activity.onCreate()。如果应用进程不存在则需要先去AMS请求Zygote fork进程但题目明确说了不考虑进程创建所以Zygote不在选项里。这道题还有一个坑就是选项里混杂了ActivityManagerService是直接运行在应用进程中的这个说法。这是错的AMS运行在SystemServer进程里应用进程通过Binder与AMS通信。很多人如果只背过AMS管理Activity但对AMS到底在哪里、如何与应用进程通信不清晰很容易选错。4.3 三方框架原理从面试常客看笔试倾向选择题里也出现了一些三方框架相关的题目比如OkHttp的拦截器执行顺序、Glide的缓存机制、RxJava的线程切换原理。这些题目并不算很深入但都能看出笔试方对候选人实际项目质量的关注。OkHttp那道题问的是自定义Interceptor添加在哪个位置时可以被重定向响应观察两次。答案是自定义ApplicationInterceptor默认添加在RetryAndFollowUpInterceptor之后、BridgeInterceptor之前。如果你理解OkHttp的拦截器链机制这道题其实就是在考察你对责任链模式的理解。Glide那道题问的是Glide默认的内存缓存与磁盘缓存层级怎么协同。这道题可以用一个日常场景来理解当你在一个列表里快速上下滑动时看到每一张图都很快就是因为内存缓存(LruCache)和引用计数活跃资源缓存共同在工作。如果内存中没有命中再去看磁盘缓存磁盘也没有才会请求网络。这种三级缓存的设计思路和我们在项目中做图片加载性能优化时的一致性。在准备这类题目时我的建议是不要只背拦截器有哪些而是要把OkHttp的整个请求链路画出来从责任链进入、重试、桥接、缓存、连接、请求服务器到最后回调每一步做了什么都梳理清楚。这样不管笔试题目从哪个角度切入都能应对。4.4 性能优化与工程化R8、启动优化与稳定性有一道题跟R8相关问的是R8做代码压缩时哪些情况下不会被移除。答案是通过反射访问的类和方法、被native方法引用的类、通过注解保留的API等都需要额外配置keep规则。这道题其实就是在考察你对混淆和裁剪机制的实际理解。另外一个单选问的是Android 10及以上系统对启动流程的优化措施是怎么实现的。选项里出现了AOT编译、JIT编译、dex2oat等概念实际答案是Android运用了混合编译模式。应用安装或OTA后先做部分AOT编译运行过程中再通过Profile引导编译常用的代码路径这样既能保证启动速度又不至于安装时间过长。这道题考的是Android系统机制而不是纯粹的业务开发。如果平时只是调UI写接口这类题确实容易被忽略。我自己的感觉是小红书这类公司的业务体量决定了它们的Android工程师必须关注性能指标因此在笔试里设置性能优化相关题目是很合理的。备考时不要只盯着Activity、Fragment、Handler这些基础概念也要花时间了解系统编译流程、安装包治理、启动耗时统计分析这些更工程化的东西。5. 失分点复盘这些题其实不该错5.1 多选题的漏选是最大的痛5道多选里我至少有2道是能确定答对的但其余3道都出现了犹豫原因很简单多选很容易被表述正确但不符合题意的选项干扰。比如前面提到的Binder那题TransactionTooLargeException的表述本身是对的但它不是Binder优势还有Handler那题同步屏障一旦插入就一直存在本身不好判断因为你得知道MessageQueue在什么情况下会主动移除屏障才能决定是否勾选这个选项。我的教训是做多选时如果选项中出现绝对化的关键词比如一定所有任何要特别谨慎。Android系统很少有绝对的结论。一道题如果出现了同步屏障一旦插入就一直存在几乎可以断定这个表述是错的因为屏障在消息处理完后会被移除。但在考场上我还是犹豫了说明平时对源码细节的掌握还不够扎实。5.2 算法题的边界情况处理不足第二道编程题的跨天场次问题是我这次笔试最大的遗憾。如果在做题时能冷静下来把开始时间大于结束时间视为跨天在比较时统一加上当前时间24小时这个处理题目就能完整解出。但我先写了主逻辑在边界处理上考虑得不够细致虽然样例通过了但不确定隐藏测试是否有跨天数据。事后复盘我觉得这是我平时刷题时的一个坏习惯刷力扣的题目时输入数据通常都是规整的很少有这种业务型的边界条件。但笔试的算法题往往就是针对真实业务场景的简化输入数据未必规整所以平时刷题时要有意识构造这类非标准输入。5.3 时间分配上的失误我前面提到我做了25分钟选择题但实际上有大约8分钟都在同一道Activity启动模式的题上推翻重想。这8分钟如果冷静下来其实可以更快地解决因为我已经做过类似的题只是被选项的措辞绕进去了。更好的策略是遇到犹豫的选择题先标记待定把整张卷子会做的题稳定拿分回头再处理犹豫题。笔试不是研究时间有限确保简单题全对比挑战难题更有价值。5.4 准备盲区业务场景题暴露出项目广度的不足这次笔试有几道题明显是结合内容社区App的业务场景出的。比如图片加载策略、直播场次安排、不同权限下的文件读写方式。我平时做的项目里图片加载和文件读写都有涉及但对不同加载策略的优先级排序、不同Android版本文件读写的变化这些细节没有系统整理过。做这些题的时候会感觉好像接触过但选不出最合适的方案。准备这一类题目的方法不是多背题而是真正把自己做过的项目从头梳理一遍你的图片加载是怎么设计的、有没有考虑过弱网下怎么处理、多图列表的内存占用怎么控制、本地文件在Android 11以上怎么读取。如果项目复盘做得细致业务场景题其实是比较容易得分的。6. 给后来者的备考路线从基础到笔试的实操建议6.1 算法部分刷题不是越偏越好从这次笔试来看算法题难度整体适中但比较贴近真实业务场景。我的建议是优先刷力扣的热题100尤其是有序数组操作、区间合并、贪心优先队列、双指针这类题型。这几类题在笔试中出现频率较高。另一类是输入格式的处理比如字符串解析、时间转换也值得练习。不太建议花大量时间在纯竞赛型题目上比如网络流、复杂动态规划优化、后缀自动机这类在Android岗笔试里出现概率很低。6.2 Android基础建立机制面而非点状记忆的复习方式很多人在复习Android基础时习惯用八股文的方式把知识点背成一条一条的。但笔试对细节的考察决定了这种复习方式的效果有限。更好的复习方式是以机制为主线构建一张知识网络。比如复习Handler时不只是背Handler用于线程间通信而是顺着这个主线往下挖Looper怎么创建、MessageQueue如何存储消息、next()方法如何阻塞和唤醒、同步屏障在什么时候插入、IdleHandler什么时候执行、底层为什么使用epoll来休眠唤醒。把这个知识链补齐之后关于Handler的多数笔试题目你都能应对。同理复习Activity时不要只记住四种启动模式的定义而是要把启动模式与任务栈的关系Intent Flag与launchMode的优先级一个App的默认任务栈如何组织多个App之间如何通过taskAffinity组织任务栈串起来。这样的复习方式虽然初期比较慢但每一份努力都会转化为做选择题时的确定性。6.3 源码阅读按什么顺序读效果最好源码阅读是准备Android岗笔试时让人头疼的部分。我的建议是不要漫无目的地读源码按高频考点优先的顺序来。首选Handler和MessageQueue因为这是面试和笔试中出现频率最高的源码而且代码量不大读一遍源码大概两三个小时就能完成。其次是View的measure/layout/draw流程重点理解requestLayout和invalidate的区别。然后是Binder不用从头读binder驱动只需理解一次拷贝、mmap的作用、以及AIDL生成代码的执行流程。之后是AMS和ActivityTaskManager的启动流程把链路里的关键类比如ActivityRecord、TaskRecord、ActivityStack搞清楚。最后是三方库OkHttp的拦截器链、Glide的缓存机制这些按需补充。读源码时不要逐行读也不要追求每个方法都理解。核心是抓住主线比如读Handler时只要把sendMessage - enqueueMessage - next() - dispatchMessage这条链路读完再逐步补充其他细节。如果在网上搜索源码分析文章注意看文章是否基于你当前使用的Android版本。6.4 关于笔试心态当作一次真实项目的排障最后想聊一下笔试时的心态。我自己的感受是笔试更像是一个在限定资源下做决策的过程而不是展示你会多少东西的过程。比如一道题有多种解法最优解可能比较复杂但在笔试环境下一个时间复杂度和空间复杂度稍差、但正确性容易保证的解法反而更值得优先写出来。另一个体会是遇到不确定的题目先标记跳过避免情绪被一道题拖住。90分钟说长不长说短也不短稳住节奏比什么都重要。我备考时最大的一个感受是与其刷100道新题不如把自己已经做过的题目从头梳理一遍。你可以把题库里做过的每道题按核心考点、我的解法、更优解法、易错点四个方面整理成一个文档考前翻一遍。这样看起来工作量不小但坚持下来之后你会发现自己知识体系里的空白点被一点点填补了。这次笔试结束之后我最大的收获不是我拿到了多少分而是第一次发现自己对Android系统的理解还有很多靠背而不是懂的地方。笔试里那些看似抠细节的题目其实是在逼你把系统中的每个机制真正理解透彻。如果你正在准备类似的笔试希望这篇复盘能帮你少踩一些坑把有限的备考时间花在真正重要的地方。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →