FEATURED · 精选文章

小米安卓开发笔试题B卷解析:Java基础、Handler与性能优化

发布时间 / 2026/8/31 5:37:07
来源 / 创域科博编辑部
栏目 / 资讯中心
小米安卓开发笔试题B卷解析:Java基础、Handler与性能优化 小米2019秋招安卓开发笔试题B我当年是在准备校招时专门刷过的。这套卷子给我最大的感受是它不跟你玩偏题怪题而是把安卓开发日常里最容易忽略的底层原理换着角度反复考。网上关于这套题的零散回忆很多但很少有人把考点、答题思路和避坑点串成一条线。这篇文章我就以B卷为线索把Java基础、Android系统机制、性能优化、编程题和开放题这几个方向完整拆一遍。不管你是2025年才准备安卓开发校招还是社招想补基础这套复习框架都能直接用。1. 笔试题整体印象B卷的考察逻辑与应对思路1.1 从B卷看小米对安卓开发者的能力要求小米这套B卷整体偏向“原理驱动”。它不会只问你某个API是干什么的而是把API放到具体场景里让你判断生命周期变化、线程状态或者内存结果。比如同一个Activity在A页面跳转、按Home键、接电话、屏幕旋转这四种场景下onSaveInstanceState和onRestoreInstanceState的执行顺序分别是什么。这类题表面考生命周期实际考你有没有真正跑过应用、有没有看过系统源码的调用链。我后来复盘时发现B卷想筛选的是那种“能独立排查问题”的人。安卓开发做到后面很多问题不是百度能搜到的需要你从Handler消息机制、View绘制流程、进程优先级这些底层概念去推断。所以备考不能只背结论要把每个知识点都当成一棵树的根节点顺着它去理解周边概念。比如你学Activity启动模式不能只记四种模式的区别还要把taskAffinity、onNewIntent、返回栈的合并规则一起串起来因为笔试题专门喜欢把这些概念混在一起出。另外一点B卷对“知识面广度”有明显要求。除了纯安卓内容还会涉及HTTP缓存、数据库索引、线程池参数、数据结构算法。这说明小米希望候选人不是“只会写页面”的客户端小白而是具备完整计算机基本功的工程师。备考时如果只刷安卓题不补Java基础和算法最后分数会被拉开很大。1.2 题型分布与时间分配虽然每年笔试题都会调整但B卷的题型结构可以作为参考先是选择题覆盖Java集合、并发、字符串、Android基础然后是简答题以Handler、Activity生命周期、View绘制、内存优化为主接着是两道左右编程题难度大约在LeetCode Easy到Medium之间最后是一道开放设计题让设计一个图片加载库或模块。整体时间通常在90分钟到120分钟实际题量要看当年平台设定但时间压力普遍存在。这里我建议的时间分配是这样的选择题控制在20分钟内简答题40分钟编程题50分钟开放题15分钟最后留5到10分钟检查。选择题里如果遇到完全没把握的概念先凭第一感觉选一个并做标记不要恋战。简答题是拿分大户哪怕记不全也要把关键术语写上去比如“不可见性”、“重排序”、“引用泄漏”。阅卷人往往是按点给分你多写一个正确的点就多一分。编程题则要注意先想清楚边界条件再动手不要一上来就敲代码后面我会单独展开。2. 核心考点逐题拆解Java基础与安卓四大组件2.1 Java基础高频题集合、并发与内存模型Java基础部分B卷几乎必考HashMap。它喜欢让你比较HashMap和Hashtable然后问ConcurrentHashMap的分段锁或CAS实现。别只回答“HashMap线程不安全Hashtable线程安全”要能解释为什么HashMap的put操作在扩容时可能出现环形链表JDK 1.8后虽然引入红黑树并发时仍可能丢数据。如果想答得再深入一些可以说HashMap的扩容因子是0.75这是时间复杂度和空间占用之间的折中。链表长度超过8且数组长度不小于64时转红黑树是因为在随机哈希下节点数分布符合泊松分布链表长度到达8的概率已经极小转树能应对极端哈希冲突。并发题是很多人的短板。B卷不会直接让你写一段多线程代码而是结合安卓场景问为什么主线程不能做耗时操作子线程能不能更新UIsynchronized和ReentrantLock怎么选回答时要从JMM的可见性和指令重排讲起。volatile能保证可见性和禁止重排但不保证原子性如果要实现原子操作就用AtomicInteger或者加锁。能举出安卓场景会更加分比如一个全局计数器在多线程下载任务里累加进度用volatile是不够的必须用AtomicInteger或者synchronized保证read-modify-write的原子性。Java内存模型这一块建议你把“主内存-工作内存”的模型和安卓的“主线程-子线程”对应起来理解。子线程读到共享变量后并不一定立即看到主线程的修改这跟代码执行顺序有关。理解了这一点就能明白为什么需要在Activity onStop里取消异步任务不然线程还在后台跑却引用了已经销毁的Activity就会造成内存泄漏。2.2 四大组件与启动模式Activity、Service、BroadcastReceiverB卷对Activity的考察从来不是单纯背启动模式而是让你解决实际问题。比如题目可能会给你这样一个需求从桌面通知栏点击一条消息进入聊天页面希望此时聊天页只存在一个实例如果已经存在就复用并刷新数据。这就是singleTask的典型场景但要拿全分你还得说明为什么要加上taskAffinity。因为singleTask默认在当前task中查找如果通知栏通过PendingIntent启动Activity可能被放到新的task里这时需要使用合适的taskAffinity和Intent标志配合。Service部分startService和bindService混合使用的生命周期是高频题。很多人搞不清先startService再bindService最后unbindService后Service会不会销毁。答案是不会必须再调stopService或stopSelf。onStartCommand的返回值也是考点START_STICKY、START_NOT_STICKY、START_REDELIVER_INTENT分别对应“被系统杀死后是否重建并传入null Intent”、“不重建”、“重建并重传最后一条Intent”。这类题结合场景来记下载任务选REDELIVER普通消息推送选STICKY后台定位选NOT_STICKY因为定位服务被系统回收后最好别自动重启避免耗电。BroadcastReceiver现在的使用频率下降但笔试仍然爱考。重点有两个一是静态注册在Android 8.0之后对大部分隐式广播失效许多系统广播只能动态注册二是动态注册的Receiver要记得在onPause中注销否则Activity销毁后Receiver仍然存在导致内存泄漏。B卷还会把LocalBroadcastManager拿来做比较但这里要注意LocalBroadcastManager底层是通过Handler实现的并不是系统广播它只能应用内通信好处是广播不会跨进程泄露。后来官方推荐用LiveData或者Flow做应用内通信但笔试考到旧知识时你最好把原来的原理也讲清楚。2.3 Handler与消息循环为什么主线程不卡死Handler在安卓笔试里的地位就像“链表反转”在算法面试里的地位是必考中的必考。B卷常见的问法是子线程里能不能直接new Handler不能。你必须在子线程里先调用Looper.prepare()再创建Handler最后调用Looper.loop()消息循环才会跑起来。如果直接new会抛出“Cant create handler inside thread that has not called Looper.prepare()”的异常。这个异常信息本身就是提示很多人只在书上看到没实际跑过所以记不清。重点来了主线程为什么一直在Looper.loop()死循环却不卡死核心原因是MessageQueue在队列没有消息的时候会让主线程进入阻塞状态这个阻塞是基于Linux的epoll机制不会持续占用CPU。你可以把它理解成饭店门口排队的叫号员没有客人叫号时就坐在那儿休息有号了再起来喊人。所以这个“死循环”并非空转而是有事件才醒、无事件就睡。补充一点MessageQueue里还有同步屏障和IdleHandler的概念Choreographer会利用它安排帧绘制。能把这些说出来说明你不是只背了八股。另外一道常考的变形题是能不能在子线程更新UI正确说法是“Android不允许在子线程直接更新UI”但严格讲ViewRootImpl是在onResume之后才创建的如果Activity还没执行onResume子线程在onCreate里操作TextView可能不会立即报错。笔试不建议较这个真重点是说清UI更新最终要回到主线程常用方式是Handler.post、runOnUiThread、View.post。这些其实最后都会进入主线程的MessageQueue所以Handler就是整个线程通信的枢纽。3. 系统机制与性能优化从原理到实战3.1 View绘制与事件分发一道题说明白View的绘制流程在B卷里通常会以对比题出现比如让区分invalidate和requestLayout。invalidate只是告诉系统这个View“我要重画”它不改变尺寸和位置所以只会走onDraw不会走onMeasure和onLayout。requestLayout则相当于告诉系统“我的尺寸或位置变了”会从ViewRootImpl开始触发整个View树的measure、layout、draw。在主线线程更新UI用invalidate在子线程更新UI时要用postInvalidate因为它会通过Handler切回主线程再执行invalidate。这个细节就是新手和熟练工的分水岭。事件分发部分核心是三个方法dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。它们的调用顺序是Activity-ViewGroup-View但要注意intercept只发生在ViewGroup中。一次DOWN事件会先经过Activity的dispatchTouchEvent然后ViewGroup的dispatchTouchEvent先询问自己的onInterceptTouchEvent再由子View处理。如果DOWN事件没有被任何一个View消费同一个序列的MOVE和UP事件就不会再传递下去而是由父容器自己处理。理解这个“第一个DOWN事件就决定了后续事件归属”的机制比背一堆方法名更重要。实际项目中滑动冲突就是事件分发的典型应用。比如在垂直RecyclerView的一个Item里面放一个横向滑动的Banner如果没做处理横滑时RecyclerView会抢事件。解决方式是在子View里调用getParent().requestDisallowInterceptTouchEvent(true)或者重写父容器的onInterceptTouchEvent根据横向滑动距离和竖直滑动距离来判断是否拦截。笔试题不会让你写完整代码但会把场景抽象出来问你怎么解决。3.2 内存优化与OOM排查LeakCanary背后的原理B卷简答题经常出现“线上OOM怎么定位”这样的题目。答题不能只说“用Android Profiler”要说出完整链路的思路先判断是内存抖动还是内存泄漏。内存抖动表现为内存曲线频繁升降通常是大量短生命周期对象不断创建比如在onDraw里new对象内存泄漏则是GC后占用不降说明对象被不该持有它的对象引用住了。定位泄漏时用Memory Profiler dump一份heap查找Activity实例是不是仍然存活再顺着引用路径找强引用来源。LeakCanary的设计机制本身就是一个很好的答题素材。它在Activity.onDestroy之后把Activity包装成WeakReference并关联一个ReferenceQueue。当Activity真正被回收时这个WeakReference会进入ReferenceQueue如果一段时间后队列里没有这个引用就说明Activity仍然被其他对象持有于是手动触发GC再次确认然后dump堆内存分析引用链。用这种“弱引用引用队列二次GC”的思想可以回答很多关于内存检测的问题。优化方案要落到代码上。最常见的优化就是静态变量和单例里不要直接持有Activity或View尽量使用ApplicationContextHandler写成静态内部类并持有外部Activity的WeakReference注册的Receiver、Observer、回调接口一定要在onDestroy里注销。图片内存也占大头加载大图时BitmapFactory.Options里设置inSampleSize按2的幂次采样压缩同时用LruCache做内存缓存避免反复解码。这些点每一个都不难但组合起来就是完整的内存治理体系。3.3 网络与数据存储HTTP缓存与数据库设计网络题在B卷里通常不会单独出一个很大的题目而是夹杂在开放题或者简答题中。比如让你设计一个图片加载库就必然会涉及HTTP缓存。你需要说清楚Cache-Control、ETag、Last-Modified这几个头的关系。简单说Cache-Control控制缓存生命周期如max-age3600ETag是资源内容的哈希标识资源变化后ETag跟着变。客户端第一次请求后缓存了响应第二次请求客户端带If-None-Match字段值就是之前的ETag。服务器比对后如果资源没变化就返回304客户端直接用本地缓存。这个流程能清清楚楚写下来说明你真的做过网络优化。数据存储部分SQLite相关题目主要考察建表能力和查询能力。笔试题不会让你查一个特别复杂的SQL但会问索引怎么建。凡是where条件里用得多的字段order by排序的字段都可以建索引。但索引不是越多越好因为每次插入删除数据索引都要跟着更新写性能会下降。另一个高频考点是SQL注入答案非常简单不要用字符串拼接SQL要用参数化查询。在Android里就是SQLiteDatabase的selectionArgs参数或者Room的Query带参写法。能提到Room的依赖注入和编译期SQL校验说明你已经不是原始SQLiteHelper那一代的开发了。4. 编程题与开放题写代码的边界感4.1 手写单例与线程安全编程题第一类高频是手写单例很多同学都能写出懒汉式但B卷考察的往往是DCL双重检查锁。代码如下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()这一步在字节码层面不是原子的它包含分配内存、调用构造器、把引用赋值给变量三步。如果不用volatileJVM可能优化成先赋值再调用构造器另一个线程就会拿到一个“地址已存在但对象还没构造完”的实例。加了volatile禁止指令重排才能保证安全。笔试时如果只写锁不加volatile基本等于没答到这个题的核心考点。除DCL之外静态内部类方式也是不错的答案。它利用类加载机制保证线程安全同时实现懒加载代码比DCL更简洁public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }推荐的回答思路是先把三种写法的优缺点列出来然后说实际项目里常用静态内部类或枚举但在并发要求极高的场景会考虑DCL。最后可以补一句“单例要考虑反序列化和反射攻击”如果能提到枚举天然防反射和防序列化这个题就答得比较完整了。4.2 开放题如何设计一个图片加载库这是一道让人印象深刻的开放题因为它没有标准答案但考察维度非常清晰。我看到这个题的第一反应是想写Glide但后来明白阅卷人想看的是“设计能力”。我的回答结构是四层加载入口、缓存策略、线程池管理、生命周期感知。加载入口要区分同步和异步同步方法在调用线程直接执行异步方法通过线程池执行。缓存策略用LruCache做内存缓存底层是LinkedHashMap通过accessOrder实现最近最少使用淘汰磁盘缓存用DiskLruCache在应用私有目录里存文件。加载顺序是先查内存命中直接返回再查磁盘命中就解码并回写到内存都没有才走网络请求成功后回写到磁盘和内存。这里要提一下二次采样通过BitmapFactory.Options的inJustDecodeBounds先读宽高再按ImageView需要的大小计算inSampleSize避免加载大图导致OOM。线程池部分要说明为什么不直接用new Thread而是固定大小的线程池加队列。图片请求往往数量多但任务轻用Executors.newFixedThreadPool配合BlockingQueue可以让请求不丢、不无限开线程同时要设置拒绝策略比如丢弃最旧任务避免滑动列表时太多图片在等待。生命周期感知是加分项在Activity或Fragment销毁时取消正在执行的图片加载避免callback回来时已经无法更新UI也避免持有Activity引用造成泄漏。这套结构说完基本上就是Glide的简化模型了。4.3 算法题排序、字符串与二维矩阵B卷的算法题整体不难但要求手写干净。排序的话快排和归并最常考。快排的边界条件是重灾区分区函数里left和right的移动顺序、pivot选左还是选右、递归终止条件都要反复验证。归并排序难度略高但思路清晰先递拆分再归并合并重点在于merge操作要借助临时数组时间复杂度O(nlogn)空间复杂度O(n)。能把空间复杂度说清楚比单纯背代码更让阅卷人放心。字符串题常见无重复最长子串这种题一看就要想到滑动窗口。用两个指针维护一个窗口用HashSet记录窗口内出现过的字符右指针向右扩展遇到重复就移动左指针收缩过程中记录窗口长度最大值。另一类常见是判断两个字符串是否互为字母异位词可以用数组计数因为字符范围有限用长度为26的int数组比HashMap更省内存。二维矩阵题岛屿数量是经典思路是DFS或者BFS遍历遇到“1”就把整片岛屿标记为“0”统计触发次数。这些算法本身不难但要在笔试限时环境下写对需要平时达到“肌肉记忆”的程度。算法题的答题习惯也非常重要。我练习时强制自己先写注释把思路写出来。比如“// 滑动窗口right向右扩展left收缩保证无重复”然后再写代码。这样不仅自己逻辑更清楚阅卷人也能一眼看到你是懂还是蒙。哪怕代码漏了边界分支只要注释里有思路也能拿到步骤分。4.4 安卓开发新方向uniapp、车载与多屏适配的延伸思考B卷虽然考的是2019年的知识体系但其中一些开放题其实已经暗示了行业趋势。比如多屏设备和Launcher开发今天再看特别有现实意义。小米的业务早已不止手机手环、平板、车机、智能家居都有安卓系统存在。笔试题里如果考“如何让应用在两个屏幕上正确展示”本质就是在考多屏适配。你要能说出DisplayManager、Presentation、多Display对应的Context和窗口参数以及OnDisplayAdded回调的注册与注销。能把这些点引到知识框架里说明你不是只会写手机页面。另一个热词是uniapp这类跨端方案。很多同学简历上写了uniapp但遇到原生崩溃问题就抓瞎比如uniapp里的地图组件遮挡原生控件、webview层级不适配。这些问题用uniapp无法解决只能回到安卓原生去调整比如用原生cover-view或者动态调整map的z轴顺序。这说明跨端开发虽然流行但底层的原生知识依然是核心竞争力。笔试题考原生原理并不是老古董而是在筛掉那些只会“调包”不懂原理的人。车载安卓和Launcher开发也同样Launcher本质就是一个最高优先级的Activity但它涉及的窗口管理、状态栏控制、壁纸服务、快捷方式图标适配都是普通应用开发接触不到的深度内容。如果你在面试里能提到自己因为做Launcher项目才去研究WindowManager和Activity启动流程那比任何背诵都有说服力。5. 高频陷阱与答题技巧复盘5.1 概念题最容易丢分的三种情况第一种是只答定义不答边界。问你“进程和线程的区别”只写“进程是资源分配单位线程是CPU调度单位”只能拿一半分。要补充到安卓场景同一个应用默认跑在同一个进程里组件之间通过Binder跨进程通信而线程只是进程内的不同执行路径不能用来代替进程。第二种是背了源码不知道设计原因。比如讲Looper.loop()死循环一定要说明它基于epoll的休眠机制目的是在无消息时释放CPU不是真的把CPU占满。只背一句“死循环是为了保证主线程不退出”是不完整的因为主线程本来就不会因为方法执行完而退出真正原因是需要复用线程并随时处理新消息。第三种是相似概念混淆。较多见的就是invalidate和requestLayout、startService和bindService、BroadcastReceiver动态注册和静态注册。建议把这些易混点整理成一张表放在每日复习的第一页。5.2 答题顺序与取舍策略走到笔试环节时间管理比知识储备更影响结果。我一般会先花两分钟快速浏览整卷把会做的题按顺序标成A、B、C三类。A类是特别熟练的、能拿全分的B类是有点印象但需要思考的C类是完全没把握的。答题时按A、B、C顺序做而不是按照试卷顺序。这样保证熟题的分先拿到手后面遇到难题即使时间不够也不会出现“会的没做、不会的乱写”的后悔。编程题如果暂时没思路先跳到简答题等脑子绕过弯再回来有时候换个题目反而能激发思路。关于开放设计题很多同学全篇写一大堆框架代码反而没有结构。更推荐用“需求目标-模块划分-技术选型-优化点”的骨架来写。举个图片加载库例子先说需求目标是异步加载、降低重复网络请求再划分模块加载器、缓存、线程池、生命周期然后选型内存用LruCache磁盘用DiskLruCache最后留两个优化点比如缩略图、GIF支持、预加载。这个骨架能保证你在短时间内给到阅卷人一个完整且有层次的回答。5.3 考后复盘我踩过的坑准备这套B卷时我犯过一个典型错误在选择题上过度纠结。有一道关于Activity启动模式和taskId的题我试图用手推演整个任务栈的变化结果推演错了导致前后花了近十五分钟最后编程题差点没时间。后来我定了一条规矩单选题超过两分钟没有明确思路就先填一个最可能的答案并标记等全部题做完再回来推导。有时候大脑先处理后面的题目后再回头看这道题反而很快能定位到之前想错的地方。另一个坑是手写代码时变量名乱起。当时写一个遍历二维矩阵的题用i、j、k混着来最后自己都分不清哪个是行哪个是列越改越乱。现在我的习惯是先写清晰有意义的变量名比如row、col、nextRow并且提前在注释里把循环不变式写好。这样就算某个细节没写对代码逻辑一眼就能读懂阅卷人给的印象分会完全不同。笔试不只是结果的评判更是编码习惯的体现。最后分享一个持续受益的小技巧每做完一套笔试题都把错题和靠蒙对的题整理出来按考点分类然后去源码里找到对应实现。比如回答错了Activity栈的问题就去翻一下ActivityTaskManager的源码注释自己画一遍栈变化图。这个过程一开始很慢但坚持两三套题之后你会发现大多数面试题的答案都能串起来到真正面试时心里会非常稳。安卓开发这个领域底层原理永远是通用的它不会随着框架更替而过时。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻