
2018年那个秋天我正在准备Android校招的冲刺阶段刷到京东这套笔试题的时候第一反应是“稳了”——倒不是题目简单而是它的出题风格非常典型几乎把大厂Android岗笔试喜欢考的东西都串了一遍。Java基础、四大组件、消息机制、性能优化、手写算法一个没落下。后来我身边好几个同学也做了这套题普遍反馈是选择题有坑简答题考理解编程题考基本功整体区分度很高。这篇文章我打算把京东2018秋招Android工程师笔试题里最值得反复琢磨的考点结合我自己的复习笔记和面试复盘逐个拆开来讲。不管你是正在准备春招秋招的应届生还是工作了一两年想跳槽的Android开发这套题背后的知识点都值得认真过一遍。我会把每类题型的考察意图、核心原理、常见坑点和答题思路都整理出来尽量让这篇文章能直接当复习提纲用。1. 题目风格与出题思路解读1.1 2018年这个时间点的Android考察风向先说说这套题出现的行业背景。2018年正是小程序和跨端方案开始冒头的阶段Flutter还没大范围普及RN已经在部分大厂落地但原生Android依然是招聘主力。这个时间点的大厂笔试有一个很明显的特点不考新框架的API用法而是考底层原理和基础功。原因很简单新框架迭代太快今天考了明天可能就换了但Java基础、操作系统、网络协议、Android核心机制这些是十年都不会变的底盘。京东这套题也遵循了这个规律。你翻完整张卷子会发现它几乎没有问“某某框架的某个API怎么调用”这种问题而是反复从底层机制、源码实现、异常排查这些角度去考。这说明出题人想筛选的不是“用过什么”而是“理解了什么”。所以你在复习的时候如果只背API和框架用法做这套题会非常吃力反过来如果你能把Handler的消息循环机制、HashMap的扩容逻辑、Activity的启动流程讲清楚这套题做起来就会顺很多。1.2 整份卷子的题型布局与分值分布从题型结构上看京东这套题基本沿用大厂笔试的经典配置单选多选题、简答题、编程题部分批次还加了设计题。选择题主要覆盖Java基础、并发编程、Android组件、网络协议这些硬知识点考法很细经常会在边界条件和特殊返回值上做文章。简答题则集中在Handler机制、进程通信、性能优化、View绘制这几大块要求你不仅知道结论还要能讲清楚过程和原因。编程题以LeetCode中高难度为主常见的有链表操作、二叉树遍历、动态规划偶尔会出一道与Android业务场景结合的设计题比如图片加载框架的设计、线程池的设计。分值分布上选择题和编程题通常是大头简答题次之。这里有个容易被忽略的点笔试不仅仅是看总分有些公司会卡单项比如编程题如果零分即使选择题全对也可能直接挂掉。所以如果你时间有限编程题无论如何都要保证能写出一道完整的题。1.3 什么样的考生容易在这套题上拿高分我后来和几个通过京东笔试的同学交流发现高分的共性不是刷题量多而是知识体系完整。具体来说他们能做到“给一个知识点能当场画出它的完整链路”。比如讲到Activity不只是背四种启动模式而是能从startActivity开始到AMS的调度再到生命周期回调把整条链路复述出来。再比如讲到线程池能现场推导出不同核心线程数、任务队列长度下的执行行为。这就引出这套题真正的筛选逻辑它不考你记住了多少独立的知识点而是看你能否把散落的知识点连成一张网。笔试中的简答题和设计题本质上都是让你展示这张网。所以我的建议是复习时不要按知识点列表一个个背而是按“一条主线串到底”的方式去学。这个思路后面我会在每个考点里具体演示。2. 高频考点一Java基础与并发编程2.1 equals和hashCode为什么总被放在一起问Java基础部分京东这套题几乎必考equals和hashCode的关系。这个知识点看似简单但每年的错误率都很高。核心考点有两个一是equals相等的两个对象hashCode必须相等二是hashCode相等的两个对象equals不一定相等。前者是规范后者是数学上的必然因为hashCode的取值空间远小于对象的可能集合。很多候选人能背出这两条规则但一落到代码就出问题。常见错误是在重写equals时没有重写hashCode导致对象放入HashSet或HashMap后出现“存得进去、取不出来”的情况。举个实际场景你有一个User类只重写了equals方法比较的是id字段。你把两个id相同的User对象先后放入HashSet第一个先放入第二个再放入时equals确实返回true但HashSet先调用hashCode判断桶位置两个对象的默认hashCode不同所以它们进了不同的桶Set里就会出现两个“逻辑相等”的对象。复习这个考点时我的建议是把HashMap的存取过程一起复习。当put一个键值对时先计算key的hashCode定位到桶再遍历桶内的链表或红黑树用equals判断是否有相同key。这个过程就能把hashCode和equals的分工讲清楚hashCode负责“快速定位”equals负责“精确匹配”。答题时能说出这一层分数立刻就不一样。2.2 HashMap源码级考点与JDK版本差异HashMap在2018年的笔试里几乎是必考题。京东这套题选择填空和简答都喜欢考它尤其是扩容机制和JDK 1.7与1.8的差异。先说基础HashMap的默认初始容量是16负载因子是0.75当元素个数超过容量乘以负载因子时触发扩容新容量是原来的两倍。为什么要取0.75这个值这是空间和时间的一个平衡点太高会导致哈希冲突概率增大太低会浪费空间。JDK 1.7到1.8的差异是高频中的高频。1.7的底层是数组加链表头插法扩容时在多线程环境下可能出现环形链表导致get时死循环1.8改为数组加链表加红黑树尾插法当链表长度超过8且数组容量大于等于64时链表转为红黑树。这里有个容易被问到的细节为什么是8因为泊松分布下负载因子0.75时链表长度达到8的概率已经低到千万分之一级别用8作为一个阈值既保证正常情况下链表够用又能在极端哈希冲突时通过红黑树保证性能。选择题里经常出现的坑是把初始容量和实际容量搞混。比如问new HashMap(10)的实际容量是多少答案是16因为HashMap会向上取2的整数次幂。原理是哈希值对容量取模运算时用位运算hash (cap - 1)替代取模要求容量必须是2的幂这样cap - 1的二进制位全为1能充分利用哈希值低位减少碰撞。这个设计细节非常经典面试官问“为什么容量是2的幂”时你一定要答到这一层。2.3 线程池参数、执行流程与拒绝策略并发相关题目里线程池的考察频率极高。京东这套题一般会给一个ThreadPoolExecutor构造方法的场景问你核心线程数、最大线程数、任务队列长度分别怎么设置或者直接让你手写一个适合IO密集型和CPU密集型任务的线程池。先理清执行流程提交一个任务时如果当前线程数小于核心线程数创建新线程执行如果达到核心线程数任务进入阻塞队列如果队列满了创建新线程直到最大线程数如果线程数已达最大值且队列也满了执行拒绝策略。这是标准的四个阶段每一步都有对应的前提条件。线程数设置的估算原则是CPU密集型任务核心线程数设置为CPU核数加一或等于核数IO密集型任务因为线程会频繁阻塞等待IO可以设置更多常见公式是CPU核数乘以二或者核心线程数等于CPU核数除以1减去阻塞系数。阻塞系数越高需要的线程越多。答这类题时不要只背公式要能解释为什么IO密集型需要更多线程线程阻塞时不占用CPU可以创建更多线程去利用空闲的CPU时间片。拒绝策略也需要记住四种AbortPolicy直接抛异常、CallerRunsPolicy让提交任务的线程自己执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃队列中最旧的任务然后重试提交。选择题喜欢考这些策略的类名和行为建议记忆时把每种策略的适用场景也带上。实际开发中CallerRunsPolicy比较稳妥因为它在任务被拒绝时不会丢失任务只是把压力回传给调用方。2.4 volatile和synchronized的关键差异Java并发里volatile和synchronized的对比也是常客。volatile主要有两个语义保证可见性和禁止指令重排序。它不保证原子性所以像count这种操作即使变量声明为volatile多线程下依然会丢更新。synchronized则通过内置锁保证原子性、可见性和有序性但代价是线程阻塞和上下文切换。2018年的大厂笔试特别喜欢问单例模式的线程安全问题尤其是双重检查锁为什么需要volatile。代码里有这么一段instance new Singleton()这个操作在JVM层面对应三步分配内存、初始化对象、把引用指向内存。如果不加volatile第二步和第三步可能被重排序线程A先让引用指向了未初始化的内存线程B此时检查instance不为null直接返回拿到一个半初始化的对象。volatile禁止了这种重排序保证引用赋值一定发生在对象初始化完成之后。这个考点完美的把并发、JVM和设计模式串在一起值得反复练习。答这类简答题时有一个技巧先讲概念再讲机制最后举例说明。比如问volatile先说明它是什么、解决什么问题再讲内存屏障和缓存一致性协议最后用双重检查锁的例子说明实际应用场景。这样答案的层次感很清晰阅卷人容易踩点给分。3. 高频考点二Android核心机制与组件3.1 Activity启动模式与onNewIntent的触发时机Activity作为Android最核心的组件启动模式是笔试必考项。四种启动模式standard、singleTop、singleTask、singleInstance每种都要知道三个维度是否创建新实例、是否调用onNewIntent、与任务栈的关系。standard和singleTop的区别平时用得多也比较容易判断。standard每次启动都会创建新实例singleTop则是在栈顶复用。这里有个选择题高频坑如果singleTop模式的Activity已经在栈顶再次启动时走onPause、onNewIntent、onResume而不是onCreate和onStart。很多人误以为复用就不走任何生命周期实际上是要走onNewIntent的。singleTask和singleInstance的区分需要理解任务栈。singleTask启动时系统会先查找是否存在该Activity所在的任务栈如果存在就复用栈内实例并把该实例之上的所有Activity出栈同时回调onNewIntent。singleInstance更特殊它所在的Task只能有这一个Activity。这个模式下很容易出关于Task的题比如一个singleInstance的Activity被启动后按下Home键再点击应用图标回到的是哪个Activity答案是回到该singleInstance所在的Task而不是之前的Task。答这类题时我建议画一个任务栈的示意图来辅助判断虽然笔试不能画图但自己在草稿纸上画一下能减少很多逻辑错误。3.2 Handler消息机制从post到执行的全链路Handler机制是Android笔试里最核心的考点没有之一。京东这套题的简答题部分极大可能会出现“简述Handler、Looper、MessageQueue的工作原理以及Handler是如何实现线程切换的”。这个题能考察出候选人是否真正理解Android异步机制。全链路是这样的Handler在创建时会关联当前线程的LooperLooper通过loop方法进入死循环不断从MessageQueue中取消息。当你在子线程调用handler.post(runnable)或sendMessage时其实都是把Message添加到MessageQueue中Message持有你传入的callback目标。Looper取出消息后调用msg.target.dispatchMessage再由Handler的handleMessage或handleCallback处理。整个过程是生产者-消费者模式MessageQueue是线程安全的单链表入队和出队通过synchronized和native方法实现阻塞唤醒。线程切换的本质在于Handler在哪个线程创建就绑定哪个线程的Looper消息最终会在那个线程被取出和处理。子线程通过handler发送消息消息被添加到主线程Looper的队列中主线程的Looper取出并处理这就实现了从子线程到主线程的切换。如果想要其他线程能往主线程发消息只需要拿到主线程的Handler实例即可。这里有一个大坑主线程的Looper是在ActivityThread的main方法中通过Looper.prepareMainLooper和Looper.loop初始化的如果你在子线程new Handler必须自己先调用Looper.prepare否则会抛RuntimeException“Can‘t create handler inside thread that has not called Looper.prepare”。这个异常很多老手都遇到过笔试选择题也很喜欢考。另外还有两个细节值得注意一是MessageQueue的next方法在没有消息时会阻塞在nativePollOnce而不是忙等消耗CPU二是IdleHandler机制当消息队列空时会回调可以用来做启动优化里的延迟加载。这些进阶细节如果能在简答题里主动写出来会是很强的加分项。3.3 Service两种形态与Binder进程通信Service相关的题目京东这套题倾向于考查绑定服务与启动服务的区别以及Binder这个跨进程通信机制。先说Service的两种形态startService启动的服务主要用于后台执行任务与启动者无关即使启动者销毁服务仍在运行bindService绑定的服务与绑定者生命周期绑定绑定者销毁时服务会回调onUnbind并最终销毁如果同时被启动和绑定需要两者都解除才销毁。Binder是Android实现跨进程通信的核心机制。笔试常问的是“为什么Android用Binder而不是其他IPC方式”。角度主要有三个性能上Binder只需一次拷贝而传统管道、Socket需要两次拷贝安全上Binder为每个进程分配uid内核可以校验身份天然支持权限控制易用性上Binder被设计成面向对象的调用方式调用远程方法就像调用本地方法一样。AIDL是Binder的一种封装方式。笔试如果让你描述AIDL的工作流程可以从这几个方面展开定义aidl接口SDK生成对应的Stub和Proxy类客户端bindService后在onServiceConnected中通过ServiceConnection拿到IBinder再通过Stub.asInterface转换为接口对象。真正的数据序列化发生在线程池中Binder驱动的binder_thread_read和binder_thread_write完成数据拷贝这里如果能提到“Binder调用是同步的耗时操作需要放到子线程”就是一个增分点。3.4 View事件分发三个方法的分工与协作View事件分发在Android笔试中的出现频率同样很高。三个核心方法需要把职责理清dispatchTouchEvent负责分发onInterceptTouchEvent负责拦截onTouchEvent负责处理。事件分发的默认流程是这样的事件从Activity的dispatchTouchEvent开始传到ViewGroup的dispatchTouchEventViewGroup先调用onInterceptTouchEvent判断是否拦截如果不拦截则遍历子View调用子View的dispatchTouchEvent子View如果没有消费再回到ViewGroup自己的onTouchEvent。这里有个重要结论如果一个View的onTouchEvent返回false事件会向上传递给父View的onTouchEvent处理如果返回true表示消费了该事件后续事件序列将直接交给它。选择题的高频坑点是事件序列的概念。一个完整的事件序列从ACTION_DOWN开始到ACTION_UP结束。如果ACTION_DOWN没有被某个View消费那么后续的ACTION_MOVE和ACTION_UP都不会再传给这个View。还有很多候选人会混淆onTouch和onClick的关系onTouch优先于onClick如果onTouch中返回trueonClick不会执行。我在复习这个考点时的经验是不要只背结论要自己写一个自定义ViewGroup打印每个方法的事件日志实际跑一遍单指滑动、多指点击的场景。这个实验做完以后事件分发基本就不会再错了。4. 高频考点三性能优化、网络与存储4.1 内存泄漏与OOM的定位思路性能优化题目在京东这套题里占的比重不小尤其是OOM和内存泄漏的定位。这里有个常见误区很多人以为OOM是内存不够实际上OOM大部分是因为内存碎片化或单个对象过大而内存泄漏是对象无法被回收导致的内存被占用两者有关联但不是一回事。笔试简答题如果问“如何排查内存泄漏”比较标准的回答路径是先通过Android Studio的Memory Profiler观察内存曲线看是否存在内存只增不减的情况然后使用LeakCanary自动检测泄漏并定位到具体Activity或Fragment拿到泄漏路径后通过分析引用链找到是哪个对象持有了不该持有的引用。常见泄漏场景包括静态变量持有Activity、Handler延迟消息持有Activity、匿名内部类持有外部类引用、未注销的注册监听器、单例持有Context等。回答时最好能举一个具体的场景比如Handler导致Activity泄漏的经典问题以及对应的解决方案在onDestroy时removeCallbacksAndMessages。如果进一步问到OOM的解决思路可以从四个方面回答减少内存占用比如Bitmap用采样率加载、及时释放资源比如回收不再使用的Bitmap、使用更合适的数据结构比如用SparseArray替代HashMap、以及通过复用机制降低频繁创建对象的开销比如ListView的convertView复用。4.2 ANR的产生场景与分析方法ANR相关的题看起来简单但真正能答好的人不多。ANR全称Application Not Responding产生的原因是主线程在规定时间内没有处理完输入事件、广播或服务相关操作。具体类型要记住输入事件分发超过5秒、BroadcastReceiver前台广播执行超过10秒、后台广播超过60秒、前台Service执行超过20秒。笔试如果问ANR的排查方法关键点是抓取和分析trace文件。ANR发生时系统会把主线程的堆栈信息写入/data/anr/traces.txt通过分析这个文件可以看到主线程阻塞在哪里。常见原因有主线程做了耗时IO、数据库操作、网络请求、死锁等。另一种排查方式是通过logcat搜索ANR in关键字能快速定位到是哪个包名和进程发生ANR。要避免ANR核心原则是主线程只做UI操作耗时任务全部走子线程。这里需要注意子线程不能直接更新UI所以通常会用Handler或runOnUiThread把结果切回主线程。如果笔试考到“为什么子线程不能更新UI”要能从ViewRootImpl.checkThread方法和线程安全的角度回答UI访问不是线程安全的如果允许多线程更新UI需要加锁加锁会导致性能下降Android选择了单线程模型的方案通过checkThread强制校验线程。4.3 网络框架与图片加载的关键原理2018年的笔试中网络框架喜欢考OkHttp和Retrofit的原理。OkHttp的核心是拦截器链从应用拦截器、RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor、ConnectInterceptor到CallServerInterceptor每一层负责一件事。笔试如果让你设计一个网络框架你就可以参考这个链路设计请求封装、缓存策略、连接管理、数据解析每个环节都可以独立扩展。图片加载框架方面Glide是当时的重点。高频考点包括Glide的三级缓存策略内存缓存、磁盘缓存、网络加载以及为什么Glide加载图片比传统方案更优。Glide做了很多事情生命周期感知、默认使用RGB_565减少内存、根据ImageView尺寸做图片裁剪、支持加载Gif。如果被问到Bitmap的内存计算需要能答出来Bitmap内存大小等于宽度乘以高度乘以每像素字节数ARGB_8888是4字节RGB_565是2字节。一张1080乘以1920的ARGB_8888图片占用内存约为7.9MB这个计算题在笔试选择题里出现过。5. 编程题与设计题实战要点5.1 手写算法题的常见类型与模板京东这套笔试的编程题整体难度在LeetCode中等到困难之间。高频类型是链表的操作反转、找环、合并有序链表、二叉树遍历与深度计算、动态规划经典题目。以反转链表为例建议准备递归和迭代两个版本并且能说出各自的时空复杂度。迭代版本是O(n)时间和O(1)空间递归版本是O(n)时间和O(n)空间。二叉树这块层序遍历是高频题。用队列实现逐层输出每层开始时记录当前队列大小内层循环处理完这一层。如果笔试要求输出每一层的结果列表这种写法是最直观的。动态规划题目在笔试中通常不会太难台阶问题、背包问题、最长公共子序列这些都是常客。编程题的答题策略上我的体会是先把思路用注释写清楚再写代码。笔试阅卷时如果代码没跑通但思路清晰部分平台会给过程分。还有一个小技巧注意输入输出的边界处理链表题的空指针、数组题的越界、数字题的大数溢出这些都是隐藏的挂点。5.2 设计题图片加载框架的答题框架设计题是区分度最高的题型京东偶尔会出一两道。常见设计题包括实现一个图片加载框架、设计一个线程池、设计一个消息队列。答题框架比标准答案更重要我总结了一套万能结构需求分析、整体架构、模块设计、关键流程、异常处理。以图片加载框架为例需求分析要说到支持同步和异步加载、内存和磁盘缓存、防止OOM整体架构可以分为三层API入口层、任务调度层、缓存层关键流程要画出来请求图片时先查内存缓存再查磁盘缓存都没有则发起网络请求下载完成后按先后顺序写入内存和磁盘缓存。异常处理环节尤其能体现工程经验。比如图片加载失败后的重试策略、加载中显示的占位图、加载超大图时的采样率压缩、在ListView中快速滑动时取消已经不可见的加载任务。如果能提到用生命周期感知来取消请求这个答案的含金量会非常高。6. 备考经验与避坑清单6.1 我在复习这套题时走过的弯路复习阶段我踩过几个值得分享的坑。第一个是只刷选择题不动手写代码。选择题做多了会产生一种“我都会了”的错觉但一到手写编程题连反转链表都要调试半天。后来我强制自己每天在纸上手写两道算法题不看IDE提示效果立竿见影。第二个是忽视源码层面的理解。背了一堆结论比如“HashMap线程不安全”“Handler用于线程切换”但问到为什么的时候卡壳了。后来我把每个核心知识点的源码过了一遍不是逐行读而是看关键方法的流程类如HashMap的put方法、Handler的enqueueMessage方法、ActivityThread的handleLaunchActivity方法。看完源码再看笔试题很多“偏题”其实都是源码里的细节。第三个是没有做错题总结。笔试中有一类选择题是专门挖坑的比如问“new String(”abc“)创建了几个对象”答案是1个或2个取决于常量池是否已有该字符串。这个知识点我第一次做错了但没记录下来秋招后期做其他厂的题又碰到了又错了一次。建议准备一个错题本记录哪些题容易在哪个环节出错考前看一眼比盲目刷题效率高得多。6.2 笔试现场的时间分配与答题顺序时间分配上我个人的策略是“先编程后简答最后选择”。编程题分值高且需要完整思路放在前面可以保证在头脑清醒时完成简答题虽然要写的内容多但知识点熟悉的话写得很快选择题涉及大量细碎知识点容易卡住如果在一道选择题上超过2分钟建议先标记跳过。关于代码题的细节我想强调注释和变量命名的作用。阅卷场景下一个变量名清晰的代码比一个全用a、b、c命名的代码更容易得分。而且要记得先处理边界条件大部分情况可以降低时间复杂度到O(n)或O(nlogn)。最后说一个容易被忽略的点笔试结束后不要把题丢了。每道错题都值得去翻对应的源码或官方文档把它变成自己的知识盲区补充。我当时考完京东这套题把选择题里所有犹豫的选项都列了一个清单逐个去查证这个过程比刷十套题都有用。后来面试其他公司很多原题被我按同样的思路答出来了这是后话但足以说明这套题的价值不只在笔试本身。