最近翻旧电脑里的面试笔记,翻出一份2017年京东校招Android方向的主观题汇总。那年我还在一线做Android开发,从笔试、面试到帮部门整理题库都经历过一遍,对这些题的感情挺复杂。今天回看,最大的感受是:2017年前后Android校招的考察方式正好到了一个转折点——面试官不再满足于“某个API你用过没有”,而是反复追问“为什么这么设计”“你遇到问题怎么定位”“这个方案有什么代价”。主观题就是这种考察倾向的集中体现。这篇文章就以当年的题目为索引,把几大高频方向逐类拆解,讲清楚每道题背后的考察点、高分答题思路和容易翻车的细节。无论你是正准备校招的应届生,还是工作几年想跳槽的Android工程师,这套梳理都会对你有帮助。
1. 2017年那会儿,Android面试到底在考什么
1.1 当时的技术栈:Java为主、Kotlin刚冒头
2017年的Android生态放在今天看很有时代感。Android 8.0刚刚发布,后台服务限制、自适应图标这些新特性让不少老项目开始焦虑兼容成本;Kotlin 1.2刚发布没多久,虽然社区讨论热度在起来,但绝大多数团队的线上代码还是Java。网络层基本是Retrofit 2加OkHttp的天下,异步任务被RxJava 2.x拿下了半壁江山,图片加载则是Glide和Fresco各占一片阵地,Fresco在图片巨大、内存敏感的场景里优势明显,Glide以轻量和生命周期绑定收获了大量粉丝。
移动架构上,MVP已经成为团队标配的讨论话题,很多项目已经在用MVP重构旧代码。那一年Google在I/O大会上推出了ViewModel和LiveData,把MVVM从理论推向了工程落地,不少技术团队开始认真评估“要不要从MVP迁到MVVM”。组件化、插件化、热修复更像是一场军备竞赛,Tinker、Sophix、Robust各显神通,面试题里自然少不了“你了解插件化吗”这种问题。
这个技术背景下,校招考点的分布其实很明确。Java基础、Android四大组件、Handler、性能优化、架构演进,这些是硬核主干。但如果你只看面试书去背答案,很容易发现2017年的面试官已经开始追问“那底层是怎么实现的”“线上出问题你会怎么排查”,光记结论已经不够用了。
1.2 为什么笔试里开始塞主观题
客观题适合大规模筛选,但劣势也很明显:它可以轻松筛掉完全不会的人,却筛不掉背题库的人。一个候选人可以靠刷题记住“LruCache是什么”“ANR有哪几种”,但你要是在面试里追问“为什么用LruCache而不是FIFO,淘汰一个元素的时候要做什么”,很多人就露馅了。
主观题的价值恰恰在这里。它不给选项,让你在一个开放问题里自由组织答案,考察的是三件客观题测不出来的事:
- 第一,有没有一套属于自己的方法论。比如“设计一个图片加载库”,你是否能按链路从加载、解码、缓存、显示完整拆开。
- 第二,有没有真实的项目经验或者动手实验的经验。用词里的细节感骗不了人,做过和背过是两种完全不同的表达状态。
- 第三,能不能在表达里展现逻辑和取舍。主观题没有标准答案,面试官真正想看的,是你面对不确定问题时是否具备工程判断力。
京东2017年这轮Android校招的主观题,从我接触到的题库和面经反馈来看,大概集中在四类:架构设计、性能稳定性、系统机制、以及开放性设计。下面我按这四类一个一个拆。
2. 架构与设计题:不是让你写代码,是让你拆链路
架构设计题在主观题里最考验内功。很多同学拿到“从零设计一个图片加载库”这种题就懵了,觉得要么无从下手,要么只能背几个名词。其实这类题的答题节奏很重要,与其堆名词,不如沿着一条核心链路把问题讲完整。
2.1 图片加载库:从一张图到整条加载链路
“从零设计一个图片加载库”当年被问到的概率非常高。很多人一上来就答“三级缓存,内存、磁盘、网络”,这句话没毛病,但它只是骨架,不填血肉的话,面试官没法判断你是真懂还是只会背口诀。
我建议的回答顺序是:先定义核心流程,再逐层补细节。一张图片从加载到显示,本质上要经过“图片源的获取→解码与裁剪→缓存策略→线程调度→显示回调”这几步。其中图片源有三个层次:内存缓存、磁盘缓存、网络。你可以顺着这个流程,把架构拆成几个模块,比如负责发起请求和调度资源的Loader、负责缓存淘汰策略的Cache、负责Bitmap采样与复用的Decoder、负责管理子线程与主线程回调的Dispatcher、负责最终显示到ImageView的Display层。
接下来必须讲关键细节。比如怎么避免OOM,这是图片库绕不开的问题。你需要提到BitmapFactory.Options里的inJustDecodeBounds,它能让解码器先只读图片的宽高信息而不生成Bitmap,方便你计算采样比例。然后用inSampleSize做采样缩小,处理大图时还要考虑inBitmap做内存复用,把已经不再使用的Bitmap内存块交给新的解码使用。这里有个知识点容易混淆:采样和有损压缩是两回事,采样是减少像素数量,质量压缩是调整编码体积,图片库做缩略图时主要靠采样。
缓存策略也不能只讲LruCache的名字。你得说清为什么内存缓存用LruCache而不是普通Map——LruCache基于LinkedHashMap的accessOrder机制,能在每次访问后把最近使用的条目放到队尾,当缓存满时优先淘汰最久没被访问的条目。磁盘缓存一般参考DiskLruCache,多维护了一个journal文件来记录访问和删除操作。能把这个机制讲清楚,面试官会认为你确实研究过缓存实现,而不只是知道依赖里有个Glide。
还有一个隐藏加分点:生命周期绑定。Glide为什么能感知Activity和Fragment的生命周期?因为它会向当前Activity的FragmentManager注入一个透明的SupportRequestManagerFragment,利用Fragment生命周期回调来暂停和恢复图片请求。这个设计在2017年算是个“进阶彩蛋”,说出来了,整道题的答案质感会明显不一样。
2.2 MVP和MVVM:别急着站队,先给对比维度
“MVP和MVVM的区别,项目里你会选哪个”是当年架构题的另一座大山。注意,2017年大家讨论的不是Jetpack Compose,也不是Flutter,而是最朴素的架构模式选择问题。但正因为朴素,回答时才更要小心。
很多人容易一上来就表态“MVVM更好,因为它数据驱动”。这种站队式回答在面试官眼里其实很危险,因为你暴露了自己缺乏对比意识。更聪明的做法是,先拉出几个对比维度,把两种方案放在维度上比较,最后再落到实际项目约束里给出选择。
第一个维度是职责划分。MVP里的Presenter是控制器,持有View接口,手动管理页面逻辑,坏处是接口数量爆炸,每多一个页面交互就要多维护一组View接口。MVVM里ViewModel对外暴露LiveData或ObservableField,UI层通过观察者模式自动更新,接口少了,但数据驱动的心智模型需要团队适应。第二个维度是可测试性。MVP的Presenter可以在纯JVM环境里做单元测试,但需要mock大量View接口;MVVM的ViewModel天然不依赖Activity,测试更轻。第三个维度是生命周期安全。这也是当年Architecture Components刚推出时最大的卖点:LiveData能感知生命周期,Activity销毁后自动停止派发数据,不会出现“页面关了,回调还更新UI导致崩溃”的老大难问题。
我这里建议你千万不要说“无脑选MVVM”。正确的落点是:如果是复杂页面、交互频繁、需要大量单元测试,我会选MVVM;如果是一个快速迭代的小模块,团队又对数据驱动不熟悉,那MVP甚至MVC都完全够用。关键不是哪个架构更先进,而是团队能不能形成统一的约定。这种“没有银弹”的态度比单纯吹某个模式要可信得多。
2.3 组件化与路由:解耦后模块之间怎么说话
组件化在2017年同样是高频话题,对校招生来说偏难,但答好了非常出彩。主观题通常这样问:“如果你们App要做组件化改造,模块之间怎么通信?为什么用路由而不是直接startActivity?”
你要先点出问题背景:模块化拆分后,模块A要跳转模块B的页面,但两者不能形成编译期依赖,所以不能直接写“startActivity(new Intent(this, BActivity.class))”。这个问题的标准解法是路由。路由的核心是“把页面跳转抽象成路径字符串”,用一个运行期注册表保存“路径到页面类”的映射关系,跳转时通过路径找到对应Activity,同时支持参数注入、拦截器、降级策略。你可以提一下ARouter,也可以说你自己实现过一个简单Router:注册表用Map存路径和Builder的对应关系,navigation方法负责统一入口,拦截器可以在登录校验、埋点上报这些场景里做统一处理,降级策略则负责在找不到页面时跳转到WebView或给用户一个友好提示。
只答路由还不够完整,组件化通信还有一个经典方向是接口下沉:把公共接口定义下沉到底层基础库,具体实现由各业务模块完成并注册到服务管理器里,调用方通过接口获取服务实例。这本质上是一个服务定位器,2017年的AppJoint、ServiceManager都是这个思路。
这道题最容易踩的坑,是把组件化讲成“把代码按功能分模块”就结束了。你要多讲“边界划分”“资源冲突”“重复依赖治理”“组件单独调试开关怎么做”。单靠一个路由解决不了所有问题,工程治理才是组件化真正的挑战。面试时能说清这些,说明你真的在项目里踩过坑。
3. 性能与稳定性题:答案先要有定位思维
性能优化题几乎每年必考,但很多同学在这类主观题上丢分,是因为答案太像教科书——列一堆优化手段,却看不出你实际解决问题的过程。面试官想听的是“现象→定位→根因→修复→验证”的完整闭环,而不是“我们可以用LeakCanary”这种一句话方案。
3.1 内存泄漏:Handler作为经典入口
如果问“你遇到过哪些内存泄漏,怎么解决的”,先别急着把七个场景全背出来。挑一个最有代表性的场景,把它从头到尾讲透,效果远好于蜻蜓点水。
Handler就是最典型的一个。为什么Handler容易泄漏?因为非静态内部类持有外部类的隐式引用。使用时你经常在Activity里new一个Handler,然后发送一个延迟消息到MessageQueue。问题是:MessageQueue里的Message通过target字段持有Handler,Handler又隐式持有Activity,这就在系统还没处理这条消息时,把Activity和整个View树都钉在了内存里,导致Activity无法被回收。
怎么定位?LeakCanary的原理你可以提一嘴:它在Activity.onDestroy之后用弱引用观察Activity对象,通过ReferenceQueue判断对象是否真的进入回收流程,如果没回收,就主动触发GC并分析引用链,最终把“谁持有这个Activity导致它无法释放”的路径显示出来。你能讲清楚这个原理,面试官就知道你用过LeakCanary,而不只是集成过。
怎么修复?两个方案配合使用:一是把Handler声明为静态内部类,并对外部Activity使用弱引用;二是在Activity.onDestroy时调用handler.removeCallbacksAndMessages(null),把队列里所有未执行的消息清掉。这里有个细节值得补充:静态内部类为什么相对安全,是因为它不再隐式持有外部类;弱引用是兜底,真正的关键是取消那些还没执行的消息,不然弱引用也救不了已经被消息队列持有的target链路。
除了Handler,内存泄漏还有一些高频场景:单例传入Activity当作ApplicationContext使用(单例生命周期长,一直持有Activity导致页面无法回收);注册了系统服务或者广播没有在onDestroy里注销;网络请求回调持有Activity(比如OkHttp回调是匿名内部类,在页面销毁后线程仍执行回调);自定义View中动画未停止等。每个都能用“泄漏路径→修复手段”这个结构去答,比单纯罗列场景要有说服力。
3.2 ANR:阈值只是入场券,定位才是正题
“什么是ANR?如何定位?”这道题很多同学以为背完“Activity 5秒、BroadcastReceiver 10秒、Service 20秒”就过关了,结果面试官立刻追问“那线上碰到ANR,你怎么查”?这就卡住了。
我对这道题的理解是:ANR的本质是系统通过超时机制,兜底判断主线程是否长时间无法响应用户操作。Activity输入事件分发超时、广播处理超时、Service执行超时是三类典型场景,数值可以记忆,但更要理解超时背后的意图——主线程被某件事阻塞太久了,必须有一个机制让系统“弹窗”而不是无限等下去。
定位手段要按工具链来回答。拿到logcat之后,去读系统输出的traces信息,这个文件在不同版本上路径不一样,但通常可以在/data/anr目录下找到,或者通过bugreport导出。重点看主线程(main)的栈顶函数卡在哪:卡在某个IPC等待,就要看Binder调用在等谁;卡在锁等待,就要看锁被哪个线程持有;卡在SharedPreferences的apply或者文件读取,说明主线程做了耗时IO。能顺着栈定位到具体线程,基本就找到了根因。
常见的ANR根因也要准备:主线程里做了文件读写或数据库操作、广播回调里执行耗时任务、主线程排队等待一个长时间运行的锁、Binder调用调用远程服务超时。如果面试官接着问预防措施,你可以说:用StrictMode在开发阶段发现主线程IO;对耗时可预见的任务做埋点,比如在Choreographer回调里统计主线程消息执行时长;项目里规范网络、数据库操作必须走子线程,用HandlerThread、AsyncTask或协程(2017年还没有协程,可以说线程池)。
这道题还有个加分思路:把ANR和卡顿放在一条线上理解。主线程消息执行时间过长,先是掉帧,用户感知为“卡”,如果继续恶化到系统判定超时,就会弹出ANR。这样回答就不只是介绍ANR,而是把系统机制关联起来,显得格局更大。
3.3 卡顿:别把性能优化八个方向全倒出来
“App滑动不流畅,你会怎么排查?”这是一道非常典型的开放型主观题。它的陷阱在于:很多人会把“卡顿优化”答成“性能优化大全”,把启动、内存、电量、网络全堆上来。面试官问的是卡顿,你就应该围绕帧渲染和主线程这条主线展开。
第一层是理解卡顿的本质。屏幕每16.6ms刷新一帧,如果一帧的生产时间超过这个阈值,就会掉帧,掉帧多了用户体感就是卡顿。所以排查思路的第一步,是确认掉帧发生的位置:是主线程CPU占用过高导致的逻辑计算耗时,还是布局绘制太慢,还是GC频繁导致线程挂起。
第二层是使用工具验证。我建议的回答主线是:先用Systrace抓一段trace,看主线程在每一帧里执行了哪些耗时Task,区分是CPU调度问题还是代码问题;再用TraceView或CPU Profiler对可疑方法做采样分析。如果你是现在准备面试,可以说Perfetto,但2017年面试时Systrace还是主力,能说一句“我抓过Systrace,看主线程有一段长时间执行了JSON解析”会非常加分。比这更重要的是“先有假设再上工具”的思维,而不是打开工具盲目点来点去。
第三层是具体优化手段,这层不必贪多,挑两三条和你经验最匹配的展开就够了。比如布局优化:减少层级堆叠,用ConstraintLayout减少嵌套,用merge合并根节点,用ViewStub延迟加载不常显示的模块。比如内存抖动:主线程频繁创建小对象会让GC频繁触发,GC的stop-the-world会挂起线程,导致帧来不及渲染,监控到GC密集后,要定位是哪里在循环里创建了对象。又比如过度绘制:打开开发者选项的“显示布局边界”,看是不是有多层背景色叠在一起,避免在父布局和子View同时设置同色背景。
这里分享一个我自己踩过的坑:当年我遇到一个页面列表滑动首帧明显掉帧,第一反应是去优化列表Adapter里的item xml层级,结果优化半天效果不大。后来用Systrace一看,掉帧点根本不在layout,而在主线程等一个锁,锁是线程池里一个图片下载任务在前面占着。所以卡顿优化,工具定位一定优于凭感觉猜。这个经验讲给面试官听,他会觉得你是个真正解决过线上问题的人。
4. 系统机制题:主观题更爱问底层为什么
系统机制题在校招里本来就必考,但主观题的考法比客观题狠得多。客观题可能只是“Looper在哪创建”,主观题会直接说“聊聊Handler机制,顺便解释为什么主线程的Looper是死循环却不会ANR”。答得好的人,一定对源码有概念,而不只是背过面经。
4.1 Handler消息机制:死循环和epoll的相遇
Handler这套机制由Handler、Looper、MessageQueue三个类组成。Handler负责发送和接收消息,Looper负责循环取消息,MessageQueue是消息的存储队列。你可以这样理解:Handler.sendMessage把Message按时间顺序插入队列,Looper.loop()用一个死循环不断从队列里取消息,取到就交给Handler的dispatchMessage处理,处理完再取下一条。
关键在MessageQueue.next()这个方法的阻塞机制。当队列里暂时没有消息时,next()不会让线程空转,而是会进入阻塞状态:如果有延迟消息,就计算一个距离执行时间还剩多久的阻塞时间;如果没有任何消息,就无限阻塞,等待被唤醒。它内部调用的nativePollOnce,在Linux层使用的是epoll机制。也就是说,主线程在空闲时是让出CPU陷入睡眠的,不是占着CPU疯狂空转,这就是为什么主线程Looper死循环不会导致CPU耗尽。
顺着这个逻辑往下推,ANR的真正原因就不是“Looper是死循环”,而是“loop()抽到了一条消息,但这条消息的处理耗时长到超时”。比如你主线程执行了一个10秒的SharedPreferences读取,队列里后续的触摸事件没法及时被取出处理,系统发现输入事件长时间没有回调,才判定ANR。把“死循环让出CPU”和“消息处理超时导致ANR”这两件事分清,这道题基本就是满分答案。
如果想再进一步加分,可以补充两个源码细节。一个是Message在取出处理后,会把msg.next重置成null,避免消息复用时链表串链;另一个是Looper在退出时,消息队列的next()会返回null来终止循环。这些细节不需要背源码行号,只要能说出来,面试官会觉得你确实读过。
4.2 Activity、Window、View:一套完整的事件链条
“Activity、Window、View三者的关系”和“一次触摸事件是怎么分发的”经常合在一起考,因为它们本来就是同一套体系。你不需要分开答,可以一起讲。
核心概念是这样的:Activity负责生命周期和业务交接,但它本身不画界面。真正承载界面的抽象是Window,具体实现是PhoneWindow;PhoneWindow里有一个DecorView,它是整个视图树的根View;ViewRootImpl则负责把View树和WindowManagerService对接,驱动measure、layout、draw这套流程。简单说,Activity是控制器,Window是窗口抽象,DecorView是根布局,ViewRootImpl是连接系统服务和View树的桥梁。
事件分发可以顺着责任链讲:手指按下时,事件先到Activity,转交给Window,再传到DecorView,然后从ViewGroup开始一层层向下分发给子View。这里涉及三个方法:dispatchTouchEvent负责分发,onInterceptTouchEvent是ViewGroup独有的拦截方法,onTouchEvent负责真正消费事件。如果子View的onTouchEvent返回false,事件就会回溯到父ViewGroup的onTouchEvent,再不行就传回Activity的onTouchEvent,这条链叫责任链。
还有个细节很关键:整个事件序列(DOWN、MOVE、UP)并不是每次分发都从顶层重新走一遍。DOWN事件确定了事件的目标View后,后续MOVE和UP会优先发给同一个View,除非中途被父ViewGroup拦截。这说明你理解事件序列的处理逻辑,而不是单独背了某个方法。另外可以补一句:ViewGroup的onInterceptTouchEvent不一定每次都要拦截,只在需要抢占事件时才返回true,比如列表滑动和item点击的冲突处理,这也是面试里常延伸的场景。
4.3 Binder和并发:从原理到典型的Android场景
Binder是Android系统机制里的大头,但校招主观题一般不会直接问“Binder在内核里怎么实现”,更多是“说说你对Binder的理解”“为什么Android选Binder而不是Socket或共享内存”。
我的回答习惯是:把Binder理解成一个跨进程的“快递员加共享仓库”模型。发送方要传数据时,先把数据从自己的用户空间拷贝到内核空间,接收方通过内存映射机制共享这块内核空间,省去了一次接收方侧的额外拷贝,所以Binder跨进程只需要“一次真实拷贝”。和Socket、管道、共享内存相比,Binder在传输性能、安全校验、稳定性之间取了一个平衡。特别是身份标识机制,系统可以识别调用方UID,用于权限校验,这是Binder在系统服务里大面积使用的重要理由。2017年的面试里你不一定需要把内核细节讲多深,但“一次拷贝”和“安全校验”这两个关键词必须有。
并发部分,高频主观题包括“volatile和synchronized的区别”“在多线程里怎么保证数据一致”。很多人只记住“volatile保证可见性,不保证原子性”,但缺少Android场景的绑定。我建议这样答:volatile主要用于状态标志、DCL单例的场景,它通过禁止指令重排来避免“半初始化对象被其他线程拿到”;synchronized用于临界区保护,同时提供可见性和原子性,但代价是锁竞争可能带来阻塞。在Android里,还要知道有些组件天生自带串行队列,比如HandlerThread和IntentService,它们通过单线程消息队列把并发请求串行化,反而避开了锁的问题。能在答案里体现“不是所有并发问题都要上锁”的工程判断,这道题就有区别度了。
5. 主观题怎么答,才能让面试官记住你
看完了这么多道题,你会发现主观题虽然千变万化,但答题结构其实可以训练。最后这一章聊聊这些年我总结的主观题表达经验,同样适用于社招。
5.1 三段式结构:定义、设计、边界
遇到任何主观题,我建议心里按“定义与背景→方案与设计→边界与取舍”三段来组织。这不是僵化的模板,而是一个稳定输出的逻辑框架。
第一段,用一两句话精准定义问题。比如问“什么是内存泄漏”,你可以说“生命周期较短的对象的引用,被生命周期更长的对象持有,导致该对象无法被GC回收,这就是泄漏”。定义越准确,越能呈现你的知识边界。第二段,展开你的方案或设计。这里最忌讳想到哪说到哪,一定要沿主线走。问图片加载库就按加载链路讲;问卡顿就按“掉帧→工具定位→优化手段”讲;问组件化就按“解耦→通信→工程治理”讲。主线清晰,面试官才能跟得上你的思路。第三段,聊边界和取舍。要么说“这个方案在什么情况下不适用”,要么说“如果继续优化,我会从哪个方向切入”。无论哪句,都能体现出你不是在背题。
我见过一个候选人讲MVP讲得头头是道,从职责划分到接口设计都非常流畅,但面试官问“那MVVM比它多了什么”,他明显卡住。这种尴尬就来自只准备了方案的优点,没有准备对比和边界。主观题的开放提问,不是让你证明“某个答案是对的”,而是让你展示一套“在复杂环境下怎么做决策”的思考框架。
5.2 这几类答题姿势,属于送命题
我从面试官视角替大家总结了几种特别影响主观题分数的行为,希望你们有则改之。
第一类是背概念但答非所问。比如问“你为什么在项目里用MVP”,回答“MVP由Model、View、Presenter组成,把逻辑分离了”。这句话每个字都对,但压根没回答“为什么”。面试官真正想听的是:当时项目面临什么问题,MVP帮你解决了什么具体痛点,比如Activity代码膨胀、单元测试难写,还是多人协作时代码边界混乱。第二类是大量使用模棱两可的词。“好像是”“我记得应该是”“可能就是”这些话会极大削弱可信度。哪怕你对细节只有六成把握,也应该用肯定语气说“它的做法是……”,再补充“具体实现细节我记不太清,但原理上应该是……”。第三类是纯理论没有例子。你说自己有优化经验,一定要跟一个项目例子,哪怕是一个非常小的点:“当时我们列表卡顿,我在onBindViewHolder里发现创建了太多临时对象,改成复用后明显改善。”有具体项目背景的表述,可信度会高很多。第四类是遇到不会的问题直接沉默。你完全可以坦然说:“这个点我确实没有深入过,但根据我的理解,它可能和……有关,我下去会补一下。”这比硬编一个答案体面太多。
这些技巧看起来都很简单,但真到面试高压状态下很容易忘。我的建议是在面试前找朋友做一次模拟问答,专门针对主观题练习,强迫自己在五分钟内把“定义—设计—边界”说完整。练熟了,面试时才能自然表达。
这些年我陆陆续续参与过几次校招评审,再回头看2017年京东这套Android主观题,最大的感触是:很多问题到现在都不过时。技术栈从Java迁到了Kotlin,架构从MVP演进到MVVM再到Compose,但面试官考察的核心始终没变——你说你会,那你到底理解到什么程度,你拿什么证据证明。如果你正在准备Android面试,与其刷一百道题,不如挑十道最典型的主观题,每一道都用“原理加实践加边界”完整过一遍,再找个人模拟讲一遍。能讲清楚,才算是真的会了。