公司动态
爱奇艺2019秋招Android笔试题核心考点与复习路线解析
每年秋招Android岗的笔试题目都绕不开那几个老伙计Handler、线程池、事件分发、性能优化。爱奇艺2019秋招Android方向笔试题B我印象很深因为我当时把这套题当成了“考前摸底卷”认认真真做完一遍再去对比其他大厂的真题发现核心考点高度重叠。这套题不是什么偏题怪题它的价值在于考察面广、贴近实际开发很适合用来检验自己到底有没有真正理解Android的底层运行逻辑。如果你是正在准备秋招的应届生或者工作一两年想跳槽的Android开发我强烈建议你找一套类似的真题按考试节奏做一遍。做完别急着对答案先把每道题背后牵涉的知识链捋清楚再看自己在哪里卡壳。这篇文章我会把这一套题涉及的核心知识点、我当时踩过的坑、以及笔试后总结出来的复习路线完整写出来方便你对照着查漏补缺。1. 这套题想考的其实是这三层能力很多人刷笔试题喜欢背答案这是最亏的做法。笔试题目只是表象出题人真正想筛选的是三层递进的能力基础知识的覆盖面、原理理解的深度、以及解决实际问题的思路。爱奇艺这套B卷的题目设计恰好把这三层都踩到了。1.1 高频考点板块与建议权重分配先看整体考察范围。以当年的秋招行情来看Android笔试题基本可以划分成六个板块这套卷子也沿用了类似结构。我根据自己的做题经验给每个板块列一个建议复习权重考察板块常见题型建议复习权重Java语言基础选择题、输出题20%并发与多线程选择题、代码分析15%Android核心机制选择题、简答25%性能优化与稳定性简答、方案设计15%设计模式与代码题手写代码15%网络与数据存储选择题、简答10%别小看最后那10%的网络和数据存储很多人在这一块丢分丢得莫名其妙。比如HTTP与HTTPS的差异、TCP三次握手为什么不是两次、SQLite升级时怎么处理字段变更这些都是笔试常客。我后来复盘了一下真正拉开差距的往往不是难题而是这些“你觉得你会、但说不清楚”的基础题。1.2 B卷相比A卷的差异点爱奇艺这套B卷和同期的A卷相比明显更偏原理考察。A卷里有很多直接背结论就能选的题B卷则喜欢给你一段代码让你判断输出结果或者问“为什么这样写会有问题”。这就意味着如果你只是背了八股文做B卷会非常难受。举个例子关于单例模式A卷可能只问你“以下哪种写法是线程安全的”B卷则会给你一个双重检查锁的代码片段让你指出哪里有问题、volatile到底起着什么作用。题目本身不难但需要你真正理解Java内存模型知道指令重排和可见性的概念。所以我建议准备笔试时不要光看结论要把结论背后的“为什么”弄透这样无论题目怎么变形你都能应付。2. Java与并发基础笔试中丢分最狠的两个板块Java基础这块看似简单实际上涵盖了JVM内存模型、集合类源码、异常处理、泛型、反射等多个细节点。爱奇艺这套题里我印象最深的是关于HashMap的考点因为它一问就能问出一串连环题。2.1 HashMap的底层原理与并发问题HashMap在笔试题里出现频率极高几乎到了“十套题八套有”的地步。核心考点包括底层数据结构、put操作的流程、扩容机制、为什么线程不安全、以及和Hashtable、ConcurrentHashMap的区别。我更想强调的是底层数据结构的变化。JDK 1.7及之前的HashMap是数组加链表JDK 1.8之后引入了红黑树。当链表长度超过阈值8并且数组长度大于等于64时链表会树化。这个阈值8不是随便定的它基于泊松分布的计算结果在负载因子0.75的情况下链表长度达到8的概率已经非常低树化是为了应对极端情况下的哈希碰撞。面试官如果顺着HashMap继续问ConcurrentHashMap你要能说出JDK 1.7的Segment分段锁和JDK 1.8的CAS加synchronized有什么区别。这个知识点在笔试简答题中也常见不要只答“线程安全”要答出锁的粒度变化和性能提升的原因。2.2 equals与hashCode的约定这个考点单独拿出来说是因为它经常藏在代码输出题里。题目会给你一个自定义类重写了equals但没有重写hashCode然后把它放进HashSet或HashMap中让你判断结果。很多人在这里翻车原因是只记住了“重写equals必须重写hashCode”这个结论却不理解背后的逻辑。规则其实很简单如果两个对象equals返回true那么它们的hashCode必须相等。反过来不成立hashCode相等不代表equals返回true。HashMap和HashSet依赖hashCode定位桶的位置依赖equals确认对象是否相同。如果你只重写equals而不重写hashCode两个逻辑相等的对象会被分到不同的桶里导致HashMap中出现“两个key值相同但对象却存在”这种诡异情况。2.3 线程池参数与拒绝策略线程池是并发题里的重头戏。笔试很少让你背ThreadPoolExecutor的构造方法参数而是给你一个场景让你判断该用哪种线程池或者分析某个线程池的队列满了之后会发生什么。核心参数有七个核心线程数、最大线程数、空闲线程存活时间、时间单位、任务队列、线程工厂、拒绝策略。很多人能背下来但理解有偏差。比如核心线程数和最大线程数的关系很多人以为任务来了就直接加到最大线程数实际上线程池的处理逻辑是先判断核心线程数是否已满如果没满就创建线程执行任务满了之后新任务先放进队列排队队列也满了才会尝试创建新线程直到最大线程数实在不行才会触发拒绝策略。我建议你亲手写一个ThreadPoolExecutor把各个参数打印出来观察一遍特别是LinkedBlockingQueue和SynchronousQueue的区别。SynchronousQueue不存储任务而是直接把任务交给线程如果当前没有空闲线程就会立即创建新线程所以Executors.newCachedThreadPool用的是它适合大量短时任务但并发量高时会频繁创建线程开销很大。2.4 JVM内存模型与GC的常见问法JVM相关考点在Android笔试里通常不会考得太深但“Java内存区域”和“垃圾回收算法”是必背内容。我当时的复习方法是画一张内存区域的图把程序计数器、虚拟机栈、本地方法栈、堆、方法区各自存什么标清楚再对照Android的OOM场景去理解堆内存。GC的常考算法有三个标记清除、标记复制、标记整理。它们各有优缺点标记清除会产生内存碎片标记复制浪费一半空间标记整理效率较低。现代JVM的垃圾回收器大多是分代收集新生代用复制算法老年代用标记清除或标记整理。Android的ART运行时和传统JVM不完全一样但笔试中一般还是按JVM的知识回答如果你能补充一句“ART在Android 8.0之后改用并发标记清除回收器”会显得你的知识面更广。3. Android核心机制Handler、Binder、组件与事件分发Android核心机制是整套笔试题的压舱石也是面试官最看重的部分。爱奇艺这套题在这一板块考察了Activity启动模式、Service生命周期、Handler消息机制、事件分发和自定义View等内容题量不小。3.1 Activity启动模式与taskAffinity的使用场景Activity的四种启动模式——standard、singleTop、singleTask、singleInstance——基本是送分题但想拿满分不容易。难点在于它们与taskAffinity的配合以及onNewIntent回调的触发时机。我遇到过一道题问的是“两个Activity分别设置了不同的taskAffinityA以singleTask启动B以standard启动点击通知从后台拉起A此时任务栈会发生什么”。这个场景在真实开发中很常见比如从通知栏跳转到某个页面不希望界面上堆积大量历史页面。理解taskAffinity的关键在于不同taskAffinity的Activity可以存在于不同任务栈而singleTask会先查找是否存在相同taskAffinity的任务栈并复用它。还有一个高频考点是“singleTop和singleTask有什么区别”。前者是栈顶复用后者是栈内复用。如果启动singleTask的Activity时它所在的栈里已经存在该Activity实例系统会把它上面的所有Activity出栈并回调它的onNewIntent。这个“把上面所有Activity出栈”的行为和“清除栈顶”很像但很多人会记混。3.2 Service两种启动方式与Binder原理Service这个考点在笔试里常以生命周期选择题出现。startService启动的Service生命周期是onCreate、onStartCommand、onDestroy不会回调onBind。bindService启动的Service生命周期是onCreate、onBind、onUnbind、onDestroy。两者可以共存先startService再bindService时需要同时调用stopService和unbindService才能销毁Service。Binder是Android进程间通信的核心也是很多人觉得难啃的硬骨头。笔试一般不会让你写AIDL代码但会让你解释Binder相对其他IPC方式的优势。核心思路是Binder只需要一次数据拷贝而传统管道、消息队列需要两次拷贝性能更高同时Binder为每个进程分配了UID安全性更好。回答这个问题时把“一次拷贝”和“UID身份标识”这两个关键点答出来就基本达标了。3.3 Handler消息机制以及为什么会导致内存泄漏Handler几乎是Android笔试的必考题没有之一。考察内容包括Looper、MessageQueue、Handler三者的关系主线程为什么能无限循环而不卡死以及Handler造成内存泄漏的原因和解决方案。先梳理关系。每个线程只有一个LooperLooper内部维护一个MessageQueueHandler在发送消息时把Message插入MessageQueueLooper通过loop方法死循环取出Message并交给Handler的dispatchMessage处理。主线程的Looper在ActivityThread的main方法中通过Looper.prepareMainLooper创建然后调用Looper.loop进入循环。关于“为什么主线程无限循环不会卡死”这是一个很经典的追问点。答案是Looper.loop里的死循环并不占用CPU当MessageQueue没有消息时主线程会阻塞在MessageQueue.next的nativePollOnce方法上此时线程是睡眠状态不会消耗CPU资源。有消息到来时通过epoll机制唤醒线程。这个机制总结成一句话就是“没有消息就休眠有消息就唤醒”。内存泄漏的原因则是Handler持有外部Activity的引用如果Handler中有延迟消息在排队而Activity已经销毁消息队列仍然持有Handler的引用导致Activity无法被回收。解决方案有几种最有效的是把Handler定义成静态内部类使用WeakReference持有Activity引用同时在onDestroy中移除所有未处理的消息。3.4 事件分发机制三个方法要分清事件分发考察的是dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent这三个方法的职责。我见过最好的记忆方式是dispatchTouchEvent是总调度员决定把事件交给谁onInterceptTouchEvent是拦截器只有ViewGroup有onTouchEvent是最终处理者。笔试常考的一个问题是“子View的onTouchEvent返回false事件会怎样传递”。答案是事件会从子View回传到父ViewGroup的onTouchEvent如果所有View都不处理最终会传回Activity的onTouchEvent。这个“递归返回”的过程很多人画图能画明白一写代码就懵。我当时是自己在Demo里给每个方法加日志然后点击屏幕观察输出顺序做了几遍就彻底记住了。3.5 View的绘制流程三步走自定义View相关的笔试题通常会考measure、layout、draw这三个流程以及MeasureSpec的三种模式。UNSPECIFIED、EXACTLY、AT_MOST分别对应什么含义是必须背下来的。其中EXACTLY对应match_parent和具体数值AT_MOST对应wrap_content。如果题目让你自定义一个View并实现wrap_content需要注意一个坑如果不在onMeasure中处理wrap_content自定义View的宽高会默认和父容器一样大而不是根据内容大小来。原因很简单系统对wrap_content的处理取决于View是否自己实现了onMeasure逻辑默认实现等同于match_parent。很多新手自定义View时遇到的“wrap_content失效”根源就在这里。4. 性能优化与稳定性进阶题拉开差距的地方性能优化是Android工程师进阶的必经之路也是笔试简答题和方案设计题的重点。爱奇艺这套B卷在这方面给了不少篇幅考察内容包括内存泄漏、ANR、布局优化、Bitmap优化等。4.1 内存泄漏的典型场景与排查套路内存泄漏的考点除了前面说的Handler还有几个常见场景静态变量持有Activity引用、单例模式持有Context、非静态内部类创建了静态实例、资源未关闭、以及注册了监听器但没有反注册。笔试如果考内存泄漏的排查答案通常是配合LeakCanary。你要能说出LeakCanary的原理是基于WeakReference和ReferenceQueue检测到弱引用被回收之后说明对象没有被泄漏如果一段时间后弱引用还没有被加入引用队列说明对象可能被泄漏了。这时候再手动触发一次GC并dump堆内存分析引用链就能定位到泄漏路径。实际开发中我自己的排查步骤一般是这样先让用户复现内存暴增场景然后用Android Studio的Memory Profiler录制一段内存分配操作完功能后手动执行一次GC观察内存是否回落到操作前的水平如果明显偏高就dump一份Java heap文件用MAT或者Profiler自带的分析工具查看大对象和泄漏嫌疑。这套流程看起来简单但能解决90%的内存泄漏问题。4.2 ANR的类型与定位方法ANR在笔试中的问法通常是“什么情况下会触发ANR怎么定位”。触发场景有三个分别是输入事件5秒未响应、广播前台10秒后台60秒未完成、前台服务20秒后台200秒未完成。这个数据在历年笔试中出现过多次值得死记硬背。定位ANR的方法是查看/data/anr/traces.txt文件但在Android 8.0之后traces文件位置和格式有所变化可以通过adb bugreport来抓取。关键是理解ANR的本质原因主线程被耗时操作阻塞或者主线程被其他进程的Binder调用长时间占用。我在实际项目里遇到过一个比较隐蔽的ANR主线程没有明显的耗时操作但频繁做了大量的文件读写由于CPU被IO抢占导致输入事件得不到及时处理。这种情况用trace文件不太容易一眼看出来需要结合CPU Profile进一步分析。4.3 布局优化与Bitmap内存占用计算布局优化最常见的两个工具是ConstraintLayout和include/merge标签。2019年的时候约束布局已经逐渐普及笔试中会问“为什么ConstraintLayout比传统多层嵌套布局性能好”。答案是减少View层级层级越深measure和layout递归越耗时也更容易在GPU渲染时造成过度绘制。Bitmap的内存占用计算方式是一个高频计算题内存大小等于图片宽度乘以图片高度乘以每像素字节数。ARGB_8888格式每个像素占4字节RGB_565占2字节ALPHA_8占1字节。举个例子1920x1080的图片用ARGB_8888加载内存占用是192010804约8.29MB。如果不做压缩直接加载很容易造成OOM。笔试可能会问你inSampleSize的采样率怎么选回答时要讲清楚inSampleSize只能取2的整数次幂而且系统会向下取整到最接近的2的幂。5. 手写代码题题题见功底代码题在笔试中的占比一般不高但一旦出现就是决定能否进入面试环节的关键。爱奇艺这套B卷的代码题偏向基础和实用没有特别偏门的算法但写起来非常考验代码功底。5.1 单例模式的双重检查锁写法单例模式是代码题中的“Hello World”但能完全写对的人不多。笔试时最容易出错的点就是漏写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第二步和第三步可能被指令重排导致一个线程先拿到了引用却发现对象还没初始化完成从而出现空指针。volatile可以禁止指令重排并且保证可见性。笔试如果问你“有没有更简单的线程安全写法”可以回答静态内部类方式。它利用JVM的类加载机制保证初始化时的线程安全没有同步开销也是我日常工作中更推荐的方式。5.2 生产者消费者模型考察线程协作基本功另一个常见代码题是生产者消费者模型。考察点包括wait和notify的配合使用、Lock和Condition的实现方式以及对阻塞队列的掌握程度。用synchronized加wait和notify的写法是最基础的版本但需要注意wait必须在循环中使用因为线程可能被虚假唤醒。这个细节很多人都知道但手写时容易写成if。用ReentrantLock加Condition的版本更清晰也更容易让面试官看出你对并发包的理解。笔试时间紧张如果你只打算记一种写法我建议优先掌握BlockingQueue版本。它几行就写完逻辑清晰不易出错而且能展示你对Java并发工具类的熟练度。当然如果能再写出Lock和Condition的版本会在面试中加更多分。5.3 高频数据结构题链表反转与LRU缓存链表反转属于很基础的算法题递归和迭代两种方式都要会。更值得重视的是LRU缓存的手写因为它涉及HashMap和双向链表的结合能考察数据结构和业务场景的结合能力。LRU题目的要求一般是“设计一个数据结构支持get和put操作时间复杂度为O(1)当缓存满时淘汰最久未使用的数据”。手写时容易犯两个错误一是直接用LinkedHashMap实现代码虽然简单但如果题目要求考察原理需要写出底层结构二是双向链表的节点更新逻辑没理清出现断链问题。我的建议是把LinkedHashMap的构造方法参数accessOrder设为true这个知识点记住并理解它为什么能实现LRU。面试官如果追问原理你再讲HashMap加双向链表的实现思路就非常稳了。6. 实战避坑清单与复习路线最后这部分我根据自己的实战经验把笔试中容易翻车的细节和复习时需要注意的问题整理成一个速查清单方便你在考前快速过一遍。6.1 笔试中容易翻车的三个细节第一个细节是代码输出题里的字符串比较。很多题目喜欢考String的和equals区别尤其是String s1 aaa和String s2 new String(aaa)同时出现时s1 s2是false因为一个是常量池对象一个是堆对象。这种题属于送分题但越是送分题越容易粗心。第二个细节是Activity的onSaveInstanceState调用时机。它不一定在onStop之前调用而是在Activity即将被销毁且有机会被恢复时调用。系统在内存不足杀死后台Activity之前会调用它保存状态这个时机和onPause、onStop没有绝对的先后关系。笔试题有时会故意把顺序写错诱导你选错。第三个细节是AsyncTask的串行与并行问题。在Android 1.6到3.0之间AsyncTask是串行执行的3.0之后默认也是串行但可以通过executeOnExecutor改为并行。2019年的笔试题还在考这个点因为它很能体现你是否有实际开发经验而不是只看过新版本文档。6.2 可持续使用的复习路线以我个人的经验来看刷笔试真题不能只图数量要按“知识点—原理—应用”三层递进。第一遍做套题时先按考试时间完成不管对错第二遍逐题分析背后的知识点把每道题涉及的原理写一遍第三遍重点做错题并且把错题转化为自己的知识清单。如果时间充裕我建议在笔试前一周把Handler消息机制、自定义View绘制流程、Activity启动模式、线程池参数这几个核心模块用几句话向别人讲一遍。能讲清楚说明你是真的理解了而不是停留在背结论的阶段。6.3 面试进阶还可以沿着这个方向深挖如果你笔试过了准备面试建议在现有基础上再延伸几个方向一是Jetpack组件库的具体使用和原理比如LiveData和Lifecycle如何实现生命周期感知二是Kotlin协程与线程池的对比为什么协程更轻量三是性能优化实战案例准备一个你亲手解决过的线上问题从问题定位到修复再到验证描述得越具体越好。爱奇艺这套题虽然是2019年的但这些方向到今天依然是Android面试的核心主线值得花时间继续深挖。最后说一点个人的体会。笔试刷题最忌讳的就是“我见过这个题”的错觉真正到了考场上题目稍微换个角度很多人就露馅了。我自己当时把爱奇艺这套B卷做了三遍每一遍都能发现新的知识盲区。前两遍做的时候我都是老老实实把涉及到的源码翻出来看把关键方法的实现逻辑记下来而不是只看答案。这个过程很花时间但效果确实好。希望这套复盘思路也能帮你在今年秋招里少走一些弯路。