小红书Android笔试复盘:启动模式、Handler与源码细节
2026/9/1 4:40:34 网站建设 项目流程

2024年春招那阵子,我投了小红书的Android开发岗,简历筛选通过后,收到了第一批笔试邀请。这批笔试和我想象中不太一样,它不单纯是算法题堆砌,而是把Android基础、源码理解、代码设计、项目场景判断都揉在一起考。我整理了一下当时的复盘笔记,把考点分布、真题思路、错题原因都梳理出来,今天一次性写清楚,给接下来准备Android大厂笔试的朋友做个参考。

1. 笔试整体情况与考点分布

先说这批笔试的基本盘。整场笔试大约两个小时,题量不小,总分100分,题型分三块:单选题、多选题、编程题。多选题和单选题大概有30道左右,覆盖Java/Kotlin语法、Android四大组件、Handler消息机制、自定义View绘制流程、性能优化、三方框架原理这些。编程题有两道,一道是纯Java/Kotlin编码题,一道是偏Android场景的代码设计题。

我当时的感受是,笔试难度属于“中偏上”,但它难在考察点很细,细到你在项目里根本不会注意的角落。比如有一道题问startActivityForResult废弃后,新的ActivityResultLauncher在不同场景下的回调时机,这个知识点平时写业务代码基本不会深究,但笔试就是要考你对API演进的掌握程度。

另一个印象很深的是,多选题的判分很严格,少选、错选都拿不到分。所以做题策略上,没有十足把握的选项就不要选,保住确定项至少能拿到部分分。

从考点热度来看,我统计了一下自己错的题和周围同学交流后的反馈,排在前几位的知识点分别是:Activity启动模式与返回栈、Handler/Looper/MessageQueue机制、Binder原理与AIDL、协程的调度与异常处理、Gradle与AGP版本适配、RecyclerView复用机制、内存泄漏的场景判断。

这些知识点其实都是Android面试的“老八股”,但笔试的考法更倾向于“给你一段代码,让你判断输出结果或运行状态”,而不是直接问你“什么是启动模式”。这就要求你在准备时不能只背概念,必须把每个机制的核心源码和运行流程吃透。

2. 单选与多选:细节题的坑

2.1 单项选择题的考察方式

单选和多选混在一起,考察面很广。我记得有一道题是给出了一段代码,让你判断在哪种启动模式下会执行onNewIntent()。这是典型的源码级考察,不熟悉Activity的启动流程就很容易选错。

我当时靠的是自己总结的一张表,这里直接分享出来:

启动模式是否新建实例是否回调onNewIntent与现有Task的关系
standard每次新建进入启动者所在Task
singleTop栈顶复用,否则新建是(栈顶复用时)启动者所在Task
singleTask栈内唯一,否则清除其上Activity是(已有实例时)指定Task或启动者所在Task
singleInstance全局唯一,独享Task独立Task

这道题问的是一段代码:Intent intent = new Intent(MainActivity.this, SecondActivity.class); startActivity(intent);,然后SecondActivity声明为singleTask,而且已经在其他Task中存在。答案就是会复用已有实例并回调onNewIntent,同时把栈顶以上的Activity全部出栈。

这类题准备起来其实有套路,核心就是理解四种模式与Task的关系,以及onNewIntent触发条件。我在复习时做了一个小Demo,把四种模式都跑了一遍,打印出onCreateonNewIntentonStartonResumeonDestroy的调用顺序,这样比死记结论有用得多。

2.2 一道类似AIDL的易错题

还有一道题让我印象很深,题目本身不复杂,但考察得很细。大意是给你一段定义AIDL接口的代码,让你判断编译后自动生成的Stub类和Proxy类的职责。

我当时差点选错了onTransact()相关的选项。正确的理解是:Stub是Binder的服务端,接收客户端IPC请求,在onTransact()中根据code分发到对应的接口方法;Proxy是Binder的客户端代理,把接口方法调用包装成transact()流程。两者通过Binder驱动完成数据序列化和反序列化。

这种题如果没有真正用AIDL写过跨进程通信,很容易停留在“知道Binder是Android的IPC机制”这个层面试,但笔试偏偏考的就是你在实现层面的理解程度。我当时没有直接死在Binder原理上,而是把ServiceAIDLMessenger三种跨进程通信方式都手写了一遍,对比了各自的适用场景。

多说一句,笔试里有好几道题都是这样:表面在考API使用,实际在考底层实现。所以如果你准备时间充裕,建议把Binder驱动层的基本流程也过一遍,不用深入到内核源码,但至少要能讲清楚:数据如何从客户端APP进程拷贝到Binder驱动,再从驱动拷贝到服务端进程,以及mmap在内核空间映射中的作用。

3. 编程题:启动模式复用与状态提升

3.1 第一道编程题:模拟返回栈与启动模式

编程题第一道其实是“披着Android外衣的栈模拟题”。题目内容是:给定一组启动序列,每个Activity有指定的启动模式,要求模拟运行后每个Task栈的栈内元素。这题说白了就是考察你对启动模式的理解,加上基础的数据结构运用能力。

我当时的实现思路是:用ArrayList<ActivityStack>模拟多个Task,每个Task内部用一个ArrayList<ActivityRecord>模拟栈。处理singleTask时,要先检查所有Task中是否存在该Activity的实例,若存在则需要把该实例之上的Activity全部弹出,然后把它移到栈顶;standard模式就直接压入启动者的栈;singleTop需要判断栈顶是否是同一Activity。

这题的难点不在代码量,而在边界条件的处理。比如:singleTask实例存在的时候,如果它不在栈顶,需要先把其上方的Activity弹出,再把该Activity移到栈顶,同时要维护一个全局列表中Activity的存活状态。还有一点容易漏,就是启动一个singleInstance的Activity时,它需要独享一个Task,如果该Activity已经存在,则需要将其Task整体移到前台。

我最后用Kotlin实现的,核心逻辑大概长这样:

data class ActivityRecord(val name: String, val launchMode: LaunchMode) class ActivityStack { val tasks = ArrayList<ArrayDeque<ActivityRecord>>() fun launch(record: ActivityRecord, fromTaskIndex: Int) { when (record.launchMode) { LaunchMode.STANDARD -> { tasks[fromTaskIndex].addLast(record) } LaunchMode.SINGLE_TOP -> { val stack = tasks[fromTaskIndex] if (stack.isNotEmpty() && stack.peekLast().name == record.name) { // 复用栈顶,不新建实例 } else { stack.addLast(record) } } LaunchMode.SINGLE_TASK -> { var targetIndex = -1 var indexInStack = -1 tasks.forEachIndexed { idx, stack -> val pos = stack.indexOfLast { it.name == record.name } if (pos >= 0) { targetIndex = idx indexInStack = pos } } if (targetIndex >= 0) { val stack = tasks[targetIndex] while (stack.size - 1 > indexInStack) { stack.removeLast() } } else { tasks[fromTaskIndex].addLast(record) } } LaunchMode.SINGLE_INSTANCE -> { // 查找是否已有独立Task中存在该Activity var targetIndex = -1 tasks.forEachIndexed { idx, stack -> if (stack.size == 1 && stack.peekFirst().name == record.name) { targetIndex = idx } } if (targetIndex == -1) { val newStack = ArrayDeque<ActivityRecord>() newStack.addLast(record) tasks.add(newStack) } } } } }

这段代码我后续在IDE里跑通了,处理了大部分边界情况。不过笔试的时候时间有限,我建议优先把题意理解清楚,再动手写。这类题对代码能力的要求不算很高,更看重你能不能像计算机一样“模拟”系统的调度逻辑。

3.2 第二道编程题:实现一个响应式事件总线

第二道题偏实际工程,要求用Kotlin实现一个简化版的事件总线,支持主线程和子线程的事件分发,并具备一定的生命周期安全能力。这题直接命中了我在项目里用过EventBus和LiveData的痛点,但因为平时只是调用API,真正手写底层时发现坑不少。

核心要求大概是:

  • 支持在任意线程发布事件,通过注解或注册目标方法反射调用。
  • 事件分发时可以指定在发布者线程执行,还是切到主线程执行。
  • 需要处理订阅者生命周期,避免内存泄漏。

我先写了接口定义,再写注册表,再写分发逻辑。关键点在于反射获取订阅方法的时候,需要区分方法的线程模式。我当时用的是注解方式,类似:

@Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) annotation class Subscribe(val threadMode: ThreadMode = ThreadMode.POSTING) enum class ThreadMode { POSTING, MAIN }

然后通过注册表,把所有订阅类中带有@Subscribe注解的方法保存起来。事件发送时,遍历所有注册过的实例,找到方法参数与事件类型匹配的方法,再根据线程模式决定是直接调用还是post到主线程Handler。

我当时写这题的过程比较顺利,因为项目的模块通信用的就是轻量级事件分发,我对订阅管理和线程切换都很熟。但有一个点差点写错:反射获取方法时,需要把isAccessible设为true,否则在Java 8+的环境下,非公共方法反射调用会抛IllegalAccessException

还有一个细节是订阅者的注册需要支持lifecycleOwner绑定。简单实现就是在注册表里保存一个自定义封装对象,里面同时持有目标实例和关联的Lifecycle实例,在ON_DESTROY时自动解绑。这个如果笔试要求没有那么严,可以做成简化版,只支持手动unregister,但写清楚设计意图也能加分。

这题总结下来,考察的是工程能力。你不光要把API设计得足够好读,还要清楚反射、回调、线程切换这些基础操作背后的坑。备考时我建议大家不要停留在“用过EventBus”,而是自己动手写一次简化版,哪怕只支持一个线程模式,也能让你对这些框架的理解上一个台阶。

4. 高频Android知识点自查:从原理到实战

4.1 Handler/Looper机制:不可缺失的底层功底

笔试中Handler相关的题几乎是必考的。要么给你一段代码让你判断handleMessage的执行顺序,要么问你ThreadLocalLooper中的作用,要么问IdleHandler什么时候被调用。

说实话,Handler这套机制本身不难,难的是把它放到真实场景里推演。比如这样一道题:主线程创建一个Handler,子线程通过Handler.post一个任务,紧接着主线程的MessageQueue里还有之前执行的延迟消息,问任务执行顺序。要准确回答,必须清楚MessageQueue是按时间排序的优先队列,Handler.post不指定延迟时,when=0,会排在队首。

还有一种常见考法,就是让你判断以下代码是否会造成内存泄漏:

public class MainActivity extends Activity { private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { // 更新UI } }; }

答案显然会漏。原因在于非静态内部类持有了外部类的引用,而Handler又通过Looper在消息队列中持有MessageMessage持有Handler,于是形成了一条“主线程Looper -> MessageQueue -> Message -> Handler -> Activity”的强引用链。如果消息还没处理完,Activity就无法被回收。

我当时答这类题的时候,会有意识地把整个引用链写出来,这样既能帮助自己理清思路,也能在答案中体现出你对对象的引用关系足够敏感。

4.2 Binder机制与IPC:不止是说概念

小红书这批笔试没有直接问你“Binder有什么优点”,而是考了AIDL生成的Java类结构和事务调用过程。这说明现在的Android笔试已经进入了“源码理解”时代,至少需要你熟悉AIDL的编码流程、StubProxy的分工、transactonTransact的调用时机,以及linkToDeathunlinkToDeath的使用场景。

我给自己的复习方法是:在Android Studio里新建一个AIDL文件,编译后打开build/generated/source/aidl下的生成代码,逐行阅读。你会发现Stub是一个继承Binder的抽象类,内部实现了onTransact方法;而客户端通过Proxy类调用接口方法时,会构造_data_reply两个Parcel,然后调用transact方法,把调用请求发给远程服务。这个过程如果你自己手动写过一遍,笔试再怎么变形都不怕。

还有一个需要留意的点,是从Android 8.0开始,Service必须使用前台服务才能保证不被系统频繁回收,而bindService的调用方式、Context.BIND_AUTO_CREATE的作用,以及onServiceConnected回调所在的线程,这些都是常考的点。这里补一句:onServiceConnected默认回调在绑定者的主线程,所以里面可以直接更新UI,但跨进程的耗时操作不能放在里面。

4.3 内存与性能优化:笔试里的“场景判断”

性能优化在笔试中不会让你直接写一个大项目,而是给你一个具体的现象,让你推断可能的原因。比如“首页启动时出现明显卡顿,可能的原因有哪些”。这类多选题,往往需要你从多个维度去分析。

我整理了一个自查表,笔试前反复看了几遍:

现象可能原因
启动卡顿主线程做IO、布局层级过深、过渡绘制严重、频繁GC
列表滑动掉帧RecyclerView嵌套、Item布局过度重绘、图片没有缓存、频繁notifyDataSetChanged
内存上涨静态集合持有Activity、Handler延迟消息未移除、匿名内部类持有外部引用
电量消耗异常频繁唤醒CPU、WakeLock未释放、网络轮询短、定位回调不注销

笔试考的就是你把原因和现象对应起来的能力。在准备这类题目时,我建议把Android性能优化的几个方向都过一遍,包括布局优化(用ConstraintLayout降低嵌套层级)、绘制优化(onDraw中避免新建对象)、内存优化(合理使用SparseArray替代HashMap)、启动优化(异步初始化非必要组件、IdleHandler延迟加载)、APK瘦身(移除无用资源、使用App Bundle)。

当时有一道题问“冷启动时在Application.onCreate中做大量初始化有什么影响”,选项包括“延长冷启动时间”“增加卡顿概率”“可能导致ANR”。这些都是正确的。但你选了这些之后,还要想一想为什么:ApplicationonCreate执行在主线程,且早于第一个Activity的onCreate,如果在这里做大量磁盘IO或者网络请求,会直接阻塞UI渲染。

5. 容易被忽略的细节:从语言到构建工具

5.1 Kotlin与Java混编时的可见性

笔试里有几道题,老老实实考Kotlin语法,但角度比较刁钻。比如Kotlin的internal关键字在Java中会被编译成public,这导致如果你在混合项目中,Java代码可以绕过internal限制访问到Kotlin模块内部的API。

这种题如果不踩一次坑,很难答对。我实习的时候就遇到过一个问题:一个Kotlin库模块中定义的internal类,居然在Java代码里能直接引用。后来查了文档才知道,internal在JVM平台并不是真正的“模块私有”,编译后会变成public,其名称也会被混淆成Modifier之类的名字。所以笔试一旦涉及Kotlin可见性,记得务必选择“internal对Java不生效”这个选项。

另外还有一个点,Kotlin的空安全在Java互操作时也会失效。Java传入null给Kotlin非空参数,运行时才会抛出NullPointerException,而不是编译期。这意味着你不能说“Kotlin代码一定没有NPE”。

5.2 Android Gradle Plugin 与新版工具链

2024年春招这批笔试,也考了一些跟构建工具链相关的题目。比如AGP版本与Gradle版本的适配关系、compileSdktargetSdkminSdk的区别,以及BuildConfig字段如何开启。

有一道题问:Android Studio Hedgehog(2023.1.1 Patch 2)到底支持哪些AGP版本?我复习的时候查过官网映射表,Hedgehog比之前的Giraffe版本新一些,支持AGP 8.1到8.3左右的版本,如果你本地使用AGP 8.0,整体也能兼容运行。但笔试不会问你具体版本号,而是会给你几个组合,让你判断哪些是可用的。这类题其实说穿了就是考察你是否关注过Android官方发布页的版本兼容表。

我的建议是,准备笔试时把当前主流Android Studio版本对应支持的AGP和Gradle版本范围记下来,不用背精确版本号,但要能分辨“AGP 8.2配Gradle 7.6”这种组合大概率是错的。

另一个经常被忽略的点是namespace的配置。AGP 8.0之后,build.gradle中的package属性被移除了,必须替换为namespace,否则编译报错。笔试虽然不会让你现场编译,但会给你一段配置代码,让你判断哪里写错了。如果你平时升级过项目,大概率一眼就能看穿。

5.3 文件Provider与跨应用URI访问

热词搜索里连续出现了好几个content://com.baidu.searchbox.fileprovider/...content://com.ss.android.uri.key/external_root/...这样的访问链接,这说明实际开发中FileProvider的配置和URI授权是非常高频的问题。笔试不会直接给你这些链接字符串,但会考你对FileProvider的理解。

核心考点包括:

  • 从Android 7.0开始,file://URI直接暴露给其他APP会抛FileUriExposedException,必须改用content://URI。
  • 通过FileProvider.getUriForFile()获取content://URI,并在Intent中设置FLAG_GRANT_READ_URI_PERMISSION权限。
  • file_paths.xml中配置的路径必须是应用私有目录的子集。如果你想分享外部存储中Android/data/目录下的文件,路径要配成external-path并指向Android/data/包名

这类配置问题,往往项目里用过一次就不会忘。但笔试考的是细节,比如FileProviderauthorities必须全局唯一,同一个手机里两个APP用同一个authorities会冲突,导致运行时崩溃。

6. 实操过程:我是怎么准备这批笔试的

6.1 我的复习时间分配

说实话,从收到笔试通知到正式考试,中间只有大约一周时间。我给自己定了一个复习计划,核心思路是用“高频考点+源码验证+手写Demo”三件套来闭环。

前两天集中过了一遍Java/Kotlin基础、集合类源码、泛型擦除、协程原理。中间三天死磕Android四大组件、Handler、Binder、View绘制流程、事件分发机制。最后两天刷题,主要是翻看近两年的Android大厂真题,尤其是小红书的风格。

我复习时有一个习惯:每个基础知识点,不只是看博客和文档,而是会打开Android Studio写一个最小Demo,验证说明中的结论。比如为了验证View.post为什么可以在onCreate中拿到宽高,我会真的打印一下onCreatepost回调中的width/height。这种实验会让你对机制有肌肉记忆,笔试时靠直觉也能选对。

6.2 笔试现场的时间控制技巧

笔试题量不小,编程题尤其容易卡住。我给自己定的时间线是:选择题部分最多60分钟,剩下60分钟全部给编程题。实际操作时,选择题大约用了50分钟,第一道编程题写了25分钟,第二道编程题写了30分钟,最后剩下15分钟检查。

检查时重点看几个地方:一是变量的初始值判断,二是ArrayList/ArrayDeque的索引边界,三是判空逻辑是否完整。我当时为了省时间,几乎没有写单元测试,全靠肉眼推理,这种习惯其实不太好。如果你时间允许,建议在本地IDE或者白板上至少手工跑一遍简单的示例。

还有一个小技巧:如果编程题卡住了,先跳过,做下一道,不要在一棵树上吊死。笔试最终看的是总分,编程题也不是完全不拿分就没机会。我有一个同学第一道编程题完全没做出来,但选择题正确率很高,最后还是进入了面试轮。

7. 给后来人的几条实在建议

7.1 重点知识点和题目反思

准备这一类大厂Android岗笔试,我个人的体会是:基础决定下限,源码决定上限。

所谓的“基础”,包括Java/Kotlin语法、集合、线程、网络,这些必须非常熟练。所谓的“源码”,指的是你不光会用Handler、Binder、HashMap,还能从底层理解它们的设计思路。笔试的题目往往不会超纲,但会变着花样考你对源码的理解。

我整理了下面几个自己认为值得反复锤炼的知识点,分享出来:

  • Activity启动模式的代码级模拟,尤其是singleTasksingleInstance的栈操作。
  • Handler消息机制的完整链路:MessageQueueenqueueMessagenextLooper.loop取出消息后如何回调Handler.dispatchMessage
  • RecyclerView的回收复用机制:ViewHolder如何进入RecycledViewPoolCachedView,以及在嵌套滚动时缓存命中率的问题。
  • 协程的调度器与异常处理:Dispatchers.MainDispatchers.IO的切换,SupervisorJobJob的区别,CoroutineExceptionHandler的使用。
  • 内存泄漏的经典场景识别:非静态内部类、匿名Handler、单例持有Context、静态集合、未注销的BroadcastReceiver。

7.2 最后分享一个小技巧

笔试前我会把手机里高频热词相关的文章快速扫一遍,比如“android studio”“android framework”“android AMS”“android OpenOCD”这些,不是为了临时抱佛脚,而是看一下最近社区里大家都在聊什么。有些笔试题目确实会结合当下的行业热点,比如新的Android 14特性、动态图标主题、R8混淆规则、APEX模块机制。你未必能猜到原题,但至少能让自己对这些新名词有印象,不至于看到选项完全陌生。

另外,笔试前一晚不要刷题到太晚,保证睡眠。Android笔试题目量大且细,非常考验注意力。我那次做选择题时,有几道题差点因为粗心选错,回去检查时才改过来。清醒的头脑比什么都重要。

这批笔试让我最大的收获,是意识到自己平时开发中对底层原理的依赖太少了。如果你也准备投Android岗,我真心建议在刷题之余,把平时项目里的常用框架,比如OkHttp、Retrofit、Glide、EventBus的源码挑一两个核心类,从头到尾读一遍。笔试考的不只是那几道题,而是你有没有持续追踪、深入理解技术系统的习惯。祝大家好运,也欢迎考完来交流你的复盘心得。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询