公司动态
2018货拉拉Android笔试题解析:从Handler到性能优化
1. 这份卷子的整体考点分布与命题思路1.1 五个必考模块几乎年年不落先说结论这份2018年货拉拉的Android笔试题卷二考的东西放到今天依然是面试高频区。我当时拿到卷子第一反应是题目不算偏但覆盖面很广而且有不少题是“看着简单写起来容易翻车”的坑题。整张卷子大致能拆成五个模块Java基础与并发、Android四大组件与消息机制、自定义View与事件分发、性能优化与内存管理、开放性设计题。这五个模块几乎就是中大型互联网公司Android岗笔试的“标准套餐”。哪怕到了现在我帮朋友做面试辅导时还是会按这个框架去准备。具体到卷二B我记得比较清楚的几个方向Handler消息机制、Activity启动流程、线程池参数、HashMap原理、图片加载优化、内存泄漏场景分析以及一道关于App整体架构或者模块化设计的开放题。这些题目本身不算难但它考察的不是“会不会背”而是“有没有真正理解过”——很多题只要稍微追问一下细节就能筛掉一大半人。1.2 命题人真正在考察什么能力说句实在话笔试题目本身能筛掉的人有限。因为只要认真刷过题绝大多数题目都能写个七八成。真正拉开差距的是命题人在题目里藏的那些“追问点”。举几个例子考Handler一定追问Looper和ThreadLocal的关系问为什么主线程的Looper不用手动创建。考Activity启动流程一定会往AMS、Zygote、binder调用链的深了问不过笔试阶段能画出一个大致时序图就够分了。考线程池不会只问核心参数必然要问执行流程和拒绝策略触发的边界条件。考HashMapJdk 1.7和1.8的头插尾插区别、扩容死循环问题是高频追问方向。所以我的建议是准备这类笔试题不能只背结论要把每个知识点往底层拉一层。你多准备的那一层往往就是面试官给你打高分的理由。2. Java与并发基础题解析线程池、synchronized、HashMap2.1 线程池七大参数答到多细才算稳线程池几乎是每一份Android笔试都绕不开的题。货拉拉这份卷子也不例外我记得题目给了一个场景某个App里需要频繁执行短时IO任务问你应该怎么设计线程池并说明参数设置的理由。这道题的答题要点分三层第一层把七大参数列全核心线程数、最大线程数、空闲存活时间、时间单位、阻塞队列、线程工厂、拒绝策略。缺一个都不行。第二层讲清执行流程新任务进来先判断核心线程是否空闲不空闲就放队列队列满了才开新线程到最大线程数再满才触发拒绝策略。注意核心线程默认不会因为空闲而被回收除非设置了allowCoreThreadTimeOut。第三层结合场景谈参数取舍。比如短时IO任务核心线程数可以设置为CPU核数1队列用SynchronousQueue或者容量较小的LinkedBlockingQueue因为IO任务虽然阻塞但单次执行时间短不需要堆积大量任务在队列里。如果乱用无界队列容易出现任务堆积导致内存问题。我看过不少人的答案普遍问题是只写了参数没有写执行流程更没有写参数设置的依据。这样只能拿一半分。2.2 synchronized与volatile锁升级过程才是分水岭并发编程的题卷子里通常会有两三道。经常出现的一种问法volatile和synchronized的区别以及synchronized的锁升级过程。volatile和synchronized的区别是基础题几乎都能答上来volatile保证可见性和有序性不保证原子性不能替代锁synchronized保证互斥、可见性和有序性。但“锁升级过程”这道题答完整的人就少很多了。完整链条是这样的无锁状态对象头里Mark Word记录的是正常的hashCode等信息。偏向锁单线程竞争时Mark Word记录当前线程ID后续该线程再次进入无需CAS。轻量级锁多线程交替竞争时偏向锁撤销升级为轻量级锁通过CAS自旋获取锁。重量级锁CAS自旋超过阈值或者竞争激烈升级为重量级锁由操作系统管里线程阻塞和唤醒。笔试时要把这个链条写清楚并且说明Mark Word是锁状态的关键存储位置。很多人在这一步就暴露了对JVM内存布局的不熟悉。我当年复习这块时一直用“轻量级锁是自旋等待重量级锁是阻塞等待”这句话来帮助记忆——前者在用户态搞定后者要陷入内核态。理解了这一层离线升级条件的题就很好答了。2.3 HashMap源码put流程与扩容时机HashMap是笔试必备题。货拉拉的卷子里有一道“HashMap put方法的过程”题看似送分实则有坑。完整的put流程是计算key的hash值通过扰动函数高16位异或低16位降低碰撞概率。(n - 1) hash计算数组下标如果为null直接放进去。如果当前位置有元素分三种情况链表节点就直接尾插1.8以后树节点就走红黑树逻辑key相同则覆盖value。插入完成后如果size大于阈值thresholdcapacity * loadFactor触发resize扩容为原来的2倍。重点要答的是为什么阈值是负载因子乘以容量为什么不直接用size capacity做判断——因为负载因子0.75是时间复杂度和空间复杂度的折中过大容易产生哈希冲突过小则浪费空间。另一个高频坑点是1.7和1.8的区别。1.7是头插法扩容时链表会反转并发环境下可能形成环形链表导致死循环1.8改为尾插法避免了这个极其隐蔽的问题。我遇到不止一个候选人能背出全部源码流程但问“为什么1.8要改尾插法”就一脸茫然。这一问往往就能筛出是真懂还是背题。3. Android核心机制题解析Handler、Activity启动流程、Binder3.1 Handler机制从ThreadLocal到epoll的完整链路Handler是Android笔试的“题王”几乎没有哪份卷子不考。货拉拉这份卷二里我记得是直接让描述Handler的完整工作机制。很多人的回答是“主线程创建LooperHandler发送消息Looper从MessageQueue里取消息然后handleMessage处理。”这个回答只能得基础分不到一半。真正完整的答案应该是这样每个线程通过ThreadLocal持有唯一一个Looper主线程在ActivityThread的main方法里通过Looper.prepareMainLooper()初始化。Looper内部维护一个MessageQueue它是单向链表结构按时间排序是一个“无限循环取消息”的调度器。Handler的enqueueMessage把消息按when时间插入队列sendMessageDelayed最终也是走这里。关键点在MessageQueue.next()当没有消息时会调用nativePollOnce进入epoll等待状态此时线程不会空转这也是为什么主线程Looper死循环却不占满CPU的原因。这个nativePollOnce的底层是Linux的epoll机制通过一个管道eventfd监听读写事件写端由enqueueMessage唤醒。如果能把这个链路完整画出来并且把nativePollOnce和epoll的关系讲清楚这道题就稳了。别担心笔试时写不了太多代码面试官要的是“结构完整”比堆细节更重要。3.2 Activity启动流程一次startActivity经历了什么Activity启动流程这道题货拉拉卷子里问得比较深我记得题目要求画一个完整的时序图。完整链路是这样的App调用startActivity最终走到Activity.startActivityForResult。通过Instrumentation.execStartActivity发起请求。通过binder调用AMS.startActivity进程切换到system_server侧。AMS进行一系列校验包括权限、uid、taskAffinity等然后调用ApplicationThread.scheduleLaunchActivity。通过binder回调用到App进程的ActivityThread由它来创建Activity实例调用onCreate等生命周期方法。这里最容易漏的两个点第一个是Instrumentation很多人根本不提这个类但它贯穿了整个Activity生命周期管理。第二个是AMS和ApplicationThread之间的binder回调如果跨进程通信的机制理解不透这个流程很容易讲“串台”。建议大家把这个时序图在A4纸上画一遍把每一步的IPC方向和调用类写清楚。画顺了以后对四大组件的理解都会上一个台阶。3.3 Binder通信一次IPC的完整旅程Binder可以说是Android底层面试题的常客就算笔试没直接考面试也一定会追。货拉拉卷子里有一道选项题涉及ContentProvider跨进程访问本质就是在考binder。Binder的核心机制说复杂也复杂说简单也简单客户端通过SMServiceManager拿到远程服务的Binder代理。调用代理的transact方法把Parcel数据发送过去。驱动层binder驱动完成数据拷贝映射到接收进程的地址空间。接收进程的线程池中某个线程执行onTransact处理完后再把结果Parcel发回。这里有一个高频考点为什么Binder只需要一次拷贝而传统的管道/Socket需要两次答案是binder驱动通过mmap把接收方的物理内存映射到内核空间发送方只拷贝一次到内核缓冲区接收方直接从映射区读取省掉了接收方这边的第二次拷贝。这个知识点单独背总结没用建议配合源码或者binder驱动流程图去理解。笔试时写清楚“靠mmap实现一次拷贝”这个关键点就足够和其他候选人拉开距离了。我自己的体会是Binder这个东西第一次接触确实很抽象。但一旦理解了它的设计目标——“既要跨进程通信又要保证安全和性能”再去读代码思路就顺了。4. 性能优化与内存专项图片加载、内存泄漏、布局优化4.1 图片加载缓存策略与采样压缩图片加载优化是Android笔试中非常爱考的一道题因为在实际开发中确实高频遇到。货拉拉这份卷子里有一道题问“如何高效加载大图”其实就是考察图片压缩和缓存。答题要点分两部分第一部分是采样压缩。通过BitmapFactory.Options的inJustDecodeBounds先读取图片宽高计算inSampleSize然后再真正解码。计算inSampleSize的逻辑是让压缩后的宽高不小于目标宽高的一倍再取最近的2的幂。第二部分是三级缓存。内存缓存用LruCache磁盘缓存用DiskLruCache网络下载走后台线程。LruCache的size通常设为当前进程可用内存的1/8这个比例是实践中调出来的既不会占用太多内存又能保证较高的缓存命中率。此外还要提到glide这类图片库其实已经帮我们封装好了这些逻辑但笔试考的是原理不是会使用。如果你能在答案里提到Glide内部用的LruBitmapPool和Downsampler会给面试官留下一个很好的印象——说明你看过源码不只是停留在API层面。4.2 内存泄漏Handler泄漏、ActivityContext、资源未关闭内存泄漏题笔试常见的问法是“给出一个由Handler导致的内存泄漏场景分析原因并提出解决方案”。标准答案是非静态内部类Handler隐式持有外部Activity的引用如果MessageQueue里还有未执行的消息会通过Message持有Handler引用从而间接持有Activity引用导致GC无法回收Activity。解决办法有两个主流方向一是把Handler声明为静态内部类用WeakReference引用Activity在onDestroy时移除所有消息。二是使用生命周期绑定的工具比如Lifecycle框架自动在销毁时清理消息。除了Handler内存泄漏还有一个高频考点是ActivityContext乱用网络请求回调、静态集合、单例等都可能导致Activity无法释放。原则就是一句话短生命周期对象不要被长生命周期对象持有引用如果必须持有就用弱引用或者及时清理。笔试里只要把“引用链”梳理清楚、把解决方案写具体就能拿不错的分数。比光写“注意内存泄漏”五个字强一百倍。4.3 布局优化include、merge、ViewStub、ConstraintLayout布局优化在笔试中占比不算大但基本每年都会有一道选择或简答题。常见的问法是“优化布局层级有哪些手段”。这道题简单但很多人答不全。完整答案包括include复用公共布局减少重复代码。merge标签用于去除多余的一层父布局注意它只能作为include的根节点或作为FrameLayout的子布局。ViewStub用于延迟加载不常用的布局比如网络错误页、空态页面随用随加载不占绘制时间。ConstraintLayout用扁平化布局减少层级能用一个布局搞定的就不要嵌套三层LinearLayout。用布局检测工具Layout Inspector、Systrace、GPU渲染模式先看看哪里有问题再动手优化。不要凭感觉改。说实话这道题本身不难但可以聊得很深。如果你能在答案里提到“过度绘制”和“View的measure/layout/draw”三遍流程面试官对你的评价会直接上一个档次因为这代表你不仅仅知道怎么用还知道为什么能优化。5. 编写答案的实战策略与避坑指南5.1 答题顺序与时间分配的实用建议我就直说了拿到卷子第一件事不是做题是花三分钟快速浏览全卷按分值、难度、是否熟悉三个维度给每道题做个标记。然后把时间分成三块第一块快速拿下基础送分题。比如四大组件概念、Handler机制概述、线程池原理简述。这类题占了大约40%的分值20分钟内要全部写完。第二块集中精力处理中等难度题。比如启动流程时序图、图片压缩计算、内存泄漏分析。这些题需要画图和写步骤每道题控制在15到20分钟。第三块最后处理开放题和难题。开放题没有标准答案要留足够时间组织好逻辑。另外有个很实用的小技巧简答题如果能画图一定画图。流程图、时序图、数据结构图都能帮你拿到“结构清晰”的加分。我自己阅卷时会特别留意有图的试卷因为图解往往比纯文字更容易展示理解深度。5.2 遇到不会的题怎么写才能拿分所有笔试都会有超出你准备范围的题目。货拉拉这份卷子的开放题我记得涉及“如何设计一个图片加载框架”网上答案五花八门但真正能拿高分的很少。如果遇到不会的题我的建议是三步走第一步把题目拆解成你能理解的最小模块。比如“设计图片加载框架”就拆成加载、缓存、线程调度、生命周期管理、错误处理。第二步对每一个模块能写多少写多少。加载就写InputStream和BitmapFactory缓存就写LruCache和DiskLruCache线程调度就写线程池生命周期管理就写绑定Activity的onDestroy错误处理就写替换占位图。第三步在你确定不会的地方写“此处可以考虑XXX方案”的设想而不是空白。这样做最大的好处是即使最终答案不完整阅卷人也看得到你的设计思路和分析能力。笔试从来不是只比谁会得多更比谁会拆问题——毕竟到了真正的开发环境里遇到的问题绝大多数不是你能照搬熟练方案的。这里多说一句开放题最忌讳的是“答非所问”。比如问“设计一个图片加载框架”你花大量篇幅写Glide怎么用、怎么集成——这等于告诉面试官你只懂API不懂框架设计。一定要从存储、调度、缓存结构这些底层维度展开哪怕实现不够完整也比跑题的完整答案强。5.3 复盘做完笔试之后一定要做的三件事笔试题交卷不是结束而是下一步面试准备的起点。我自己的习惯是交卷后立刻做三件事第一件把不会的判断题、选择题题目原样记下来。这些知识点往往就是你真正的技术盲区下次面试前重点补这里。第二件把所有开放题的答案重新写一遍尽量往框架级别靠。比如图片加载这道题笔试时可能只回答了LruCache加线程池复盘时就要往“生命周期绑定、内存缓存命中率、磁盘缓存版本控制、Glide源码对比”这几个方向补全补齐了面试时碰到这套题你会发现自己像开了挂。第三件把整份卷子的知识点按“已掌握、部分掌握、完全不会”三个等级分类记录到错题本或者备忘录里。梳理完这步你对自身技术程度的判断已经是大多数候选人的一步之遥了。复盘这个动作看似简单长期坚持对知识体系的帮助非常大。我带的学员里凡是笔试之后认真做这三件事的后续面试通过率反而比那些只顾着刷更多新题的人高。原因在于他们能把一次笔试真正吸收成下一次面试的弹药。6. 从2018年的卷子看到的行业趋势与面试风向6.1 核心知识点这几年变了吗说实话2018年的这份安卓笔试题放到今天来看90%的知识点依然完全适用。Handler、Binder、Activity启动流程、线程池、HashMap这几个核心考点在2024年、2025年的面试里依然是高频题。原因也很简单这些底层机制是安卓系统的地基只要安卓系统还跑在Linux内核上只要app还是跑在ART虚拟机里这些机制就不会变。变化的部分主要在新增的考察方向上协程Kotlin协程的异步编排、结构化并发现在基本是必问项2018年基本没人考。Jetpack Compose声明式UI、重组机制、状态管理现在大厂面试越来越爱出。性能监控严格模式下怎么定位卡顿、冷启动优化、弱网优化这些应用层面的经验题比重也在增加。架构设计MVVM、MVI、组件化、插件化这些在2018年是加分项现在更像基础要求。但注意新题并不会取代旧题而是在旧题的基础上叠加新问题的复杂度。所以备考时千万不要因为时代变了就跳过底层基础。6.2 货拉拉这类业务型公司的出题偏好从这份卷子能看出货拉拉这类“互联网物流”公司的出题风格基础题占比大业务场景题也很重视。物流行业App的特点是高并发下的地图定位、订单状态同步、司机端和用户端双端联动。这些业务场景会导致一系列Android核心问题被高频问及比如地图卡顿如何优化涉及自定义View和绘制优化。大量订单消息推送如何保证消息不重复不丢失涉及进程通信和消息队列。弱网环境下图片和文件上传怎么做断点续传涉及OkHttp拦截器和网络策略。多模块团队并行开发的代码冲突怎么解决涉及模块化架构设计。你可以把一份笔试题目当成一个窗口通过出题节奏反推这家公司在真实业务里的技术栈和痛点。同样一份卷子滴滴、美团、货拉拉的题可能相似度有80%但剩下那20%的业务倾向性恰恰是面试官筛选候选人时最看重的东西。所以我的建议是刷题不能盲目除了刷通用基础题还要结合目标公司的业务特色去准备几个“业务技术”结合的案例。比如投货拉拉就要准备一个“如何优化物流列表页面的流畅度”的方案投出行类App就要准备一个“如何保证地图拖动不卡顿”的分析。这不仅能帮你在笔试时多拿印象分面试时也会格外加分。最后再分享一个我自己实际操盘多年得出的心得笔试里凡是需要用代码画流程的题千万别只写个类名加方法名就完事。把对象之间的调用关系、时序、生命周期用图示表达清楚往往比洋洋洒洒几百字更让阅卷人舒服。而真正决定你能不能被录用的很多时候并不仅仅是你答对了多少题而是你在那些“看似简单但能延伸”的题上展示出的工程思维深度。这份2018年的货拉拉卷子无论你是准备校招还是社招都值得好好过一遍。