一提到“货拉拉2018秋招Android工程师笔试题卷三(B)”这个标题,很多人的第一反应是去搜题目原文、背答案,但作为经历过多个招聘季、也参加过不少笔试出题工作的老Android开发,我想先泼一盆冷水:这套题目本身的价值,远不如它背后暴露的考点逻辑值钱。2018年秋招是个分水岭,那时候移动互联网红利还在,Android岗位的笔试已经从单纯考API背诵转向了对原理理解、源码追踪和工程思维的全面考察。说白了,面试官想看到的不是一个会写if-else的人,而是一个遇到问题能顺着代码往下追、能说清楚“为什么”的人。
这篇文章我会结合Android面试题里最常出现的几个核心模块——组件、消息机制、并发、自定义View、性能优化,逐个拆解这套笔试题(以及同类题目)背后的考察重点和作答思路。无论你是正在准备校招的应届生,还是想跳槽的初中级开发,按这条线去准备,比盲目刷题有效得多。
1. 笔试题整体设计与考点分布
1.1 一道笔试题想筛选什么样的人
2018年是一个很有意思的时间点。那一年Kotlin已经宣布成为Android官方语言,Google对Android Studio的投入明显加大,组件化、热修复、插件化这些方案在各大厂已经落地成型。在这个背景下,笔试题目已经不满足于“四大组件是什么”“Activity生命周期有哪些”这种送分题了,而是开始往“组合拳”方向出:一个场景题里同时涉及生命周期、内存、异步和UI刷新,看你能否把零散知识点串成一条完整链路。
我记得当时很多同学拿到卷子第一反应是“这题我会”,落笔却发现每个选项都模棱两可。原因很简单,题目考察的不是记忆,而是辨析。比如同样的场景,Activity切到后台再切回来,onSaveInstanceState的调用时机和恢复时机分别是什么,onRestart和onStart到底谁先谁后,这些细节在开发中平时用不到,但面试官就是要靠它们过滤掉“只看过入门教程”的简历。
所以这套笔试题的本质,是通过有限的选择题和简答题,快速判断候选人的知识体系是否成网。知识点之间是孤立的还是联通的,决定了一个人能不能独立扛起模块开发。理解了这一点,你就知道备考的重点不是背题,而是建立知识之间的因果链。
1.2 高频考点分布与出题权重
结合我对2018年前后多家互联网公司Android笔试题的观察,再对照货拉拉这套卷三(B)的出题风格,高频考点大致可以分为四层。
第一层是基础组件与系统机制,占比最高,大概在40%左右。这一层覆盖Activity启动模式、Fragment生命周期、Service两种启动方式、BroadcastReceiver的注册方式、ContentProvider的底层原理。为什么会占这么高?因为这是Android开发的底盘,任何项目都绕不开。第二层是异步与消息机制,占比大概20%。Handler、Looper、MessageQueue三者之间的关系是必考项,延伸出去就是ThreadLocal的作用、IdleHandler的执行时机、同步屏障的原理。第三层是UI与自定义View,占比15%到20%。考察点集中在measure/layout/draw流程、事件分发机制、自定义属性定义,偶尔会结合invalidate和requestLayout的异同来出题。第四层是性能优化与内存管理,占比15%左右。包括ANR的触发条件、常见内存泄漏场景、Bitmap内存计算、布局层级优化这些点。
还有一个容易被忽略的隐藏考点:代码阅读能力。卷子里经常给一段真实代码,问你输出结果或者指出问题。这层考察的是你能不能读懂别人的代码,以及有没有代码洁癖。我后面会专门展开讲这部分。
2. 核心知识点精讲与高分作答逻辑
2.1 Activity启动模式:别只会背四种模式
Activity的四种启动模式——standard、singleTop、singleTask、singleInstance——几乎是Android笔试题的必考题,但大多数人的准备方式只是背下概念,这恰恰是丢分点。货拉拉这套卷子在启动模式上至少出了两道题,一道是考返回栈的变化,一道是结合Intent Flag出组合题。
先说standard模式。它是最普通的一种:每次启动Activity都会创建新的实例,放进启动它的那个任务栈里。注意,这里有个细节很多人不知道:standard模式的Activity在启动时会调用被启动者的onCreate,而不会复用已有实例,哪怕栈顶就是它自己。这就引出一个高频场景题:“如果从Activity A跳转到A,连续跳三次,按下返回键会发生什么?”答案是三次onCreate,三次压栈,按三次返回依次出栈。如果你答成“复用栈顶实例”,说明你对standard模式的理解是错的。
再说singleTop。它和standard的区别在于:如果栈顶已经存在该Activity的实例,则复用栈顶实例,并回调它的onNewIntent方法;如果不在栈顶,行为与standard一致。这里考试喜欢考一个陷阱场景:从A跳B(singleTop),再从B跳A,此时A在栈顶吗?不在,因为栈顶是B。所以即便A设置成singleTop,也不会复用,而是再创建一个新的A实例。很多人在这里栽跟头,就是因为只记住了“栈顶复用”这个结论,没有真正理解“栈顶”是动态变化的状态。
singleTask和singleInstance是重灾区。singleTask的核心是“栈内复用”:目标任务栈里如果已有该Activity的实例,就把它上面的所有Activity全部出栈,复用这个实例,并回调onNewIntent。这个特性因为会影响返回栈里的其他Activity,所以经常被用来做App主页,但使用时要非常小心,因为它会破坏用户预期的返回栈结构。singleInstance则更极端:它所在的Activity会独占一个任务栈,这个栈里只有它一个实例。这种模式极少用,一般只在系统来电界面、闹钟提醒这类需要全局唯一的场景里出现。笔试如果考singleInstance,几乎必问“此时按Home键再点击应用图标,会发生什么”,答案是会创建一个新的任务栈,并不一定回到之前的singleInstance页面。
结合Intent Flag出题时,核心要记住两个:FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_CLEAR_TOP。前者和singleTask的效果类似,但更灵活,它可以跨Task启动;后者则会把目标Activity之上的Activity全部清掉。两者结合使用时,效果就是singleTask的完整版。很多笔试答案里会把这两种Flag与launchMode混为一谈,这是最典型的混淆点。
高分作答思路不是背四种模式的特性,而是能讲清楚两个维度:实例是否复用、任务栈是否变化。只要把这两个维度想清楚,任何组合题都能迎刃而解。
2.2 Handler消息机制:从三个类讲到同步屏障
Handler机制是Android面试题里频率最高、也最能拉开分数差距的题目。2018年那会儿很多背题党能说出“Handler用来线程间通信、Looper负责消息循环、MessageQueue存储消息”这种话,但面试官紧接着问一句“Looper.loop()为什么不会阻塞主线程?”就懵了。
我把这套机制拆成一条完整的链路来讲。Handler发送消息时,会通过enqueueMessage把Message插入到MessageQueue中。MessageQueue的数据结构不是队列,而是一个按时间排序的单链表——按消息的触发时间when字段从小到大排列。这里有个容易误解的点:MessageQueue的“时间排序”不是按入队顺序,而是按延迟执行时间。如果你sendMessageDelayed发了一个延迟10秒的消息,它会被排到当前时刻附近的消息之后,而不是插到队尾。
主线程的Looper通过loop()方法不断循环:取出下一条消息,如果消息时间还没到,就调用nativePollOnce阻塞等待;如果时间到了,就分发消息给对应的Handler去处理。这个nativePollOnce是理解“为什么不阻塞主线程”的关键。它底层用到的是Linux的epoll机制,在等待期间主线程其实是休眠状态,不消耗CPU;一旦有消息到达或者时间到,epoll会立刻唤醒线程。这就像你在食堂排队打饭,如果前面的人还没到,你可以先去旁边长椅上坐着休息,轮到你的时候再起身,而不是站在窗口前干等。
这里还有一个绝大多数人没搞清的概念:ThreadLocal的作用。Looper是通过ThreadLocal来保证每个线程最多只有一个实例的。ThreadLocal的原理是每个Thread内部维护一个ThreadLocalMap,以ThreadLocal对象为key。所以你在主线程调用Looper.getMainLooper()拿到的是主线程的Looper,在子线程new Handler()则需要先调用Looper.prepare()创建该线程的Looper。很多人写子线程Handler时报错“Can't create handler inside thread that has not called Looper.prepare()”,就是没理解这条生态链。
2018年的笔试题里,同步屏障是区分度最高的点。同步屏障消息的本质是一个target为null的特殊Message,它的作用是让MessageQueue在处理消息时优先跳过所有同步消息,只执行异步消息。这个机制的典型应用场景是UI渲染:在需要立即刷新界面前插入一个同步屏障,使VSync消息可以优先被处理,等渲染完成后移除屏障。能主动把这个机制写在简答题里,哪怕答案不完整,面试官也会对你另眼相看。
除了原理本身,笔试还喜欢考内存泄漏问题。非静态内部类Handler会隐式持有外部Activity的引用,如果Activity已经销毁但消息队列里还有待处理的Message,就会导致Activity无法被GC回收。标准解法是使用静态内部类+WeakReference,并在onDestroy时removeCallbacksAndMessages(null)。这个点几乎年年考,属于白给分,但每年都有很多人丢分。
2.3 多线程与并发:线程池参数不是背出来的
2018年的Android笔试题已经开始大量涉及线程池和并发编程了。原因很简单——项目里的异步任务越来越重,面试官需要确认你写并发代码时不会把线上搞挂。线程池的核心考点是ThreadPoolExecutor的七个参数,但很少人能把它们之间的计算逻辑讲清楚。
corePoolSize和maximumPoolSize是理解线程池的入口。当一个任务提交到线程池时,执行逻辑是:如果当前工作线程数小于corePoolSize,就新建线程;如果大于corePoolSize但小于maximumPoolSize,且阻塞队列未满,就把任务放进队列;如果队列也满了,才继续创建线程到maximumPoolSize;如果线程数已经到达maximumPoolSize且队列满了,就执行拒绝策略。这套阈值逻辑用生活类比来解释:一家餐厅有5个固定厨师(corePool),顾客多的时候,先让5个厨师忙起来;还是忙不过来,就把菜单排到等候区(workQueue);等候区也排满了,再招临时厨师(新增线程最多到maximum);临时厨师也忙不过来,就只能拒客了(RejectedExecutionHandler)。
笔试常考的一个陷阱是corePoolSize=0时的行为。当核心线程数为0时,新任务到达后会先尝试放入队列,而不是先创建线程。只有当队列满了,才会创建非核心线程去处理。这会导致一个现象:任务延迟升高、吞吐量下降,但线程数量能得到控制。如果你在项目里遇到“线程池创建了但任务一直不执行”的问题,八成是这个参数配置的问题。
同样的知识点在不同题目里的变体是CallerRunsPolicy拒绝策略。它不是在调用线程里抛异常,而是把被拒绝的任务直接交给调用线程去执行。这个策略的好处是既能降速,又能保证任务不丢失。笔试可能会问“哪种拒绝策略最不可能丢任务”,答案就是CallerRunsPolicy。但它的代价是占用调用线程的时间,如果用主线程提交任务,就可能卡UI。
我建议大家准备这部分时不要只看参数含义,要能把线程池的执行流程在纸上画出来,并说明每个分支发生在什么条件下。笔试题里经常给出一段代码,让你判断线程池内线程数量随时间的变化曲线,这种题只要掌握流程图就没问题。
3. 从笔试题到实战:解题思路与避坑经验
3.1 拿到卷子后的审题顺序
笔试的时间通常是一个半小时到两个小时,看起来充裕,但题量往往很大,尤其是选择题,每道题都要认真推演,时间并不宽裕。我的答题策略是先做简答题和场景题,再回过头做选择题。原因是简答题只要踩到知识点,拿分相对容易,但思考时间不定;选择题虽然看起来快,但很容易在模糊的记忆里反复横跳,一纠结就是好几分钟。先把容易拿的分拿到手,心里不慌,后面做选择题时状态会好很多。
审题环节有一个很大的坑:选项里的绝对化表述。比如“一定会回调onDestroy”“不会触发onNewIntent”这种带绝对词的选项,往往是错的,因为Android的生命周期和回调受太多系统状态影响。反过来,带“可能”“在特定条件下”这种留了余地的大概率是正确的。这不是玄学,而是出题人为了制造模糊,刻意在选项里埋的雷。看到绝对化表述,先给你自己提个醒,再回题干里找有没有例外条件。
另一个坑是题干里的时间定语。2018年那会儿Android 9已经发布,但很多老项目还在用targetSdkVersion 26甚至更低。同一个API在不同SDK版本下行为不一致的情况非常常见,比如运行时权限、后台启动限制、电池优化这些。出题人如果写了“targetSdkVersion 26”,你要按26的规则去作答,而不是拿最新版本的规则来套。答题时圈出题干里的版本号、机型、厂商这些关键词,能帮你避开至少三分之一的坑。
3.2 从常见错误答案看知识薄弱点
我接触过很多候选人,也在面试后复盘过他们的笔试答案,发现错误答案也分层次。最基础的一类是概念张冠李戴。比如把onPause和onStop的调用时机搞混:onPause是Activity部分不可见时调用,比如弹了一个Dialog或者来了一个半透明Activity;onStop是Activity完全不可见时调用。很多人写场景题时判断不出哪个阶段走了onPause、哪个阶段走了onStop,本质是对“可见性”的理解不够深入。
第二类是知其然而不知其所以然。典型的例子就是“为什么不能在子线程更新UI”。很多人能背出答案,但说不出根本原因。其实本质是UI控件的线程安全性问题——Android的UI访问没有加锁,如果在多线程里同时操作View,就可能导致状态不一致。为了保证简单高效,Android规定了只有主线程能修改UI,检测机制就是ViewRootImpl里的checkThread方法。如果笔试的简答题问到这个,你把这个机制写出来,比单纯背结论强很多。
第三类是缺少链路思维。比如问“点击一个Button到界面更新,中间发生了什么”。这个问题的完整链路是:触摸事件由InputManager读取,通过WindowInputEventReceiver传递到ViewRootImpl,再经过View的事件分发机制(dispatchTouchEvent->onTouchEvent)到达Button,触发onClick后执行更新逻辑,requestLayout触发measure/layout,invalidate触发draw,最后通过Choreographer同步到垂直信号,完成渲染。能把这条链路串起来,说明你对Android系统的理解是体系化的。答不完整也不可怕,但至少要把事件分发、UI刷新这两个核心环节写清楚。
3.3 实操心得:如何高效准备这一类的笔试
针对这类考察面广、深度也不浅的笔试,我不推荐刷题海。更高效的路线是先搭知识框架,再往框架里填细节。具体来说,可以按照“组件机制、异步与并发、UI绘制、性能优化、网络与存储”这五大模块,每个模块梳理出三条主线:这条线解决什么问题、核心类之间的调用关系、经典使用场景是什么。框架搭好之后,再针对每一条主线去看系统源码,验证自己的理解。
比如消息机制这一块,你不需要读完所有源码,只需要看三个方法:Handler.enqueueMessage、MessageQueue.next、Looper.loop。看完这三个方法的实现,你对消息机制的整个运行流程会有脱胎换骨的理解。源码阅读不要求逐行看明白,像nativePollOnce这种底层实现知道是epoll阻塞就够了,关键是把Java层的调用逻辑串通。
另外,强烈建议在准备笔试的同时开一个Demo工程,把理解变成代码。像Handler同步屏障、线程池拒绝策略、Activity在不同启动模式下的返回栈变化,这些都是可以写小Demo验证的。验证一次比背十遍强。而且面试时如果被追问,你能说“我自己写Demo验证过这个场景”,面试官的好感度会直线上升。
4. 高频易错点速查与面试官隐藏意图
4.1 易混淆知识点速查表
每年笔试题都有那么几个高频混淆点,我把它们整理成一张表,方便你考前快速过一遍。
| 对比项 | 核心区别 | 典型坑点 |
|---|---|---|
| onPause / onStop | onPause是部分不可见,onStop是完全不可见 | 弹Dialog只走onPause |
| onSaveInstanceState / onRestoreInstanceState | 前者在异常销毁前调用,后者在重建时恢复状态 | 按Home键也会触发保存,但不一定触发恢复 |
| invalidate / requestLayout | invalidate触发draw,requestLayout触发measure+layout | 改变尺寸必须requestLayout,否则只重绘 |
| Handler.sendMessage / post | post最终也会创建Message,只是把Runnable包进callback | 两者本质一样,post只是语法糖 |
| Serializable / Parcelable | Parcelable专为Android设计,效率更高 | Intent传对象只能用Parcelable或Serializable |
| ANR / Crash | ANR是主线程超时无响应,Crash是异常崩溃 | 产生原因和排查思路完全不同 |
| Activity的onNewIntent触发条件 | 只有reuse实例时才调用 | 不是所有启动模式都会触发 |
这些对比项在选择题里出现的频率极高。考前把这张表过一遍,能帮你稳定住大多数基础题的得分。
4.2 从笔试题看到面试官的隐藏意图
聪明人做笔试题,不只是为了得分,还会去揣摩出题人想考察什么能力。我以“自定义View的measure过程”为例,这道题表面上是考知识点,实际上有三个隐藏考察点。
第一个考察点是工程意识。面试官想知道你在自定义View时,有没有认真处理wrap_content和padding这两个坑。初创者在自定义View时最容易犯的错就是只处理EXACTLY模式,没有写wrap_content的默认逻辑,结果在XML里设了wrap_content却得到和match_parent一样的效果。第二个考察点是边界处理能力。有没有考虑过measureSpec的UNSPECIFIED模式,比如ScrollView里的子View高度测量就会用到这个模式。第三个考察点是阅读系统源码的意愿。如果你能说出View的onMeasure默认实现和setMeasuredDimension的关系,说你读过源码,面试官是会放在心里的。
再比如,笔试题里如果考了ContentProvider的权限设置,一般不是真的让你当系统级应用开发者,而是想考察你对敏感数据保护的理解。2018年后,Android对权限的控制越来越严,从运行时权限到分区存储,都是数据安全方向的延伸。回答这类问题能让面试官看出你平时写代码有没有安全意识。
4.3 笔试通过后的下一站:技术面的准备思路
笔试只是第一关。比试卷本身更有价值的,是它帮你完成的自我诊断。拿到笔试结果后,不管过没过,都建议把错题整理一遍,分析错误类型:是概念模糊、经验缺失,还是对源码理解不够。然后针对薄弱点去补强,这比漫无目的地刷题效率高得多。
如果笔试通过了,接下来就是技术面。技术面问的问题往往是笔试的延伸。比如笔试考了“MessageQueue的next方法如何阻塞”,面试时可能会追问“IdleHandler是干什么的”“什么时候会执行”。建议你在准备笔试时,就对每个知识点主动做一次追问,向上问一层的设计原因,向下问一层的代码实现。
从我这些年带新人的经验来看,能把笔试题目讲透的人,工作中写代码通常也有章法。因为Android开发的本质,就是不断在系统机制和业务需求之间做权衡。对系统理解越深,你就越能在产品需求里找到最优方案。这套知识体系,值得花时间慢慢沉淀。