1. 笔试前我做的岗位画像与考点预判1.1 第二批笔试和第一批有什么不同2023年秋招有一个很明显的信号不少公司把笔试分批来做并不是为了多筛人而是为了“换一批题目降低泄露影响”。小满秋招的Android研发岗第二批笔试和第一批对比下来能明显感觉到命题组在刻意调整难度曲线。第一批偏重Java基础、四大组件和常规算法第二批则明显加大了Android Framework、性能优化和工程化题目的比例。举个例子第一批出现的是“Handler导致内存泄漏的解决方案”这种常识题第二批直接变成了“在Looper.loop()中插入一套自定义消息拦截机制给出实现思路”这要求你不仅要懂机制的结论还要能拆开内核看原理。所以我的第一条经验是参加第二批笔试前千万不要把第一批的真题当“幸存者样本”更不要去背题。小满的笔试系统是动态题库同一批考生拿到的题也可能不一样但考察方向会保持一致基础扎实、能讲清为什么、能动手画架构、能处理边界条件。第二批的题目往往会在第一批考查过的高频考点上做“一层包装”把八股文变成场景题把单一知识点变成跨模块的组合题。1.2 “小满”式出题风格基础不背题场景见真章我在笔试前把小满历年秋招的技术分享和招聘JD反复看了几遍总结出他们的出题风格不直接问“Service有几种启动方式”而是问“App要在后台播放音乐同时需要执行一个10分钟的任务你会怎么设计”不直接问“Bitmap怎么压缩”而是问“长列表滑动时内存飙升你如何用Glide和复用机制定位并解决”这种问法就是典型的“小满式”出题每个问题背后都藏着一个真实业务场景。第二批笔试的题干普遍偏长信息密度很高。有些题会故意给你一大段伪代码或者日志要求你分析问题。我印象最深的一道题是给了一段包含多个关键字的事务日志让你判断哪些操作在主线程执行哪些在Binder线程池并要求画出线程切换的时间线。这种题完全靠死记硬背是答不完整的你需要真正理解Handler、Binder、AsyncTask、协程这些机制之间的协作关系。我的建议是备考时不要只看帖子最好直接把系统源码里的Handler、Looper、MessageQueue、ActivityThread等关键类下载下来读一遍读完后你会发现笔试里的场景题其实就是在考你“源码级”的熟悉度。2. 从热搜词反推出的出题风向2.1 AI热词背后的工具链考察AGP版本、Android Studio与构建体系2023年笔试前我搜过一波“Android热词”发现“android studio hedgehog 2023.1.1 patch 2支持agp8版本吗”“android studio插件仓库”“android r8”这类工具链词条热度很高。不要小看这些“非考点”词汇它们恰恰暴露了今年的出题风向笔试不只看算法和组件还会考工程化能力的底座。比如“AGP 8.x升级”就是一个高频场景题。题目可能会问“老项目从AGP 7.0升级到8.0构建报错说buildConfig被默认关闭该怎么处理”或者“把AAR上传到Maven中央仓库时用Gradle 8怎么配置maven-publish插件”这些题目考察的不只是你会不会配而是你是否理解构建工具迭代背后的行为变化。AGP 8.0默认禁用了BuildConfig这是因为Google希望开发者减少对BuildConfig字段的依赖改用gradle.properties或运行时配置。我在笔试中遇到的是一道改错题题目给了build.gradle片段要求找出为什么生成的BuildConfig文件不包含自定义字段并给出修复方案。答这道题的关键点是在AGP 8.0里需要显式设置buildFeatures { buildConfig true }同时还要注意buildConfigField需要放在android.defaultConfig或productFlavors内部。再比如“R8”这种词听起来像混淆工具实际上它已经变成了官方默认的压缩器、优化器和混淆器和ProGuard是两套实现。笔试常考的是“R8和ProGuard的区别”“为什么R8默认开启isMinifyEnabled true后某些反射调用仍然可用”“怎么通过keep规则保留Gson的泛型信息”。这些题目背后考察的是你对字节码处理流程的理解。备考时我强烈建议自己动手写一个小项目开启R8后在release包里用retrace工具把堆栈还原观察混淆前后类名和方法名的变化。这个实操做完笔试题里考察混淆规则的题目基本就送分了。2.2 系统与底层能力AMS、OTA、APEX与蓝牙调试另一组热搜词是“android ams”“android ota”“android apex”“android openocd”“android蓝牙”“android 14 root”。如果你只是面向应用开发的候选人可能会觉得这些词离自己很远但2023年秋招的Android研发岗明显开始分层一部分岗位偏“应用层架构”一部分岗位偏“系统定制和性能优化”。小满的第二批笔试恰好覆盖了两层系统类的题目占比不低。“AMS”这个词我建议大家不要只背“ActivityManagerService是系统进程中的核心服务”而是要把它的调用链理清楚startActivity经过Instrumentation、ActivityManagerProxy、AMS、ActivityStackSupervisor、ActivityTaskManagerService最后回到应用进程的ApplicationThread再由ActivityThread执行启动。笔试中有一道题是“简述一次startActivity的跨进程调用过程并说明涉及哪些Binder对象”。这类题目是区分度很高的题因为只看博客的人能说出AMS、ActivityThread、Binder但不一定能画对方向应用进程请求AMS是单向的Binder调用AMS回调应用进程是通过ApplicationThread这个Binder对象完成的。你得理解“同一个Binder对象可以在不同进程中扮演不同角色”这件事。“OTA”和“APEX”看起来是系统升级相关其实笔试可能考的是“Android系统的模块化更新机制”。APEX是Android 10之后引入的用来代替部分系统组件的传统OTA更新方式比如MediaProvider、SDK extensions都能通过APEX独立升级。笔试如果考到一般不会让你写OTa脚本而是问“APEX与APK的差异是什么”“为什么系统应用需要APEX这种格式”。你需要答出APEX文件内部包含一个只读的ext4或dm-verity镜像挂载到一个动态分区系统可以在运行时加载新模块相比整个系统镜像的OTA它可以让核心组件在旧设备上获得新功能。这类题目适合有ROM刷机经验或阅读过AOSP文档的候选人如果你没有相关经验至少要理解“分区”“镜像”“dm-verity”这些基础概念。“蓝牙调试”和“openocd”则是嵌入式/硬件方向的延伸。小满第二批笔试有一道题是“在Android设备上通过I2C工具读取传感器寄存器写出基本调试流程”这就是典型的智能硬件研发岗题目。如果你投的是“Android系统工程师”这类题出现概率不低。我当时虽然主要投应用岗但仍然准备了一份“Android设备调试常用命令清单”包括adb shell ls /dev/i2c-*、echo -n 0x01 /sys/bus/i2c/devices/i2c-0/...这类用法防止笔试出现超纲题。事实证明这种宽泛的备题方式让我在面对一道“I2C工具在Android上使用”的开放性题目时至少能写出思路和命令关键点。2.3 UI与架构演进协调布局、动态图标、MVVM与协程热搜词里有一大串UI相关词“android中协调布局banner”“android动态图标主题”“android settings布局”“android透明度对照表”“android studio mvvm代码示例”。这些词背后反映的是2023年Android开发的“UI焦点”已经从单纯的XML布局转向复杂交互、主题化、可扩展架构。“协调布局CoordinatorLayout Banner”这种组合题笔试一般不会让你手写完整代码而是给一个滑动嵌套的场景比如“一个带有自动轮播Banner的页面下拉时Banner要缩放并最终折叠成一个标题栏请问你怎么用CoordinatorLayout实现”关键点在于理解Behavior机制你需要自定义一个HeaderBehavior在onDependentViewChanged或onNestedPreScroll里处理TranslationY和ScaleX/ScaleY的变化。单独写出CoordinatorLayout是加分项但更核心的是要讲明白“协调布局为什么会把事件分发交给Behavior”。我在备考时专门看了一下CoordinatorLayout源码发现它本质是一个ViewGroup通过NestedScrollingParent接口实现了对子View嵌套滚动的统一调度。笔试中如果你能提到“Behavior是一种策略模式的落地应用”阅卷人自然会觉得你是有工程素养的。“动态图标主题”则对应了Material Design的Theming能力。笔试题目很可能问“如何让App支持跟随系统开启深色模式同时让桌面图标也动态变化”前者对应DayNight主题和AppCompatDelegate.setDefaultNightMode()后者则需要借助Activity-alias或IconDrawable实现动态替换。这不只是一个UI问题还涉及资源配置你需要把values-night下的colors、drawables都准备好并且注意ANDROID_DAY_NIGHT模式切换时Activity的重建时机。这类题是在考察你“会不会把设计规范落到代码上”。MVVM和协程就更不用说了。小满第二批笔试有一道简答题“MVVM和MVC的根本区别是什么在Android中如何用协程优化MVVM的异步任务”如果只是回答“LiveData替换接口回调”就会显得很单薄。更好的答法是MVVM的核心是ViewModel与View之间的单向数据流View观察ViewModel暴露的LiveData或StateFlowViewModel不持有View引用协程的加入则让数据层可以用viewModelScope.launch把线程切换从回调地狱变成顺序挂起。笔试时我还补充了一段代码示意viewModelScope.launch { val result withContext(Dispatchers.IO) { repository.fetch() } mLiveData.value result }。这样既能展示编码能力也能让阅卷人一眼看到你对架构细节的把握。3. 笔试中容易踩坑的Android基础题详解3.1 Handler消息机制与内存泄漏不只是背概念Handler是Android笔试的“钉子户”第二批笔试也离不开。但它不再满足于“主线程不能做耗时操作”这种科普而是会往“机制设计”方向挖。我遇到的一道题是“Handler消息延迟10秒发送但Activity已经销毁你如何在onDestroy中避免消息回调导致的内存泄漏如果已经泄漏最直接的定位工具是什么”常规答法是“在onDestroy中移除消息使用静态内部类弱引用”。但笔试的坑在于题目给了一个很具体的场景消息已经在MessageQueue中排队但还没轮到执行此时removeCallbacksAndMessages(null)能清空该Handler的所有消息如果消息已经分发到dispatchMessage阶段那么移除就没有意义只能靠弱引用避免内存泄漏。我更想强调的一点是很多人在回答时会把“主线程”和“UI线程”混为一谈。其实所有Activity、View的调用都发生在main线程而Handler可以显式绑定到任意Looper。笔试如果问“能不能在子线程里创建一个Handler”正确答案是“能但需要先调用Looper.prepare()初始化该线程的Looper然后Looper.loop()否则会抛Cant create handler inside thread that has not called Looper.prepare()”。这道题看似简单但每年都有人因为背得不细而丢分。3.2 Activity启动模式与任务栈用场景题检验理解启动模式题同样不会直接考四种模式的名字而是给一个场景“美团外卖的店铺页A跳转到商品详情页BB可以通过返回键回到A但从B跳转到网页支付页C后支付完成需要直接回到A同时需清空B和C对任务栈的占用应该怎么设置Intent Flag”这种题目既考flag又考taskAffinity。标准答案可以是C跳转回A的时候给Intent添加FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP并在A的launchMode中配置singleTop。如果A是singleTask那在onNewIntent中更新A的数据就能把B和C全部出栈。不过注意一个坑如果A的启动模式是standardFLAG_ACTIVITY_CLEAR_TOP也会把A先销毁再重新创建这样会丢失之前的页面状态所以必须配合SINGLE_TOP才不会销毁重建。笔试时如果你能把“singleTop和standard在CLEAR_TOP下的行为差异”写出来这道题基本就是高分了。3.3 Binder与进程通信笔试常考的“半张图”Binder题是Android笔试的难点很多人在图上画不全。其实笔试并不要求你画出所有AIDL接口而是要求你理解三点Binder是Android的跨进程通信机制每个Binder对象在驱动层都有一个引用计数客户端拿到的“代理对象”不会真的执行方法而是把数据打包成Parcel后传给驱动。有一道题是这么问的“系统为什么用Binder而不是Linux传统的Pipe或System V消息队列”答案的核心是性能和安全Binder只需要一次拷贝Linux管道需要两次而且Binder可以为每个进程建立独立UID校验避免内核态被随意访问。你可以在笔试中写一段简单的AIDL服务端代码说明Stub.asInterface和Proxy的生成逻辑。面试官会更喜欢能看到“Binder代理与本地对象的关系”的选手而不是只会说“快”和“安全”的人。4. 代码与设计题第二批笔试的实战题型复盘4.1 手写LRU Cache从数据结构到线程安全第二批笔试的算法题并没有完全绕开LeetCode经典题而是加了一层“Android场景”。比如“请实现一个LRU缓存要求支持读和写且线程安全并分别用LinkedHashMap和自定义双向链表写出两种方案”。这题在很多公司都出现过但小满的第二批笔试把题目包装成“图片加载库的内存缓存如何避免OOM”。如果你选择LinkedHashMap可以直接写class LruCacheK, V { private final LinkedHashMapK, V map; private final int maxSize; private int size; public LruCache(int maxSize) { this.maxSize maxSize; this.map new LinkedHashMapK, V(0, 0.75f, true) { Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size maxSize; } }; } public synchronized V get(K key) { V value map.get(key); if (value ! null) { return value; } V created create(key); if (created ! null) { map.put(key, created); } return created; } public synchronized V put(K key, V value) { V previous map.put(key, value); // 计算size并trimToSize return previous; } }但笔试如果只给这段代码还不够。你需要解释accessOrder参数new LinkedHashMap(0, 0.75f, true)中的true就是开启访问顺序每次get会把节点移动到尾部这样removeEldestEntry判断的就是最久没被访问的头部节点。线程安全方面get和put都需要加Synchronized否则并发put时可能丢数据。我还建议用“自定义双向链表HashMap”的版本写一遍这样能体现你理解LRU的本质HashMap负责O(1)查找双向链表负责O(1)移动和删除。在笔试中我甚至画了一个简版的链表节点示意把prev、next、key、value四个字段一一列出来。这道题最终得了不错的分数因为答题时我不仅给代码还在注释里写明“这个节点移动为什么是O(1)因为我知道节点的内存引用不需要遍历链表”。4.2 自定义View测量与布局协调布局中的高度联动另一道代码题是“自定义一个可以跟随滚动伸缩高度的HeaderView要求不超过200dp不低于60dp在CoordinatorLayout中与RecyclerView联动”。这道题不一定要求你写完整实现但需要写出LayoutParams和onMeasure的关键代码。思路是这样在onMeasure中根据当前的scrollOffset计算目标高度使用MeasureSpec中传入的父容器约束来调整高度。需要重写onNestedPreScroll在RecyclerView向下滚动前先消耗一部分滚动距离用于压缩HeaderView的高度。核心代码示例override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { if (dy 0) { // 下拉准备恢复高度 val newHeight currentHeight -dy if (newHeight maxHeight) { setHeaderHeight(newHeight) consumed[1] -dy } else { setHeaderHeight(maxHeight) consumed[1] newHeight - maxHeight } } else if (dy 0) { // 上滑压缩高度 val newHeight currentHeight - dy if (newHeight minHeight) { setHeaderHeight(newHeight) consumed[1] dy } else { setHeaderHeight(minHeight) consumed[1] dy - (minHeight - newHeight) } } }这里有个坑很多人在onNestedPreScroll中直接改变LayoutParams.height但忘记在修改后调用requestLayout()。因为LayoutParams.height改变后父布局不会自动重新测量这个View。如果不requestLayoutView的高度会变但内容绘制可能还是旧的滑动时会看到头部区域出现空白。这个细节我在笔试题里专门标注了因为这是实际项目中很容易踩的坑。4.3 如何设计一个可混淆的SDKR8与ProGuard的实际应用小满第二批笔试里有一道综合性很强的设计题“请设计一个给第三方App接入的支付SDK要求对外API保持稳定同时内部实现能被R8混淆但不能影响对外方法调用。请写出核心混淆规则。”这道题把SDK设计、代码混淆、API稳定性三者结合在了一起。好的回答应该包含三块。第一块是“对外接口定义”把公共类都用public的接口暴露出来内部实现类使用internal或private修饰这样R8在优化时能把这些实现类的方法当成可混淆的候选。第二块是“混淆规则”你需要给SDK编写一个独立的consumer-rules.pro# 保留对外API入口 -keep public class com.xiaoman.pay.** { public *; } # 保留返回结果所需的字段序列化 -keepclassmembers class com.xiaoman.pay.model.** { public fields; } # 保留注解用于反射查找 -keepattributes *Annotation*第三块是“为什么这么写”。因为R8默认会移除所有未被调用的代码但如果第三方App通过反射调用SDK内部方法R8看不到这个调用链所以必须用keep规则强制保留。同时如果SDK使用了Gson解析支付结果你就得保留数据model的类名和字段名否则Gson反射创建对象时会把amount变成a导致解析失败。这道题很难但很能区分“只会调用SDK的人”和“写过SDK的人”。我在笔试中把混淆规则和原因写完后还主动补充了一个“SDK的版本号应该用BuildConfig还是用Constants类”的小问题。答案是如果你开启了BuildConfig混淆时记得保留BuildConfig类否则App端通过BuildConfig.VERSION_NAME获取不到你的SDK版本。这种细节虽然没有直接问但能展示你在真实工程中的思考深度。5. 笔试后的复盘清单与面试衔接5.1 如何用一张脑图整理笔试错题笔试结束后的24小时是记忆最清晰的时候。我会立刻把笔试中的每一道题记录下来标注“是否答出”“卡在哪一步”“正确答案的关键点是什么”。小满第二批笔试的题目我没有完全复现但复盘时我按五个模块整理了一份脑图Java基础、Android组件、Framework、工程化、算法与设计题。这张脑图的作用不只是“补漏”更重要的是发现自己的薄弱模块。我复盘后发现自己在“构建工具链”和“系统级调试”上答得很差于是花了两天时间专门把AGP 8.0的变更文档和adb常用命令重新过了一遍。这种“考后快速定点补强”比漫无目的地刷题高效很多。如果你手头还有笔试原题的关键词可以用笔记软件把它们串起来每道题旁边留一个链接指向官方文档或源码文件方便面试前快速复习。另外我还会统计“时间分配”问题。第二批笔试给的题量大概有20道选择题、4道简答题、2道编程题总时长90分钟。第一次做时我在一道“滑窗算法”题上耗了25分钟导致后面的设计题没时间写完整。复盘后我给自己定了一个硬性规则编程题如果15分钟没有清晰思路先写伪代码和关键算法框架立刻跳下一题。这个习惯在后续几场笔试里帮了我大忙。5.2 从笔试到面试把“会做”变成“会讲”笔试通过只是第一关面试时你会面对同样知识点的深度追问。我在面试小满的时候面试官就盯着我笔试里写的一道“Binder机制”题展开问我“如果客户端请求一个不存在的服务Binder驱动会上报什么错误客户端进程会收到什么异常”这个问题如果笔试时只背了图没有真正看过ServiceManager的源码很容易答出“无关服务”这种错误答案。正确方向是ServiceManager内部通过checkService查找服务如果找不到会返回空Binder引用客户端调用代理方法时会抛出DeadObjectException或NullPointerException这取决于你是否对Binder代理做了空判断。因此我建议笔试后把“会做”的题变成“会讲”就是针对每一道错题准备一个“为什么我之前没答出来”的口头解释再用画图的方式把原理说清楚。面试官往往不期望你背标准答案而是想看你有没有独立分析问题的能力。你可以用“我先讲结论再拆步骤”的方式先说最终方案再倒推设计的原因。比如面试官问“为什么用LiveData不用MutableLiveData暴露数据”你可以回答“对外暴露不可变数据防止外部直接修改内部逻辑统一由ViewModel控制”。这就是把笔试里“MVVM和协程”那道简答题的答案提炼成了口头表达。最后要说的是不要把笔试成绩当成终点。秋招是一场持久战笔试只是把你和岗位之间“距离”量化出来的工具。你每做一套题都能看到自己在Android知识体系上的短板这比单纯刷课刷论坛要有用得多。希望这份复盘能对准备Android研发岗笔试的人有点启发也祝你能在下一场笔试里稳扎稳打把自己会的东西完整地写到卷面上。