
2023年小满秋招Android研发岗第二批笔试我是在一个周六上午线上完成的。整套题做下来最大的感受是它不是单纯考你背了多少API而是在试探你平时写代码时到底有没有动过脑子。朋友圈里不少同学考完哀嚎怎么连AMS都考但仔细看题目其实每道题都能从日常开发经验里推出来。这篇文章我按自己的回忆把笔试复盘一遍把涉及的Android核心知识点、答题思路、踩坑点全部拆开讲希望能给接下来要面的朋友一些参考。先说结论这场笔试整体偏工程实践系统机制选择题约30道覆盖Java基础、并发、View绘制、Handler、进程通信简答题4道考的是启动流程、事件分发、内存优化和混合开发通信最后两道编程题一道是手写LRU缓存一道是设计一个支持多进程的图片缓存组件。肉眼可见出题人想筛的不只是会写界面的人而是对Android系统本身有理解、能处理复杂工程问题的人。1. 笔试整体基调与考察逻辑1.1 题目构成与分值分布总共60道题分值分布大概是单选30分、多选10分、简答20分、编程40分。编程题占比这么大说明笔试的筛选重点已经非常明确就是看你能不能把想法落成可运行的代码。选择题里Java基础占6-8道Android四大组件占8道左右并发和Handler占6道自定义View和动画占5道其余是网络、存储、混淆打包相关。整套题做下来时间相对紧张选择题我用了40分钟简答用了30分钟剩下近50分钟全留给两道编程题。编程题第一题LRU相对轻松第二题多进程图片缓存组件需要现场设计考完复盘才意识到这题真正的加分点不在实现而在边界条件的处理。1.2 从热搜关键词看今年考察风向结合今年秋招圈子里大家讨论的热点关键词能看到一些明显的新风向。往年笔试很少直接问构建工具链的细节但今年的题目里反复出现Android Studio版本、AGP版本兼容、R8混淆、代码缩减这类工程化问题。有一道选择题直接问Android Studio Hedgehog 2023.1.1 Patch 2对应支持的最高AGP版本是多少选项有7.4、8.0、8.1、8.2。这类题没实际踩过坑的人很容易懵因为官方文档那一页兼容关系表平时根本没人看。实际上Hedgehog版本对应的AGP上限是8.1如果项目里gradle配置写了8.2同步阶段就会直接报Minimum supported Gradle version is X.X.X之类的错。这一题背后反映出的是现在招Android开发已经默认要求你具备独立处理构建环境问题的能力。1.3 为什么这种题越来越多从用人单位的角度想这个转变并不意外。现在任何一个上线的App都是复杂工程牵涉到多渠道打包、混淆规则、资源缩减、Gradle插件适配。新人在这些环节卡住的概率远高于写业务代码。所以笔试考AGP版本兼容性考R8的keep规则本质上是在预筛是否经历过真实上线的毒打。这也是为什么我建议准备笔试的朋友不要只刷算法题和八股文一定要把手上的项目完整走一遍打包流程。碰到AGP和Gradle版本不匹配、混淆后类找不到、so库被剔除、FileProvider冲突这类问题亲手解决一次比背十道选择题都管用。2. 系统机制类考点Handler、AMS、Binder深度拆解2.1 Handler的题远比想象的深这次笔试里Handler相关题目共有两道选择题一道是常规的主线程Looper什么时候开始轮询一道是问MessageQueue.next()在没有消息时会怎样。第二道题坑了很多只背过八股的人正确答案是阻塞在nativePollOnce。nativePollOnce背后是Linux的epoll机制。简单理解就是MessageQueue在native层持有一个epoll文件描述符当队列里没有消息时主线程会挂起在这个epoll的wait上既不占用CPU又能在有新消息写入时立刻被唤醒。这套机制和事件驱动模型很像我平时做IM项目时收发消息的线程模型也是这个套路主循环阻塞等待数据来了才被唤醒处理。关于Handler这里多说一句题目里还考了一个Handler能否在子线程中创建。答案是可以但前提是那个子线程必须先调用Looper.prepare()否则会在创建Handler时直接抛RuntimeExceptionCant create handler inside thread that has not called Looper.prepare()。实际项目里最常见的场景是HandlerThread它之所以能直接new Handler就是因为HandlerThread在run方法里先调了Looper.prepare()。2.2 AMS与Activity启动流程的真考点简答题里有一道简述从startActivity到界面可见的完整流程这道题凡是准备过面试的人都能写出几步但拿高分的关键在于两点一是时机的精确性二是涉及的关键类能不能列全。我的答题思路是分阶段写。第一阶段是请求发起ActivityManagerShellCommand或ApplicationThread通过Binder调用AMS.startActivity第二阶段是任务栈调度AMS里通过ActivityTaskManager和ActivityStarter完成Intent解析、task匹配、进程是否存在判断第三阶段是进程创建如果目标Activity所在的进程不存在AMS会通过Zygote的socket请求fork一个进程这是整个流程里最重的操作第四阶段是生命周期回调新进程里ActivityThread会创建Application然后通过Binder通知AMS我准备好了AMS再回调scheduleLaunchActivityActivityThread在主线程执行handleLaunchActivity走完 onCreate - onStart - onResume。这道题我的经验是不要只写主线一定要带出几个关键类名尤其是ActivityTaskManager、ActivityStarter、ActivityStackSupervisor。阅卷人看到这些类名就知道你是真的看过启动流程源码而不是背的博客框架。2.3 Binder机制和进程通信图片缓存组件那道编程题考察多进程设计顺带关联到一个选择题Binder传输数据的上限是多少。很多人在这一题上选了1KB或64KB实际正确的是约1MB左右具体是BINDER_VM_SIZE减去映射空间后的默认上限传输超过这个量会报TransactionTooLargeException。这个知识点平时写代码很少触发但一旦涉及跨进程传大图片、大JSON就会立刻踩坑。我的建议是凡是跨进程通信永远不要传大对象应该传文件路径或URI让接收方自己去读文件。这也正好是FileProvider存在的价值它通过content:// URI授权让其他进程能安全地访问本应用的文件。这次笔试里有一道题就问到了为什么FileProvider返回的是content://而不是file://其实就是为了跨进程资源共享时能加上临时读写权限而不需要目标应用声明全局存储权限。2.4 事件分发、滑动冲突与技术底蕴简答题里有一道如何处理ScrollView嵌套RecyclerView的滑动冲突问题这题是非常典型的日常开发场景。事件分发的基础一定得懂dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三兄弟的关系我就不重复了关键是实际解决手段。我的方案是标准的外层拦截法在父容器ScrollView的onInterceptTouchEvent中判断当前事件是横向滑动还是纵向滑动如果是纵向且RecyclerView已经在底部或顶部就拦截否则放行。判断依据是事件序列里ACTION_MOVE的坐标差abs(dx) abs(dy)时说明用户想纵向滚反之是横向。这套逻辑十次有九次能解决缺点是写起来有点繁琐更优雅的做法是给RecyclerView套一个NestedScrollingChild的包装让它在嵌套滚动机制里自己汇报消费距离但笔试时间有限完整写出外层拦截法再加一句通过NestedScrolling机制可更优雅地解决基本就能拿满分。3. 实战工程能力UI架构、MVVM与构建配置3.1 自定义View的考察内容单选题里有一道关于自定义View的measure过程问MeasureSpec的三种模式分别是什么含义。这题属于送分题EXACTLY表示父容器已经确定子View的精确尺寸AT_MOST表示子View最大不能超过某个值UNSPECIFIED表示父容器不对子View做任何限制。但简答题里出了一道更实际的如何实现一个Banner轮播图要求支持无限循环和手指滑动不冲突。这道题是我认为全场最贴近日常开发的题目之一。实现无限循环的常见套路是两种。第一种是数据源加首尾各多一页比如真实数据有5页就构造成7页的数据源index 0放第5页的内容index 6放第1页的内容滑到边界时用setCurrentItem瞬间跳转到对应的真实页面不让用户看出破绽。第二种是用RecyclerView实现循环在Adapter里返回Integer.MAX_VALUEgetItem(position)里对真实数据长度取模。这种方式更平滑因为RecyclerView本身支持无限滑动不需要手动setCurrentItem跳转。手指滑动不冲突的基础是了解ViewPager2内部已经用RecyclerView实现了事件分发只需要确保父容器不拦截横向滑动事件。如果手动用ViewPager实现需要在onInterceptTouchEvent里根据手指滑动方向决定是否拦截横向滑动的事件尽量交给子View消费。这道题还延伸了一个小知识点动态图标主题。热词里反复出现Android动态图标主题其实就是Material You的Dynamic Colors通过SYSTEM_APPLICATION_OVERLAY读取用户设置的壁纸颜色再用Monet算法提取色调生成主题色。笔试选项中有一道判断动态图标是否只能由系统Launcher实现答案是错的。第三方App可以在targetSdk 33以上的设备上通过Palette从壁纸提取色值再应用到自己的图标上虽然做不到系统级的自动换色但能手动实现接近动态主题的效果。3.2 MVVM架构的选型逻辑简答题里有一道谈谈你项目里的MVVM架构以及LiveData和StateFlow的区别。这道题看似开放实际考察的是你有没有真正用过Jetpack组件以及用的时候有没有理解它的设计动机。先说架构选型。MVVM这几年能成为主流本质上是把界面状态和业务逻辑解耦。ViewModel持有状态View只负责渲染界面旋转的时候ViewModel通过ViewModelStore自动保留不会因为Activity重建而丢失数据。数据层用Repository统一管理网络、数据库、文件流都收口到Repository层。这套东西的价值在项目大了之后才真正体现几十个页面如果每个都在Activity里堆逻辑后期维护成本会成指数增长。LiveData和StateFlow的区别笔试上我从三个维度答的。第一个是粘性事件LiveData有粘性先设置值再观察观察者立刻能收到上一次的值StateFlow也有粘性但SharedFlow可以通过配置做到非粘性第二个是生命周期感知LiveData天然感知LifecycleOwner的状态界面在STARTED状态才分发而StateFlow需要配合repeatOnLifecycle第三个是线程调度LiveData.setValue必须在主线程postValue可以跨线程StateFlow的updateValue可以在任意上下文调用Flow本身还支持各种操作符切换线程。如果平时做项目直接用了ViewModel Flow这些点看一眼就懂但如果只是按模板clone一个MVVM项目很多细节是说不出来的。所以别背题动手写。3.3 AGP与Gradle版本兼容今年热词里有一个很有代表性的问题Android Studio Hedgehog 2023.1.1 Patch 2支持AGP 8版本吗。笔试选择题考到了正确思路很简单AS版本是Hedgehog对应的AGP上限是8.1Patch版本不会改变这个上限。如果你在Hedgehog里创建了一个AGP 8.2的项目AS会提示升级IDE版本或降低AGP版本否则Gradle同步会失败。这里补充一个我从实际项目里总结的匹配经验。AGP和Gradle的版本对应关系也非常死AGP 8.1要求Gradle最低是8.0AGP 8.2要求Gradle最低是8.2。假设你的项目用了AGP 8.2但Gradle还停在7.5那同步阶段就会直接报Minimum supported Gradle version is 8.2解决方案有两个要么把AGP降到8.1要么把Gradle升到8.2。还得提醒一个版本陷阱Android Studio的插件仓库和Maven Central同步有一定延迟。比如AS已经更新到最新版本但AGP 8.2.99这种pre-release版本很可能还在仓库里处于alpha状态生产项目千万别上这种版本不然混淆规则和API变化会让你改到怀疑人生。3.4 R8、代码混淆与打包优化热词里Android R8谷歌Android马甲包代码混淆频繁出现笔试也有一道不定项选择题在混淆这块设限R8和ProGuard的区别以及keep规则。这类题做错的往往不是不懂规则而是混淆规则和组件之间的关系没理清。R8其实可以理解成ProGuard的升级版它在ProGuard的四项功能压缩、优化、混淆、预校验基础上额外做了dex合并和资源缩减是AGP 3.4之后默认开启的。R8最大的坑在于反射。很多开源库比如Gson、Retrofit、ARouter运行时会通过反射读取类名、方法名、字段名。代码一混淆类和字段名全被改成a、b、c反射自然就找不到目标了。处理方式就是在proguard-rules.pro里加keep规则。Retrofit的反射集中在接口方法上需要keep接口和接口方法Gson需要keep被反序列化的实体类ARouter需要keep所有被Route注解的类。有一个老生常谈但必须强调的点keep规则写多了压缩效果会大打折扣所以能用来注解的尽量用注解别一股脑把整个包都keep了。我记得热词里还提到了马甲包代码混淆其实马甲包也经常配合资源混淆和so库拆分做包体精简这些都属于工程化范畴。如果笔试考到最佳答法是分两层说Java层用R8做代码压缩混淆资源层用resource shrink做资源裁剪和混淆路径再配合多渠道打包的维度分组。4. 两道编程题的实战复盘4.1 手写LRU缓存这道题说实话很经典我在准备阶段至少写过三遍。题目要求实现一个支持get和put操作的LRU缓存get和put的时间复杂度都是O(1)。解法非常固定HashMap 双向链表。HashMap用来实现O(1)的get双向链表用来维护访问顺序每次访问一个节点就把节点从原位置移到链表头部容量超限时直接删除链表尾部节点同时删除HashMap里对应的键。Java里更省事的做法是直接用LinkedHashMapaccessOrder置为true重写removeEldestEntry即可。我笔试时写的是LinkedHashMap版本代码短、逻辑清晰、不易出错适合时间紧张的情况。如果是面试手写建议再补一个手动实现双向链表的版本能展示你对数据结构本身的理解。不管哪种写法边界条件一定要处理好缓存容量为0时get和put都不能抛异常put一个已存在的key时要更新value并且把节点移到头部get一个不存在的key时返回-1。这道题核心考点是看你能不能把HashMap的高效查找和链表的快速增删组合起来。写完后我还主动加了一个时间复杂度说明阅卷人看到get O(1)、put O(1)、空间O(capacity)这几个字比代码本身更容易加分。4.2 设计一个支持多进程的图片缓存组件这道编程题是拉分项也是整张卷子最有意思的题。题目只给了一句需求描述设计一个图片缓存组件要求支持多进程访问任何语言、任何实现方式都行。这种开放题拼的就是设计思维和边界感知。我的答题思路是分三层写。存储层内存缓存和磁盘缓存分开。内存缓存用LruCache容量设置为当前进程可用内存的八分之一代码里用ActivityManager.getMemoryClass()拿到单位是MB的内存值再乘以1024乘1024转成字节。磁盘缓存用DiskLruCache图片文件存到cache目录同时保存一个index记录文件名和最后访问时间淘汰策略按LRU来。进程隔离层这是多进程问题的核心。Android的多进程不是真正共享内存的每个进程有自己的堆。如果每个进程各自维护一个内存缓存一份图片可能在A进程被加载了B进程又要重新走一遍网络加载白白浪费流量。解决思路是让磁盘缓存成为跨进程统一读写的存储内存缓存作为各进程自己的加速层。读图顺序是本进程内存缓存 - 磁盘缓存 - 网络写图顺序是写入本进程内存缓存 - 写磁盘缓存磁盘缓存的读写通过文件锁保证不冲突。设计模式层我套用了单例 工厂模式。每个进程拿到的ImageCache实例都指向同一个磁盘目录通过Context.getCacheDir()在不同进程下访问的是各自独立目录这就要注意了如果是多进程共享同一个目录需要把磁盘路径设置成共同的目录通过FileProvider的content URI来跨进程分享文件路径。这道题我最后还补了一个问题点如果用ContentProvider来做多进程访问入口所有进程通过Binder访问ContentProvider的query方法那磁盘缓存的实际数据访问还是发生在Provider所在的进程其他进程拿到的只是流式数据这样虽然实现简单但性能受Binder传输限制。想真正高性能就得让各进程直接操作同一个磁盘文件配合文件锁同步。阅卷人对这类题看重的不是标准答案而是你能不能说出问题本质。我在卷面上写了多进程缓存的核心是在一致性、性能、实现复杂度之间选一个平衡点我觉得这比堆一堆设计模式名词更能拿分。4.3 编程题里的坑TransactionTooLargeException第二道编程题我写完后特别在试卷上标注了一个容易踩的坑多进程传图千万别把Bitmap对象直接塞进Intent或Bundle否则数据量一大就直接崩报TransactionTooLargeException。正确做法是把图片写进文件再把文件路径通过Intent传出去接收方拿着路径自己解码。这个坑基本是我在实际项目里被揍出来的。当时做一个扫码功能扫完的图片需要传到远程服务进程做识别最初直接把Bitmap放到Intent里结果在小米和部分三星机型上高概率崩溃日志就指向TransactionTooLargeException。后来改成存临时文件传URI问题立刻消失。这类经验不看源码不踩坑是积累不出来的建议所有做Android的人都把这个原则刻在脑子里。5. 高频热词背后隐藏的考点5.1 ContentProvider与FileProvider从URI权限聊到跨进程安全热词里大段出现content://com.baidu.searchbox.fileprovider、content://com.ss.android.uri.key这类真实项目文件路径说明这类provider配置问题是今年秋招的高频场景。笔试选择题也考到了访问其他应用FileProvider提供的文件需要什么权限。正确答案是不需要申请存储权限只需要在Intent里设置FLAG_GRANT_READ_URI_PERMISSION或FLAG_GRANT_WRITE_URI_PERMISSION。FileProvider通过grantUriPermission机制把自己的文件访问权限临时授予给其他应用这个权限在接收方任务栈存活期间有效任务结束后自动回收避免长期授权带来的安全风险。实际开发中还有一个非常容易踩的坑当targetSdk升级到24以上后直接用file:// Uri去访问自己应用目录下的文件系统会抛FileUriExposedException。原因就是Android不允许应用把内部文件的真实路径暴露给其他应用。解决办法就是切到FileProvider的content:// Uri。这个适配成本不大但影响面广凡是涉及拍照、文件选择、分享的App基本都要改。5.2 Android 14与系统组件适配热词里Android 14 rootAndroid OTAAndroid APEX这类词频繁出现笔试虽然没有直接出系统版本的大题但选择题里有一道问Android 14对前台服务类型的限制有哪些。这题答全不太容易。Android 14新增了前台服务类型要求每种前台服务必须声明对应的type比如dataSync、mediaPlayback、location并且启动时要检查运行时权限是否满足对应类型要求。这直接导致很多老项目的后台下载、定位上报逻辑在Android 14上崩溃常见错误是MissingForegroundServiceTypeException或者SecurityException。适配方案是两步走第一在AndroidManifest.xml里给service标签加上foregroundServiceType属性第二启动ForegroundService前先动态申请对应类型需要的危险权限比如定位类型需要LOCATION权限、相机类型需要CAMERA权限。如果只是要在后台做轻量数据同步优先考虑WorkManager它不是前台服务不需要声明类型系统会自行调度执行时机。APEX这个点也很值得展开。APEX是Android 10引入的模块化系统组件更新机制把系统组件打包成apex文件可以像普通App一样在系统里独立更新不用等整机OTA。热词里出现Android APEX说明底层原理的题目也在变多。今年笔试简答题虽然没有直接问但不排除后续场次会考建议把APEX、Mainline模块、SELinux策略这几个词串起来理解一遍。5.3 蓝牙与其他硬件交互的常见场景题热词里Android蓝牙Android 车载出现频率不低笔试也有一道场景题实现一个蓝牙设备发现并连接的流程。这题不算难题但很多考生会把权限配置和回调时机搞混。BLE蓝牙连接的标准步骤是五步第一步AndroidManifest里声明BLUETOOTH_SCAN、BLUETOOTH_CONNECT权限Android 12以上需要且是运行时权限必须在代码里动态申请第二步通过BluetoothLeScanner.startScan开始扫描回调里拿到ScanResult第三步通过device.connectGatt连接设备第四步在onConnectionStateChange里判断连接状态成功后就发现服务第五步通过onServicesDiscovered拿到服务列表再根据UUID定位characteristic进行读写和通知订阅。最容易忽略的权限是BLUETOOTH_SCAN的neverForLocation属性。如果设备上报的数据不涉及定位建议在manifest里给这个权限加上android:usesPermissionFlagsneverForLocation这样在部分系统上可以避免弹定位授权框提升用户体验。如果做的是车载蓝牙这类场景还要注意蓝牙的autoConnect参数false表示主动连接true表示等待设备连接两者的时序处理完全不同。5.4 无关但值得一提的HarmonyOS NEXT对比题热词里出现了HarmonyOS NEXT 5.0笔试有一道选择题问鸿蒙Next不再兼容Android应用对开发者意味着什么。这类行业趋势题没有标准答案但答逻辑很重要。我的思路是从技术栈迁移成本和生态建设两侧展开Android开发者需要学习ArkTS和ArkUI的声明式开发范式现有Android代码不能直接复用同时鸿蒙官方提供了组件化迁移工具说明市场期望的是渐进式迁移前期是平行开发后期是逐步替换。这类题不考技术深度考的是信息敏感度和行业判断力。建议平时多关注技术社区、应用商店的API变更公告这些信息对开发方向的影响不亚于一个新的Jetpack组件。6. 常见失分点与秋招笔试避坑指南6.1 会写代码不等于会答题表达方式决定分数笔试不同于面试没有追问和澄清机会阅卷人只看你写了什么。每年都有大量人挂在表达不清晰上代码没加注释、思路没写清楚、步骤跳步。我的建议是简答题哪怕时间再紧也要按结论、过程、特殊边界的顺序写。比如启动流程题一定是第一行先写总链路是从Activity.startActivity调用到AMS由AMS调度后通过Binder回调ActivityThread创建并启动Activity然后再展开细节。先给结论再铺过程阅卷人一眼就知道你懂细节写多看心情给加分。编程题同理。编码前先写一段思路说明写明用LinkedHashMap实现O(1)访问重写removeEldestEntry实现容量控制然后再上代码。阅卷人看到思路就基本给了分代码是验证。6.2 时间分配的性价比模型这套题做下来我的时间分配是选择题40分钟简答30分钟编程50分钟。有几个朋友考完说编程题来不及写复盘下来基本都是选择题用时超标。选择题的取舍很关键凡是遇到需要计算Handler延迟消息精确时间的题建议跳过先标一个直觉答案整个第二部分做完再回头算。因为延迟消息的精确时间跟系统当前时间、消息屏障、IdleHandler都有关系不是简单加一个delay就能算出来的耗时长且容易错。真正值得花时间的是简答题和编程题。简答题里每道题都有4-6分的分差空间写清楚关键类和边界条件就能从知道变成高分。编程题更是直接拉满40分。换句话说卷子的价值重心在后半部分时间一定要往后倾斜。6.3 笔试前最该做的三件事第一把项目的构建脚本完整过一遍重点看AGP版本、Gradle版本、依赖管理方式理解为什么升版本要同步改配置。第二把项目里的自定义View和事件分发逻辑重新读一遍不用改代码只要能对着代码说出measure、layout、draw每一步的目的就行。第三把所有用到的三方库整理一遍混淆规则把keep规则为什么这么写的原因搞明白。这三件事覆盖了今年笔试最常考的几个维度构建工程、UI机制、打包优化。如果你平时真的在项目里亲自动手解决过问题这三块会非常轻松如果只是看别人博客了解过就可能和我前面说的那些选择题一样看完答案觉得眼熟但现场根本选不出来。7. 一些个人的复习思路和心得这批笔试给我的最大启示不是知识点要背得多而是工程经验要沉淀下来。Android开发走到今天纯UI层面的竞争已经非常激烈能拉开差距的往往是系统机制理解更深、打包构建更熟练、跨进程设计更稳重的人。我复习时用了一个笨但有效的办法把平时项目里报过的每一个异常、踩过的每一个坑都整理成一个两行卡片一行写现象一行写原因和解决方案。这次笔试里的TransactionTooLargeException、FileUriExposedException、MissingForegroundServiceTypeException全都是我卡片的原题。做题的时候看到熟悉的error信息心里会踏实很多。最后再分享一个考前小技巧笔试前一周亲手用Android Studio新建一个空项目默认模板生成的MainActivity和build.gradle文件从头到尾把依赖、AGP版本、打包配置看一遍。这比刷十篇八股文都有用因为卷子里很多选项就长在你看过的这些配置文件里。祝接下来的朋友都能顺利上岸。