又到校招季,很多朋友在找Android开发方向的笔试真题。金山办公2020校招Android开发工程师笔试题(二)这套卷子,我当年刷的时候印象挺深,后来给团队出面试题时也参考了它的思路。这套题不是那种死记硬背的八股合集,而是把Java基础、Android核心机制、性能优化、甚至工程化问题揉在一起考,很能反映一家做办公软件的公司对候选人的真实要求。这篇内容我就以这套题为引子,把高频考点、答题思路、背后原理都拆开讲一遍,不管是准备校招还是社招跳槽,都能直接用上。
1. 这套笔试题到底在考什么
1.1 从一份笔试题看企业选人逻辑
先明确一个前提:笔试不是看你背了多少面试题,而是看你在"没有IDE、没有网络、有限时间"的前提下,能不能调用知识体系解决问题。金山办公的主打产品是WPS,这类办公App对稳定性、流畅度、兼容性的要求极高,所以考点会明显偏向Java并发、Android消息机制、进程通信、性能优化和代码设计。
我拿到的这套(二)卷子,整体难度在同级别公司里算中上。选择题部分覆盖了Java内存模型、集合类、多线程、Activity启动模式、Handler机制、网络协议等。简答题里出现了"Activity A启动Activity B时生命周期如何变化""Handler导致内存泄漏的原因及解决方案"。代码题则是一道手写LRU缓存和一道自定义View测量逻辑。怎么看都不是靠短期背题能应付的,需要真正理解原理,并且有手写代码的功底。
1.2 试卷结构和题型分布
从题型结构上,我复原一个大致的框架(不同批次可能微调):
| 题型 | 数量 | 侧重点 |
|---|---|---|
| 单选题 | 10道左右 | Java语法、Android基础、网络协议、数据结构 |
| 多选题 | 5道左右 | 边界情况、易混淆概念、多知识点交叉 |
| 简答题 | 3道左右 | 源码分析、机制原理、方案设计 |
| 编程题 | 1-2道 | 数据结构与算法、自定义View、线程交互 |
单选的很多题是"下列哪个说法正确"或"下列哪个方法在onCreate中不被调用"这类,考得很细。多选题最容易失分,因为选项之间往往只差一个限定词,比如"关于Handler的说法,正确的有"里面,把同步屏障和IdleHandler混在一起。这些如果平时只是用API不看源码,基本会错一半。
编程题的时间分配很关键,建议60分钟左右的笔试,给编程题留25到30分钟。如果前面选择题卡太久,后面代码题写不完,反而得不偿失。
2. 高频考点逐个拆解
2.1 Java基础与并发:不只是背概念
Java基础在笔试题里占的比重大概三分之一,但考察的深度经常会超出预期。String那类问题几乎每套题都有,比如"String和StringBuilder的区别""==和equals的区别"。如果只答"String不可变、StringBuilder可变",最多拿一半分。更好的答法是:
- String底层final char数组(Java 9之后是byte数组),每次修改都产生新对象,循环拼接会频繁GC。
- StringBuilder和StringBuffer都继承AbstractStringBuilder,区别是StringBuffer的append方法加了synchronized,单线程下优先用StringBuilder。
- 用"a" + b这类字面量拼接,编译器会自动优化成StringBuilder,但循环内拼接每次都会new StringBuilder。
另一个重头戏是线程池。笔试题常给出一个Executors.newFixedThreadPool(3),问核心线程数、最大线程数、队列长度分别是多少,以及会不会OOM。这就要说清楚ThreadPoolExecutor的7个参数:corePoolSize、maximumPoolSize、keepAliveTime、TimeUnit、BlockingQueue、ThreadFactory、RejectedExecutionHandler。Executors.newFixedThreadPool底层用的是LinkedBlockingQueue,队列默认Integer.MAX_VALUE,任务排长队时会占大量内存,所以企业规范里往往不让直接用Executors,要手动new ThreadPoolExecutor,用有界队列和显式拒绝策略。
并发里还有个高频题:双重检查锁单例为什么要加volatile。笔试题会问"这段代码是否有问题",如果不加volatile,第二个线程可能在instance还没初始化完成时就拿到引用,原因是CPU指令重排导致new Singleton()的三步操作(分配内存、初始化对象、赋值引用)不保证有序。volatile禁止重排,这个问题才解决。
2.2 Android四大组件深挖
四大组件里Activity和Service是考察重点。Activity的启动模式和Intent Flag组合,几乎每份卷子都会出现。standard、singleTop、singleTask、singleInstance四种模式要背,但更重要的是理解实际场景。比如WPS从文档页跳转到分享页,如果用singleTop,当前Activity在栈顶时复用onNewIntent而不会新建;如果用singleTask,会清掉该Activity之上的所有Activity。
Service的考察点在两种启动方式。startService和bindService的区别不只是生命周期,面试官常追问:bindService方式下,onUnbind返回true后再次bind会不会调用onRebind(答案:会)。还有进程通信,AIDL的流程,Messenger和AIDL的对比。这些在纯笔试里可能只出判断题,但后面面试会连环问。
BroadcastReceiver动态注册与静态注册的区别也很经典。Android 8.0之后,静态注册绝大多数隐式广播都不能在AndroidManifest里接收了,所以有些老项目检查一遍,把不必要的静态注册改成动态。ContentProvider考得相对少,但WPS这种应用,内容提供者常用于文件访问和跨进程数据共享,题目可能会问"ContentProvider的onCreate调用时机"(Application.attachBaseContext之后,Application.onCreate之前)。
2.3 Handler与消息循环:必考源码题
几乎每一套Android校招笔试题都绕不开Handler。常见问法包括:
- Handler、Looper、MessageQueue三者关系。
- 一个线程有几个Looper?几个Handler?主线程的Looper在哪创建?
- 子线程里如何new Handler?必须先调用Looper.prepare(),再调用Looper.loop()。
- Handler的post()和sendMessage()有什么区别?(源码上看post最终也是调用sendMessageDelayed,把runnable包装成message,callback为runnable。)
- MessageQueue.next()中如果队列为空会阻塞,为什么不会导致主线程卡死?因为主线程在loop()里无限循环,有消息就执行,没消息就阻塞等待,而各种callback会往队列里放消息。
再往深处问,还有同步屏障、消息优先级、IdleHandler。笔试中加入IdleHandler,往往作为加分项,比如"Activity的onResume之后通过IdleHandler做懒加载"是一个不错的实践。如果答不上来,前面答对了也不至于挂,但如果能答上来,基本就能和别的候选人拉开差距。
关于内存泄漏的答案,最好按这个层次来:非静态内部类Handler默认持有外部Activity引用 -> 消息队列中如果有延迟消息,会一直持有Handler -> 导致Activity无法回收。解决思路有:静态内部类 + WeakReference,或在onDestroy时removeCallbacksAndMessages(null)。如果用一个全局单例的Handler,其实不存在内存泄漏问题,但要注意线程安全。
2.4 性能优化与内存管理
办公App和社交App不一样,用户会在手机上打开几十MB的文档,滑动、缩放、渲染、保存,任何一步卡顿都非常明显。所以金山这类公司特别爱考性能优化。笔试题常见的有:
- 内存泄漏的常见场景:静态变量引用Activity、Handler、非静态内部类持有外部类、匿名AsyncTask持有Activity、单例里持有Context。需要区分ApplicationContext和ActivityContext。
- 可以用LeakCanary检测内存泄漏,原理是监控Activity.onDestroy之后,通过ObjectWatcher在Activity被Gc之前保存引用,如果等待一段时间后还是没被回收,就Dump Heap分析引用链。
- 布局优化:include、merge、ViewStub的使用场景。合理使用ConstraintLayout减少嵌套。如果笔试出布局代码,让你看哪里可以优化,通常能发现RelativeLayout嵌套多层LinearLayout的问题。
- 过度绘制、卡顿优化、Traceview和systrace的使用,虽然不一定让你写,但简答题里可能会让你设计一个"应用启动优化方案"。
关于启动优化,常见思路是:在Application中只初始化必须的库,把业务初始化延时或放到子线程,使用ContentProvider自动初始化但注意时序,用启动加速框架比如AnchorTask形成任务依赖图,还有今天启动页秒开、减少dex加载耗时等。答题时至少要写出"分层初始化+异步化+时序清晰",这符合工程思路。
2.5 网络与数据存储
网络这一块,笔试题不会让你直接写HTTP报文,但会问HTTPS的加密过程,OkHttp的拦截器链,以及TCP和UDP区别。尤其是"TLS握手过程"是很多公司的必考题。需要能说出:客户端发送ClientHello,服务端返回证书,客户端验证证书并生成随机密钥并用公钥加密,服务端使用私钥解密得到对称密钥,之后双方用对称加密通信。要分清公钥加密和对称加密的区别。
数据存储方面,SharedPreferences是老考点。会问"多次commit和apply有什么区别"。commit同步返回boolean,apply异步并且不返回结果,apply过程中如果Activity在onPause之后,由ActivityThread中的QueuedWork等待写入完成,可能导致ANR。所以如果频繁小数据写入且能容忍丢失,用apply;需要立即生效且关心结果,用commit。进阶点是MMKV,基于mmap的文件映射,写入时直接写到内存并由系统回调刷盘,性能远好于XML序列化。
网络题目中还经常出现"如何设计一个可扩展的拦截器链",这其实就是OkHttp的设计。如果能把Chain理解为链表,每个Interceptor拿到Request和Chain,处理后调用chain.proceed继续传递,笔试分就拿到了。
2.6 手写代码题与算法思路
代码题虽然只有一两道,但含金量最高。我见到最多的两类是:手写LRU缓存、模拟一个生命周期相关的逻辑。一道典型的LRU题:实现一个LRUCache,要求get和put的时间复杂度O(1)。标准解法是双向链表+HashMap,达到容量上限时删除链表尾部节点,新访问的节点移到头部。很多人能背出LinkedHashMap的removeEldestEntry,但笔试要求手写,平时不看链表操作的人容易卡住。写的时候注意几个细节:
- 定义双向链表节点Node<K,V>。
- HashMap用于查找节点,每个节点需要同时保存key和value(因为删除尾部节点时要拿到key去map里删除)。
- 包含虚拟头尾节点,翻转操作时避免空指针。
- 头尾节点初始化时互相连接,moveToHead和removeNode要逻辑统一。
手写完成后再答一道追问:如果并发读写怎么办?此时想到加锁,或者用读写锁,或者直接同步代码块。这考察工程意识。
还有一种代码题是"分析View的测量和布局",给出一个自定义View的onMeasure,问你measure mode是AT_MOST时应该怎么做。此时需要明确区分EXACTLY、AT_MOST、UNSPECIFIED三种情况,并且说明MeasureSpec是父View限制子View的规则。常见错误是直接使用getDefaultSize而不根据mode判断,导致wrap_content时占满全屏。
3. 复盘一次真实笔试过程
3.1 考前准备清单
讲完知识点,我再用实际经验说说怎么准备这套题。如果你正在准备Android校招,下面这份清单可以照着复习:
- Java基础:集合源码、HashMap原理、并发包、异常、泛型、注解。
- 数据结构:链表、栈、队列、二叉树、HashMap、LRU、TopK。
- Android核心:Activity生命周期与启动模式、Fragment生命周期、Service、BroadcastReceiver、ContentProvider。
- 消息机制:Handler、Looper、MessageQueue、Choreographer、IdleHandler。
- 事件体系:事件分发、手势冲突、多点触控。
- 性能优化:内存泄漏、布局优化、卡顿、APK瘦身、功耗。
- 网络:HTTP/HTTPS、TCP/IP、OkHttp、Retrofit、JSON。
- 项目亮点:梳理项目里最复杂的一个技术点,能用两句话说清楚"遇到什么问题-怎么解决的-结果怎么样"。
注意,简历上写的任何一个"熟悉"都可能被面试官拎出来细问。写"熟悉Handler机制"就要准备好被问到native层。写"熟悉内存优化"就要准备LeakCanary源码。所以准备笔试答案的同时,也要把面试追问准备好。
3.2 答题时间分配与策略
正常笔试的做题节奏我是这么安排的:
- 前5分钟先通读全卷,标注出哪些题是拿分题、哪些题是难点。
- 选择题控制在25分钟内,不确定的先标出来,不要恋战。有5道单选、3道多选、2道简答、1道代码题的卷子,整体时间60分钟,这样时间基本够用。
- 简答题每题8到10分钟,先写结论,再写理由,再补充细节。比如"为什么Handler会导致内存泄漏"这类题,答案写"会"之后,要快速展开:现象、原因、解决方案,分点写清楚。千万不要只写"不会"或"会"而没有论述,阅卷人会默认你只是猜的。
- 编程题留最后25到30分钟。如果时间不够,先写思路和数据结构,再写核心逻辑,最后补边界判断。最忌讳上来就默写代码,然后发现思路错了整段推倒。
3.3 手写代码题的典型踩坑
手写代码的时候,因为不是在IDE里,很多程序员会暴露出基本功短板。常见问题有几个:
第一,不判空。比如输入数组可能为null,链表可能为null,有些人上手就写length访问,在笔试这种"环境"里很容易被扣分。
第二,循环边界搞错。LRU删除节点时,忘了处理头尾节点,或者HashMap的key和value类型不一致。写完之后如果时间充裕,一定要用几组样例在草稿纸上模拟一遍。
第三,变量命名随意。定义temp、a、b,阅卷人看着费劲,也容易把自己绕晕。我建议用有意义的命名,例如preNode、nextNode、newNode。
第四,不考虑扩容和并发。HashMap或LinkedHashMap写出来很简单,但笔试要的是双向链表+HashMap,这是考点。如果写个LinkedHashMap,虽然实现正确,但没展示出你的数据结构功底,可能就拿不到满分。
4. 常见易错点与排查技巧实录
4.1 最容易失分的5个知识点
这几年的笔试以及简历筛选中,我发现几个特别容易失分的地方,整理成表格方便对照:
| 易错点 | 错误理解 | 正确理解 |
|---|---|---|
| onNewIntent | 认为singleTop启动模式下每次都调用onNewIntent | 只有目标Activity已在栈顶时才会调用onNewIntent,不在栈顶时仍走onCreate |
| dp与px | 认为dp就是物理尺寸单位 | dp是密度无关像素,在mdpi密度下1dp=1px,在xhdpi下1dp=2px |
| Parcelable与Serializable | 认为Serializable性能更好 | Serializable使用反射,序列化慢,Android跨进程传输对象优先选Parcelable |
| Handler.getMessage | 认为Message的obj和what必须同时存在 | what标识类型,obj携带对象,无必然绑定关系,复用Message要清理字段 |
| Activity的启动模式组合 | 只背四种模式 | 要结合Intent Flag,例如FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_CLEAR_TOP等组合效果 |
比如第4条,因为面试官可能问"消息池中的消息如果被发送,出去之后要不要回收?"这其实就是Message.obtain()和recycle()的使用,但很多人会答成"发出去就不管了"。正确做法是在Message使用完毕(比如Carrier处理完)后,如果不再需要,可以回收,但Handler内部在分发完消息后并不自动回收,而是由MessageQueue在next()中取出消息后设置字段并复用。如果某个消息设置了callback,执行完不回收,导致Message被MessagePool复用后字段残留,就会踩坑。
4.2 面试官追问环节如何应对
笔试提交后通常还有一轮技术面,围绕试卷展开。这时面试官不会单纯念答案,而是在你的回答基础上深挖。我这里列举一些真实的追问方向:
- 你写了Handler内存泄漏解决方式,面试官会问:"为什么要用WeakReference?如果Activity还在正常使用,不会是弱引用吗?"答案是:弱引用会在GC时被回收,但Handler发送延迟消息时,如果持有Activity弱引用,即使消息还没执行,只要Activity没有被强引用,它就可能被回收,而Handler本身还在处理消息。但要注意Handler的Runnable或Callback如果持有Activity的强引用,哪怕Handler是静态的,泄漏依然存在。
- 你写了LRU,面试官会问:"如果只让你用get和put,你会怎么改?如果数据量很大,用HashMap会不会内存不够?"这时可以提到可以在节点上增加访问时间戳,也能实现,但时间复杂度不是O(1)。还可以提能否用Redis的近似LRU思想,但笔试一般不必讲这么深。
- 简答题里写"使用ViewStub优化布局",面试官追问:"ViewStub和include都能复用布局,什么时候用ViewStub?"答案是:当某个布局不是必然显示时用ViewStub,替换初始化开销;include用于固定静态区域,比如公共头部底部。
4.3 关于这套题的一点个人体会
在准备这套题的过程中,我最大的感受是:它不鼓励死记硬背,而是要求"知其然,知其所以然"。比如Activity启动模式,光记住"singleTop栈顶复用"不够,还要能描述出调用链。写代码题时,不仅要能运行,还要能解释复杂度。我后来把这套题的错题整理成了一个Notion笔记,每道题都补了源码截图和实验数据,效果比抱着一堆PDF资料好太多。
如果你正在准备类似笔试,建议你每天花两小时做三件事:一是用源码视角读框架,二是手写一遍核心数据结构,三是自己出题考自己。比如看完Handler源码后,合上电脑,在白纸上画出Looper.loop()的执行流程和消息回收路径。画不出来的地方,就是薄弱点。做两三套这种笔试题后,你会发现考官的出题逻辑其实很清晰:就是用一道题筛选出"看过源码的人"和"只调过API的人"。
我最后再分享一个小技巧:笔试时如果遇到不会的简答题,不要留白。把相关的、确定正确的知识点写上去,再说明"遇到这个问题,我会先通过xxx定位,再在xxx处解决"。这体现了解决问题的思路,很多阅卷官会因此给你步骤分。笔试本身就是一场压力测试,谁都会遇到盲区,但能不能用逻辑和表达弥补盲区,恰恰是公司想看到的能力。