2017挖财安卓校招笔试题复盘:Handler、启动模式与性能优化全解析 2017年的安卓校招笔试现在翻出来看还挺有意思的。挖财作为互联网金融领域比较活跃的公司当年这套卷子在圈子里流传度不低很多准备校招的安卓开发都拿它当模拟题练手。我自己当时也做过一遍如今再回头看里面的考点其实放在今天依然不过时——Handler机制、Activity启动模式、图片加载优化这些依然是面试官最爱问的东西。这篇文章就借这份试卷做个深度复盘从题目本身拆解考点结合当年的技术背景和现在的Android开发现状把每类题背后的考察意图和标准答案都摊开讲清楚。如果你是正在准备安卓校招的应届生或者想系统梳理一遍安卓基础知识的开发者这份复盘应该能帮你省不少时间。就算你已经工作几年了也可以看看当年的题目和现在的主流做法差了多少就当一次知识体检。1. 试卷整体设计与考察思路先说说这份试卷的结构。整份卷子大概分为四个板块选择题、简答题、代码输出题和设计题。题量和考试时间记不太清了但印象中是两个小时左右题量不算小想优哉游哉做完不太现实。从考察范围来看这份试卷覆盖的知识点其实非常典型Java基础、Android四大组件、异步机制、内存管理、性能优化、网络数据解析外加一道开放性设计题。可以说2017年校招安卓工程师应该掌握的核心技能基本都被这张卷子圈进去了。为什么校招笔试要这么出核心原因在于校招和社招的考察逻辑不一样。社招主要考察你对某个技术栈的实战深度比如你说做过插件化那面试官会追着你问插件化原理、坑点和落地细节。但校招候选人大多没有完整的商业项目经验考察重点就落在基础是否扎实、思路是否清晰、有没有自驱学习的能力上面。所以你会发现一个现象很多校招笔试的题目本身并不难甚至有点“陈旧”。比如让你讲讲Handler消息机制的原理或者画一下Activity的生命周期图这些都是教科书上写得清清楚楚的东西。但恰恰是这些基础题能区分出两类人一类是背了答案的一类是真正理解了的。我见过很多同学刷了几百道面试题Handler机制背得滚瓜烂熟但一旦追问“主线程为什么默认有Looper”“Looper.loop()为什么不会阻塞主线程”就答不上来。这类问题在笔试中出现往往不是想考倒你而是想通过几道层层递进的追问判断你有没有真正融会贯通。另外设计题的存在也很关键。它考察的不是代码能力而是设计思维和表达能力。能不能把一个模糊的、开放性的问题拆解成清晰的模块、明确的流程和可落地的方案这一点在后端面试和安卓面试里都是加分项。2. 选择题与基础概念题拆解选择题和基础概念题是笔试的第一道关卡。这部分通常占总分30%左右考察面比较广但难度相对较低。下面我挑几类典型题目按照当年的题目风格还原并解析。2.1 Java基础字符串、集合与并发Java基础是安卓笔试的必考板块而且通常放在最前面。2017年时Java 8刚出来不久安卓开发主力语言还是Java 7/8所以笔试题也集中在比较经典的Java知识点上。比如有一类几乎每年都会出现的题目String a hello; String b hello; String c new String(hello); System.out.println(a b); // true System.out.println(a c); // false System.out.println(a.equals(c)); // true这道题考察的是字符串常量池和对象引用的区别。a和b都指向常量池中的同一个字符串对象所以比较的是引用地址结果是true。而c通过new创建了一个新对象指向堆内存所以和a的引用地址不同结果就是false。这种题本身没有任何坑但很多人会错在最基础的概念上比较的是内存地址equals比较的是内容。字符串类重写了equals方法所以内容相等时返回true。如果连这个都搞不清后面的HashMap、HashSet源码题就更不用谈了。再举一个集合类的经典题HashMapString, String map new HashMap(); map.put(key, value);这道题的考点在于HashMap的内部结构。2017年的题还停留在JDK 7的“数组链表”结构后来JDK 8引入了红黑树链表长度超过8时树化。问法通常有两种一种是直接问HashMap的原理另一种是泛化问“为什么HashMap是线程不安全的”。关于线程安全性正确的理解是HashMap在并发写操作时多个线程同时调用put方法可能导致数据覆盖严重时在JDK 7中还会形成环形链表造成死循环。这个问题在安卓中虽然不常见但对理解并发模型很有帮助。当年很多同学答这道题时会丢分因为只回答了“因为不是同步的”这种表面原因。标准答案应该至少包含两个层面第一HashMap的put操作不是原子的多线程同时修改同一个桶位会丢数据第二扩容过程中并发操作可能引入循环依赖导致get时发生死循环。2017年时JDK 7还比较流行这个问题值得展开讲到了今天JDK 8以后循环链接问题在红黑树的改造中已经被部分规避但并发安全问题依然存在。与HashMap相对的Hashtable和ConcurrentHashMap的区别也是高频考点。这时候可以顺带补充一点Hashtable对整个Map加锁效率低ConcurrentHashMap通过分段锁JDK 8后改为CAS加synchronized提升并发度。在安卓开发中涉及到缓存和全局数据存储时ConcurrentHashMap是比Hashtable更合理的选择。2.2 Android基础生命周期、启动模式与内存机制安卓基础概念的题基本绕不开Activity生命周期和启动模式。生命周期这道题考察的不是“背出七种状态”而是“能不能结合场景说出完整流程”。例如当Activity A启动Activity B不透明然后按返回键回到A期间A和B分别经历了哪些生命周期标准回答A启动B且B不透明A.onPause() → B.onCreate() → B.onStart() → B.onResume() → A.onStop()按返回键回到AB.onPause() → A.onRestart() → A.onStart() → A.onResume() → B.onStop() → B.onDestroy()这里面有一个细节被很多人忽略B启动时A是先执行onPause再启动B的所以如果我们在onPause里写耗时操作会严重影响新Activity的启动速度。这也是为什么官方建议把耗时操作放在onStop而不是onPause。Activity启动模式也是必考题四种模式中singleTask和singleTop是重点。单拿singleTask来说onNewIntent的调用时机、返回栈的行为、和taskAffinity的关系这三层递进的深度能测出一个人对任务栈的理解程度。再有一个必考的点就是进程和内存机制。比如这个问题Android中一个进程的默认内存大小是多少如何查看如何申请大内存这道题有坑。首先不同的ROM和屏幕尺寸下默认内存值并不一样一般高档机型可以在/system/build.prop中看到dalvik.vm.heapgrowthlimit典型值在256MB到512MB之间。其次“申请大内存”可以通过在AndroidManifest.xml中给application节点设置android:largeHeaptrue实现但这只是扩展了Dalvik堆大小系统依然可能因为设备总内存不足而杀进程。我当时的回答是“不建议为了省事设置largeHeap因为你占用了系统大量内存应用更容易被杀掉而且后台驻留能力会下降”。这个观点放到今天依然成立。2.3 Android核心机制Handler、AsyncTask与消息循环Handler机制在2017年校招笔试中出现概率极高。因为它既是安卓事件循环的基石又是理解线程通信和异步编程的核心还是各种面试追问的入口。先看最常见的问法简述Handler、Looper、MessageQueue三者的关系并说明主线程和子线程的Looper区别。标准回答Handler负责发送和处理消息每个Handler创建时需要绑定一个Looper。Looper负责消息循环每个线程最多只能有一个Looper通过Looper.prepare()创建通过Looper.loop()启动循环。MessageQueue是Looper持有的消息队列采用单链表结构按时间顺序排列消息。主线程启动时系统会自动创建Looper和MessageQueue子线程默认没有Looper需要手动调用Looper.prepare()和Looper.loop()。针对这个问题我见过最优秀的回答还会补充Handler和线程的关系是一对多的一个线程可以创建多个Handler但一个Handler只能属于创建它的那个线程。从代码层面看关键点在于构造函数里如何获取Looper——如果你不显式传LooperHandler会通过Looper.myLooper()获取当前线程的Looper而myLooper()底层又依赖ThreadLocal来保证线程隔离。追问版本通常是这个主线程的Looper.loop()为什么不会导致ANR这个问题的核心在于主线程其实是被Looper阻塞住的而不是在忙轮询。当没有消息时主线程会通过epoll机制进入休眠状态释放CPU资源所以不会造成CPU繁忙和卡顿。当有新消息进来时内核唤醒主线程处理消息后再次进入休眠。ANR的本质不是“主线程被阻塞”而是“主线程在规定时间内没有处理完事件”比如广播超时是10秒、输入事件超时是5秒。AsyncTask在2017年也是高频考点因为它极简的API设计让它成为很多项目的首选异步方案。但这里考察点往往集中在“AsyncTask的串行执行机制”和“生命周期问题”上。首先要明确一点从Android 3.0开始AsyncTask默认是串行执行的。虽然它的内部维护了一个线程池但默认通过一个SerialExecutor将多个AsyncTask排队执行所以并不是并行跑。要想并行执行必须手动调用。其次AsyncTask有个很尴尬的问题在Activity旋转或重启时AsyncTask持有的Context引用可能导致内存泄漏如果Activity已经销毁onPostExecute里回调UI还会抛异常。我在2017年做这套题时对AsyncTask的态度就不太乐观因为它的边界感太模糊不符合“异步任务要可控、可取消、可追踪”的要求。后来协程和RxJava流行起来之后AsyncTask彻底沦为历史包袱。但笔试中它依然是试金石——能说出SerialExecutor和生命周期问题的候选人说明用心读过源码而不只是停留在API使用的层面。3. 代码分析题与编程题实战代码分析题是整张卷子里最“爽”的部分因为不用写很多字直接看代码输出结果或者指出问题。这类题非常考验功力因为代码可能包含隐藏的类型转换、运算符优先级、并发陷阱等问题。下面我把当年遇到的几种典型题目整理出来并给出完整的分析过程。3.1 经典输出题自增、类型转换与静态绑定来看一个经典的Java输出题public class Test { public static void main(String[] args) { int i 0; i i; System.out.println(i); Integer a 127; Integer b 127; Integer c 128; Integer d 128; System.out.println(a b); System.out.println(c d); } }输出结果是什么第一题i i的结果是0不是1。原因在于i的求值顺序是“先取值后自增”相当于int temp i; // temp 0 i i 1; // i 1 i temp; // i 0所以最终i还是0。这个题已经骗过无数人笔试里出现概率极高。第二组关于Integer的比较就更有意思了。a b输出truec d输出false。原因是Java的Integer缓存机制-128到127之间的整数会被自动装箱为缓存对象所以a和b指向同一个缓存对象而128超出缓存范围c和d各自创建一个新对象引用地址不同。这个知识点在面试中也很常考本质上是“缓存”与“对象比较”的思维陷阱。下面再看一道多态和静态方法的题class Father { public void show() { System.out.println(father); } public static void staticShow() { System.out.println(father static); } } class Son extends Father { public void show() { System.out.println(son); } public static void staticShow() { System.out.println(son static); } } public class Test { public static void main(String[] args) { Father f new Son(); f.show(); f.staticShow(); } }输出结果是son和father static。第一行输出son是因为实例方法走动态绑定f的实际类型是Son所以调用子类的show方法。第二行输出father static是因为静态方法属于类不存在覆盖关系只和引用变量的静态类型有关。Father f的静态类型是Father所以调用的是Father.staticShow()。这种题目在笔试中属于“一票否决型”答错基本上说明Java的面向对象基础和分派机制理解不到位。准备校招时一定要把“静态绑定/动态绑定”“隐藏/覆盖”这两组概念吃透。3.2 手写单例从双重检查锁到静态内部类编程题的风格通常是“用你熟悉的语言手写一个XXX”单例模式是出镜率极高的选择。但2017年挖财这套卷子有一个特点它会问“你这个写法是不是线程安全的”以及“要不要加入volatile”。最标准的双重检查锁写法public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }注意这里必须加volatile。instance new Singleton()并不是一个原子操作它分为三步分配内存空间初始化对象将引用指向内存空间在没有volatile的情况下第2步和第3步可能被重排序。如果线程A先执行了第3步引用指向了未初始化的内存线程B进来时发现instance不为null直接返回一个半初始化的对象后续使用就会出问题。另外还有一种更推荐的写法——静态内部类public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }这种写法利用了类加载机制保证线程安全而且只有调用getInstance时才会加载内部类做到懒加载。答这种题的时候可以主动把单例模式的几种写法对比一下饿汉式、懒汉式、双重检查锁、静态内部类、枚举。说清楚每种写法的优缺点和适用场景给面试官留下“你不仅仅会背代码还懂取舍”的印象。3.3 手写LRU缓存LinkedHashMap还是自己写LRU缓存是很多公司笔试的常客挖财这套题里也出现过。Android开发中图片加载框架的内存缓存基本就是LRU策略所以这道题很贴近实际场景。最简单的实现是直接用LinkedHashMappublic 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) { return map.get(key); } public synchronized void put(K key, V value) { if (map.containsKey(key)) { size - safeSizeOf(key, map.get(key)); } size safeSizeOf(key, value); map.put(key, value); trimToSize(maxSize); } private void trimToSize(int maxSize) { while (size maxSize) { Map.EntryK, V toEvict map.entrySet().iterator().next(); map.remove(toEvict.getKey()); size - safeSizeOf(toEvict.getKey(), toEvict.getValue()); } } private int safeSizeOf(K key, V value) { return 1; } }关键点在构造函数里的第三个参数true它表示按访问顺序排序。每次get或put时被访问的元素会被移到链表尾部这样链表头部就是最久未使用的元素淘汰时直接移除头节点即可。实际开发中Android官方的LruCache实现和这个差不多但增加了一些细节sizeOf可以自定义比如以图片字节数作为容量单位entryRemoved回调可以做一些资源清理工作。回答时要主动提到这些细节说明你不是从网上背的模板而是真看过源码。4. 设计题与扩展题地图、网络和多屏幕适配设计题往往是校招笔试的压轴题也是拉开分数差距的地方。这类题没有标准答案考察的是你有没有全局设计的能力。挖财这套卷子里有一道让我印象很深的设计题大意是请设计一个安卓App的基础网络层要求支持RESTful API、图片加载、缓存和请求取消。如果只是说“用OkHttp加Retrofit加Glide”那基本拿不到高分。面试官想听到的是对分层、线程切换、生命周期管理的完整思考。4.1 网络层的分层设计我会把网络层拆成四层第一层是API层定义接口比如用Retrofit的注解把RESTful API声明出来。这一层只关心“我要请求什么”不关心底层请求怎么发、数据怎么解析。第二层是请求层负责把API层的接口转换为实际的HTTP请求处理请求头、公共参数、加密签名和错误码映射。第三层是策略层负责缓存、重试、请求合并和请求取消。比如WebView中常见的资源预加载就需要网络层支持一个请求对应多个页面而列表页的刷新加载则需要支持分页请求的自动拼接。第四层是数据层负责把网络响应的JSON数据转为Java对象并进行数据库缓存或内存缓存。这一层在MVP或MVVM架构中通常对应Repository。按照这个思路设计每个层的职责都很清晰替换底层实现时只需要改第二层其他层完全不受影响。这不只是笔试答案实际项目里我的网络层也是按这个思路搭的。4.2 图片加载的艺术缓存策略与大图处理图片加载是设计题中另一个常见的考察点因为它涉及的东西非常多内存缓存、磁盘缓存、网络加载、线程管理、生命周期感知、图片压缩方案。答这种题时我建议把流程说成一个完整链路请求图片时先在内存缓存中查找命中则直接返回。内存缓存未命中查磁盘缓存命中则解码返回并加入内存缓存。磁盘缓存未命中再走网络下载下载完成后同时写磁盘缓存和内存缓存。加载过程中要考虑生命周期列表快速滑动时应该取消不可见item的加载任务。解码时要做采样压缩根据所需尺寸对原图进行inSampleSize缩放避免大图直接加载到内存导致OOM。最后这个“采样压缩”细节经常被忽略但它是区分“用过Glide”和“理解Glide”的关键。2017年时Fresco和Glide竞争激烈很多团队选Fresco是因为它的三级缓存设计更完善尤其在低端机上优势明显。但Glide胜在体积小、API简单后来逐渐成为主流。这个选型逻辑也可以在回答中提一提会让面试官觉得你确实有项目经验。4.3 多屏幕适配与碎片化问题2017年的安卓市场碎片化问题比现在还严重不同分辨率、不同屏幕尺寸、不同ROM定制版本满天飞。设计题里偶尔也会出现“如何适配不同屏幕”的提问。我当时的回答思路是布局上使用dp和sp作为单位避免硬编码像素值。对不同屏幕密度提供多套切图资源放入drawable-mdpi、drawable-hdpi、drawable-xhdpi等目录。代码中动态适配比如某些ROM的导航栏高度不一致需要通过WindowInsets或Resources.getIdentifier动态获取系统栏高度。使用ConstraintLayout作为首选布局减少嵌套层级提高适配灵活性。放到今天来看这个思路依然适用只是切图部分已经用VectorDrawable和自适应图标替代了很多场景。但万变不离其宗适配的本质是让UI在任何设备上都能正确表达间距、尺寸和比例。5. 场景题与面试官的隐藏考察点笔试中除了上面的技术题还会有一些看似开放、实则考察工程意识和项目经验的场景题。这类题往往没有标准答案但回答的好坏一眼就能看出你有没有真正做过安卓开发。5.1 线上App崩溃了你怎么办这是典型的“灵魂拷问题”。如果只是简单回答“看崩溃日志”就太单薄了。一个合格的安卓工程师面对线上崩溃处理的完整链路是第一步确定崩溃类型。是Java异常还是Native崩溃Java异常可以从Logcat拿到堆栈Native崩溃需要通过breakpad或tombstone拿到寄存器信息。第二步查看崩溃堆栈定位崩溃发生的位置结合代码和资源文件检查是否有空指针、数组越界、资源找不到等问题。第三步确认复现条件。如果不能稳定复现就通过日志系统观察频次根据用户机型、系统版本、App版本和页面路径筛选。第四步针对特定崩溃做防御性处理。比如ClassNotFoundException可能是因为混淆配置问题在proguard-rules.pro中加-keep规则比如OutOfMemoryError则需要优化图片加载策略或者检查是否有内存泄漏。我在笔试卷子里看到这个问题的次数很多但能把“上传崩溃日志后台聚合版本灰度热修复”整套流程说全的人不多。热修复技术2017年正好是风口期Tinker和阿里百川的Sophix打得火热如果你能在回答中补一句“如果崩溃不是逻辑问题而是业务资源问题可以考虑热修复方案但要控制灰度风险”绝对是一个加分点。5.2 如何降低App启动时间启动优化是安卓性能优化的永恒话题。2017年时大家聊得最多的还是冷启动优化现在又加上了启动阶段各类SDK的懒加载和启动仪表的建设。笔试中遇到这个问题可以从三个层面展开。第一层是启动流程的理解冷启动时系统首先要创建进程、加载Application、创建主线程、执行onCreate然后才会加载MainActivity。Application的onCreate里如果做了太多事启动时间就会明显变长。这个道理大家都懂但很少人真正测算过到底哪些代码耗时最长。第二层是优化手段减少ApplicationonCreate中业务SDK的初始化把不紧急的SDK放到子线程初始化。用启动器框架比如后来出现的Startup处理任务依赖让不需要依赖主线程的任务异步化。使用apply()替代commit()做SharedPreferences写入避免同步阻塞。布局优化减少不必要的嵌套布局用include和ViewStub延迟加载。第三层是量化指标优化前先通过Debug.startMethodTracing或者Android Studio的CPU Profiler测算出启动阶段各方法的耗时再针对耗时长的方法做优化。没有数据支撑的优化都是自嗨。6. 安卓生态与技术演进从2017到现在的变与不变这套卷子里有不少题目放到今天依然有参考价值但技术栈确实发生了不小的变化。准备面试时如果只知道旧技术遇到新题容易露怯。这里把2017年到现在的演进过程简要梳理一遍。6.1 语言层从Java到Kotlin的切换2017年时Kotlin刚刚被Google宣布为安卓一级开发语言但大多数团队的存量代码还是Java新项目也很少直接用Kotlin。那年的笔试题几乎清一色用的是Java包括上面说的并发、集合和设计模式。放到现在Kotlin已经在安卓开发里占据绝对主流协程替代了大部分回调式异步空安全和数据类让代码写起来顺手很多。所以如果你现在准备校招在Java基础上一定要补上Kotlin的知识协程的原理、Flow和LiveData的区别、Compose声明式UI等等。面试官拿一套2017年的Java笔试题来考你考察的初衷是逻辑和基础但你最好能在回答时主动把Kotlin的对应方案提出来展示你了解技术演进的脉络。6.2 架构层从MVC到MVVM再到现在2017年时MVP架构非常流行很多团队都在用Presenter把业务逻辑从Activity中抽离出来。那年的笔试题里如果有设计题往往也会要求你讲讲MVP和MVC的区别。但现在的面试题会更偏向MVVM、Jetpack Compose、单向数据流和ViewModel的生命周期设计。这个演变的核心原因是MVVM通过数据绑定把视图和业务逻辑做了更彻底的解耦且天然支持生命周期感知不用手动管理回调的销毁。不过基础的设计原则没有变单一职责、依赖倒置、解耦、可测试性。无论用MVC还是MVVM能把业务逻辑和数据源合理分层的人永远受欢迎。6.3 能力层性能优化与稳定性建设2017年的笔试题比较注重单个点上的优化比如内存泄漏、布局层级、图片加载。现在面试官更关注的是体系化的稳定性建设APM监控体系、崩溃治理、卡顿监控、启动耗时归因分析、流量分析、线上问题排查工具链。这背后反映的是行业对安卓工程师的要求已经从“会写功能”升级到了“会主持质量”。所以准备面试时不要只盯着笔试题本身还要想想笔试背后的时代背景。公司出的题目永远滞后于技术趋势但考察的能力模型是超前的。7. 从这套笔试题延伸出来的学习建议如果你正在准备安卓校招或者社招我的建议是第一不要只刷题。面试官出的题目可能和2017年的题目很像但追问往往比原题深得多。比如考Handler问完原理后一定会追问“主线程MessageQueue里有哪些消息类型”或者“IdleHandler是干嘛的”。这些代码细节只有真正读过源码才能答得上来。第二要整理自己的项目亮点。笔试只是敲门砖真正的面试环节一定会让你介绍最拿得出手的项目讲清楚你解决了什么问题、用了什么技术、遇到什么坑。项目不用多大但一定要准备充分。第三保持对新技术的敏感。2017年的安卓工程师不懂Kotlin还能活现在不懂协程和Compose就有点说不过去了。建议定期看Google I/O、开发者峰会的最新内容不用追求深挖但至少知道当前技术潮流是什么。第四手写代码的功夫要练。笔试题里手写单例、LRU、二叉树遍历这类题出现的频率很高。不用要求自己像编译器一样背代码但核心逻辑和边界条件一定要反应快、写得准。我自己的经验是准备校招那段时间每天花两个小时手写代码是基础操作。写完再对照标准答案做复盘把“为什么这么写”搞清楚收获会比你想象中的大。8. 写在最后这套2017年的挖财安卓笔试题放到今天来看已经不是最前沿的题库了但它反映了一个非常好的校招筛选逻辑不考死记硬背的API不考偏题怪题而是把安卓开发最基础、最核心、最能体现功底的内容拎出来通过层层追问去验证候选人是否真的理解了这个系统的运转原理。当你在准备校招时与其去寻找所谓的最新题库、压题技巧不如沉下心把这套基础框架吃透。Handler机制、Activity生命周期、事件分发、内存模型、线程并发、网络框架选型这些东西不会因为Kotlin和Compose的流行就消失它们只是换了层马甲继续留在安卓的底层。最后再分享一个小技巧做完一套笔试题后不要只对答案把每道题都当作一道面试题来准备自己问自己“如果面试官接着追问我还能答出什么”。这样做上三套题你会明显感觉到自己的知识体系变得完整了很多。面试能不能过其实在你准备的过程中就已经能预判了。