ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

小米2019秋招安卓开发笔试题B卷解析与备战指南

2026/8/31 3:06:08 拓冰建站 浏览量
小米2019秋招安卓开发笔试题B卷解析与备战指南 2019年那个秋招季小米的安卓开发笔试题在牛客网上挂出来后评论区画风基本是“细节多”“考点密集”“大题特别吃功底”。我前阵子重新把小米2019秋招安卓开发笔试题B完整过了一遍发现这套题放到今天依然值得一套一套地刷因为它没有堆砌偏题怪题而是把安卓开发最核心的组件机制、Java基础、并发模型、性能优化和系统协作能力串成了一张完整的考察地图。这篇文章就把B卷的典型题目按板块拆开讲清楚每道题在考什么、标准答案是什么、背后的原理是什么最后给一份可复制的秋招备战路线。适合正在准备安卓校招的同学也想推荐给那些打算系统复盘安卓基础、准备社招跳槽的开发朋友。1. 这份B卷的考察重心与整体风格1.1 板块分布与难度定位先说结论B卷的命制思路非常“小米”——重视底层原理但不考死记硬背更看重你能不能把原理讲清楚并用它解释实际现象。根据当年参与笔试的同学复盘和题库记录B卷的板块结构大致如下考察板块占比出题形式Java基础类加载、集合、并发、异常30%单选、多选Android组件机制四大组件、消息机制30%单选、多选、简答系统底层与跨进程通信Binder、进程优先级20%简答、编程自定义View与性能优化15%设计题、代码题开放题与软素质题5%问答难度划分很清晰选择题属于“再复习一遍就能做对”但里面埋了不少陷阱简答题和设计题才是拉开差距的地方尤其是性能和自定义View相关的大题几乎决定了你能不能进下一轮面试。1.2 2019年安卓校招的技术背景如果你现在回看2019年的安卓技术栈会有一种“熟悉又陌生”的感觉。那时候Android 9、10刚普及Kotlin已经确定是一等公民Jetpack组件化开发、AndroidX迁移是主流话题热修复、插件化方案依然活跃在各大厂的中台架构里。网络层主流组合是Retrofit OkHttp RxJava协程属于“值得关注但还没完全普及”的阶段。正因为处在这个节点B卷并没有考太多“昙花一现”的框架API而是把重心压在了Handler消息机制、Binder跨进程通信、View的测量布局绘制、内存泄漏这些十年不变的核心原理上。所以这套题即便今天拿出来依然是很好的安卓基础练兵场。你甚至可以把它当成一面镜子照出自己哪些底层概念还停留在“背诵”阶段。2. 选择题里的高频陷阱从Java基础到组件机制2.1 类加载与内存模型一题看出JVM功底选择题部分先遇到的是Java基础题。有一道关于类加载过程的题目很有代表性问的是“关于类的加载过程下列描述正确的是”选项包含了几个经典干扰项加载阶段完成后类的静态变量已经完成赋值双亲委派模型可以防止类被重复加载验证阶段发生在解析阶段之后同一个类加载器可以同时并行加载两个同名同包类正确答案是双亲委派模型可以防止类被重复加载。但这里很容易选错因为不少同学会把“加载、验证、准备、解析、初始化”五个阶段的顺序背得很熟却不知道每个阶段具体做了什么。加载阶段只是把字节码读进内存并生成Class对象静态变量真正赋值发生在后面的准备阶段和初始化阶段。准备阶段会给静态变量分配内存并置零值用户代码里的赋值要等到初始化阶段才执行。验证阶段在准备阶段之前主要是字节码的安全性校验它并不会等解析完成才启动。所以看一道题不能只看阶段顺序还要理解每个阶段干了什么。双亲委派模型为什么能防止重复加载因为每个类加载器在加载一个类时会先交给父加载器尝试父加载器再往上找直到Bootstrap ClassLoader。同一个类在全盘的加载请求最终都会被顶层加载器处理保证同一个类只会生成一份Class对象。换句话说双亲委派像一个“向上汇报”的机制避免同一个类被不同层级的加载器各自加载成两个互相不兼容的类型。2.2 Activity启动模式别把singleTask当全局单例四大组件的题目里Activity启动模式几乎每年都考。B卷有一道题问得比较细“在一个standard的Activity A中启动singleTask的Activity B如果B已经存在于当前任务栈的栈顶系统会怎么做”四个选项分别是新建B压栈、清除B上方所有Activity并回调onNewIntent、直接回调onNewIntent不清理、将B移到栈顶并重建。正确答案是第三个。singleTask的核心语义是“整个系统里只有一个实例”但如果已经有实例并不是所有场景都触发清理。只有当目标Activity不在栈顶时系统才会把该Activity上面的所有页面弹出栈再复用这个实例并回调onNewIntent当它恰好就在栈顶系统不会做任何弹栈操作只是把新的Intent交给onNewIntent处理。很多人把singleTask理解成“全局单例”忽略了栈的位置关系这里丢分很可惜。四个启动模式的差异可以整理成一张表模式匹配规则是否能多次实例化回调特点standard每次都新建能onCreate每次触发singleTop栈顶复用非栈顶新建能栈顶复用走onNewIntentsingleTask栈内唯一按taskAffinity找栈全局复用非栈顶时弹栈后onNewIntentsingleInstance独立任务栈且栈内唯一全局复用整个系统只有它自己所在栈另外还要注意taskAffinity的作用如果给singleTask配了不同的taskAffinity系统会优先去对应任务栈寻找该Activity如果找不到会在那个栈新建一个。这个细节在笔试题里也是一个常见延伸考点。2.3 Handler消息机制主线程为什么“死不了”B卷关于Handler的题目几乎每年都会出现典型问法是在子线程创建Handler前需要先做什么。正确答案是先调用Looper.prepare()再new Handler()最后调用Looper.loop()。这道题看起来很基础但背后可以挖得很深。Handler本身只是把Message投递到MessageQueue的工具真正循环取消息的是Looper。每个线程只能有一个Looper它通过ThreadLocal保存所以你在子线程里直接new Handler会直接抛异常提示“Cant create handler inside thread that has not called Looper.prepare()”。主线程的Looper是系统帮我们创建好的。ActivityThread的main方法里会调用Looper.prepareMainLooper()再调用Looper.loop()进入循环。所以主线程才可以直接new Handler。那主线程一直在loop循环里为什么不会把CPU吃满因为MessageQueue.next()在没有消息时会调用native方法进入epoll等待线程阻塞在native层不消耗CPU只有新消息入队或者遇到同步屏障、IdleHandler空闲任务时线程才被唤醒。这也是为什么主线程Looper可以常年不退出也不占资源。扩展一下如果Looper.loop()在子线程中执行后不退出会带来什么影响它会一直阻塞子线程子线程里的Handler队列会持续处理消息。很多后台HandlerThread的核心原理就是先Looper.prepare再Looper.loop再通过getLooper()给外部Handler绑定使用。理解了这个机制后面看HandlerThread、IntentService源码就很顺畅了。3. 必考的并发与跨进程通信题标准解法与踩分点3.1 线程安全的集合ConcurrentHashMap凭什么取代HashTable并发题里出现频率最高的不是线程序列而是集合类的线程安全设计。B卷有一道简答题要求说明ConcurrentHashMap在JDK 1.7和1.8版本间的实现差异并解释为什么在Android开发中更推荐它而不是HashTable。先回答版本差异。JDK 1.7的ConcurrentHashMap用的是分段锁整个Map分成若干Segment段每个段单独加锁。读操作不加锁写操作只锁住当前段所以并发度取决于段数量默认16。JDK 1.8改成了数组加链表转红黑树的结构放弃分段锁使用CAS加synchronized锁住单个桶的首节点。写锁粒度更细并发能力更强链表过长时还能转红黑树避免极端hash冲突下的查询退化。然后解释为什么不用HashTable。HashTable所有的get和put都锁住整个表并发场景下相当于把所有读写串行化性能很差。Collections.synchronizedMap也类似只是包了一层同步代码块本质还是粗粒度锁。Android开发里的场景通常是“主线程读、子线程写”或者多个子线程更新同一个内存缓存。使用ConcurrentHashMap时因为内部做了分段或锁桶处理读操作几乎无阻塞写操作也只会锁住局部这对UI线程卡顿控制很有价值。不过要提醒一点ConcurrentHashMap的迭代器是弱一致的它不保证在迭代期间看到其他线程的修改也不会抛ConcurrentModificationException这在并发场景下是特性也是坑读取时需要自己判断数据是否允许短暂不一致。3.2 Binder与AIDL跨进程通信为什么偏偏用它Binder是安卓面试绕不开的一道坎B卷自然也考了。简答题通常会这样问为什么Android要选择Binder作为主要的跨进程通信方式而不是传统IPC里的Socket或共享内存AIDL生成的Stub和Proxy是怎么配合工作的Binder最大的优势是一次拷贝。发送方进程的数据通过内核拷贝到目标进程接收缓冲区时借助mmap映射机制整个数据链路只需要一次数据拷贝而Socket通信至少需要两次拷贝。对于高频Binder调用这个性能差距非常明显。第二个优势是安全性。Binder通信由内核驱动参与身份校验每个进程都有独立的UID系统可以规定某个接口只有特定进程能调用。相比之下Socket是网络协议栈端口容易被探测和冒充共享内存虽然快但缺少统一的权限控制还需要自己处理并发同步。AIDL的配合逻辑也很清晰定义好接口后编译器会生成Stub和Proxy。服务端继承Stub实现业务逻辑Stub是一个Binder实体它常驻在服务进程的Binder线程池中。客户端拿到的是Stub的Proxy对象调用Proxy的某个方法时内部会把方法标志位和参数写入Parcel然后调用transact发送到内核服务端Stub的onTransact方法收到后解包在Binder线程池里执行对应实现。还有一个加分项跨进程调用时客户端进程不会被阻塞太久可以通过async接口让Binder调用异步执行如果服务端进程意外死亡客户端可以通过linkToDeath注册DeathRecipient在进程死亡时收到通知并自动重连。这种“可感知、可恢复”的设计才是Binder体系真正值得深挖的价值点。3.3 进程优先级与保活别把保活答成对抗系统进程优先级这道题考察的是开发者对安卓进程生命周期的理解。标准问法是“系统在内存不足杀进程时会按什么顺序清理开发者有哪些合规手段保证重要任务不被杀掉”进程优先级从低到高依次是空进程、后台进程、服务进程、可见进程、前台进程。空进程不持有任何组件最容易被杀后台进程通常是被切到后台的Activity系统会优先回收服务进程跑着Service但不可见可见进程指那些用户能感知但不在前台焦点的组件比如正在后台播放视频的界面被对话框覆盖前台进程与用户直接交互系统会极力保证它存活。大多数同学的答案到这里就结束了能拿基础分但拿不到高分。要拿高分关键在于“保活”的答法。系统杀进程的本质是资源换资源如果应用长期在后台且对用户没有价值被清理是正常现象。所以标准答案应该导向合法提升进程价值用户真正在听的任务用前台Service加通知提升到前台进程延迟任务用WorkManager或JobScheduler交给系统在合理时机批量处理偶尔需要精确闹钟可以谨慎使用AlarmManager但要注意省电限制。千万不要在笔试或面试里大谈“双进程守护、互相拉起、隐藏图标、刷机驻留”这类灰色保活思路。一方面这些方案在Android 8.0之后基本失效另一方面厂商对后台耗电和应用对抗的治理越来越严格投机的底线越来越低。你表达出“我是站在用户和系统双向体验角度考虑问题”的态度比堆砌什么黑科技都加分。4. 自定义View与性能优化大题面试官真正想看什么4.1 自定义View测量布局实现一个自动换行的FlowLayoutB卷的自定义View大题一道很典型的题目是“请设计一个支持自动换行的标签布局FlowLayout说明在onMeasure中如何计算宽高在onLayout中如何摆放子View。”这道题不是让你背ViewGroup的流程而是考察你有没有独立实现过一个可复用的容器。我建议按三层回答。第一层是测量。在onMeasure中先遍历所有子View逐个调用measureChildWithMargins拿到每个子View的width与height。测量完一个子View后判断当前行已有的宽度加上这个子View的宽度是否超过父容器可用宽度超过就换行。行高取当前行所有子View中最大的那个整个ViewGroup的高度是所有行高加行间距的和。如果父容器传进来的MeasureSpec是AT_MOST则需要遵循wrap_content语义计算出的尺寸作为最终尺寸如果是EXACTLY就优先使用父容器给定的尺寸但子View依然要按自己的测量结果摆放。第二层是布局。onLayout里按行遍历记录每一行的起始X、当前行Y、行高。把子View逐个摆到对应位置X方向累加宽度换行时Y累加行高和行间距。这里要注意child.getVisibility()为GONE的View要跳过margin也要计入宽度计算否则布局在两个不同设备上会出现明显偏差。第三层是加分的边界处理。比如父容器宽度特别窄单个子View已经超出了可用宽度这时候要判断是否强制占满一行还是选择换行比如整个布局没有子View时onMeasure应该返回0而不是父容器全宽再比如如果支持最大行数限制要在测量阶段就考虑最后一行无法完整显示时如何省略。这些细节才是面试官区分“会调用API”和“真做过自定义View”的关键。4.2 卡顿与内存泄漏从现象到根因的完整排查链路性能优化的大题里有一道很接地气“线上用户反馈某个列表页滑动卡顿你的排查思路是什么”这道题没有唯一答案但你的回答顺序能直接反映工程经验。我推荐的排查链路是先复现再抓数据最后定位。复现时打开开发者选项里的“GPU渲染分析”看列表滑动时帧耗时是否连续超过16ms。超过的话打开Systrace抓sched、input、 app三个tag观察主线程消息里哪些操作的耗时异常。Systrace能看系统调度却不好看业务方法调用链这时候再用BlockCanary或者ThreadSampler抓主线程卡顿时的方法栈。定位到具体方法后还要继续往下找根因。常见的卡顿根因集中在四个地方主线程执行了网络请求、大文件IO、Json解析这类耗时任务布局层级过深导致measure和layout阶段耗时较长特别是嵌套多层LinearLayout和RelativeLayout列表复用失效在Adapter的getView里频繁inflate布局或者加载大图内存抖动大量对象在短时间内频繁创建和回收触发GC导致掉帧。内存泄漏的排查也有对应套路。LeakCanary的核心原理是用Application.ActivityLifecycleCallbacks监听Activity销毁在onDestroy后用弱引用加ReferenceQueue包裹这个Activity再等一段时间看引用队列里是否出现了这个对象。如果没有出现说明Activity仍然被强引用持有判定为泄漏。常见泄漏根因包括非静态内部类Handler持有外部Activity、静态单例持有Activity或View、匿名内部类对象被异步任务持有、注册了系统广播没有反注册、WebView销毁不彻底。这里能拿高分的回答不是单纯罗列工具而是把工具和场景串起来先用Systrace定位主线程耗时再用Memory Profiler看GC频率和对象分配最后用LeakCanary确认泄漏每一步都指向下一步的排查方向。4.3 网络与架构设计题MVVM为什么到现在还能打B卷的设计题里有这样一道经典题“设计一个用户信息模块包含网络请求、本地缓存和UI状态展示请给出你的架构方案。”2019年标准答案是MVP或者MVVM放到今天依然成立但选型理由要更扎实。我建议直接答MVVM因为它在生命周期和可测试性上优势明显。ViewModel负责保存UI页面所需的数据通过LiveData或StateFlow观察数据变化。页面销毁时ViewModel不会跟着销毁而是等到ViewModelStore真正清空时才走onCleared方法所以旋转屏幕不会丢失数据。Repository层是唯一的数据来源。UI层调ViewModel的load方法ViewModel调RepositoryRepository再决定是走网络还是走本地缓存。网络层Retrofit加OkHttpRetrofit把接口转成CallOkHttp负责连接池、超时、拦截器缓存可以结合OkHttp的Cache和HTTP的Cache-Control也可以在Repository里维护一个私有数据库判空逻辑。协程或者RxJava负责线程切换。一个比较规范的伪代码如下class UserViewModel : ViewModel() { private val repository UserRepository() private val _user MutableLiveDataResultUser() val user: LiveDataResultUser _user fun loadUser(id: String) { viewModelScope.launch { val cached repository.getLocalUser(id) if (cached ! null) { _user.value Result.success(cached) } val fresh repository.fetchRemoteUser(id) _user.value Result.success(fresh) } } }这里有一个踩分点每次进页面都先用本地缓存刷一次UI再从网络更新用户感知到的是“秒开”而不是白屏。真正的架构设计能力就在于这些细节的取舍而不仅仅是选一个框架名字。5. 小米特色的系统协作与多屏适配题5.1 厂商定制的权限与省电差异为什么在小米上表现不一样小米的笔试天然会带一点“系统应用色彩”有一类问题很值得关注“同一个应用在原生Android设备和某些国产定制系统上表现完全不同可能是什么原因如何适配”这题本质考的是厂商ROM对后台机制的加强管控。MIUI在Android原生后台限制之上又增加了自启动权限、省电策略、后台清理、通知权限等多个维度的独立开关。原生系统里一个Service能稳定在后台运行到了定制系统上可能几分钟就被回收或者用户关了自启动后广播彻底失效。适配思路是“合规优先”。用户主动感知的任务比如播放音乐、导航用前台Service加上可见通知让系统意识到这个进程在前台纯后台任务不要硬保交给WorkManager周期性执行并在首次启动时引导用户把应用加入厂商的电池优化白名单申请敏感权限时主动引导用户打开对应的辅助开关而不是默认用户一定会给权限。经验提醒厂商适配一定要用真机测特别是小米的“省电策略”里有个“神隐模式”如果应用没适配在后台收不到消息的情况非常常见。你要在笔试题答案里体现这个维度阅卷人会觉得你是真的经历过线上问题而不是只会写Java的同学。5.2 双屏与折叠屏适配从一道延伸题看Android资源系统的边界B卷还带了一些延伸方向尤其是随着折叠屏和双屏设备出现“Activity在不同的屏幕形态下如何正确显示”逐渐成了热门考点。如果题目问“如何适配内外双屏或折叠屏”你需要答到以下三个层面。第一在Manifest里为Activity声明resizeableActivitytrue允许应用跟随窗口大小变化声明configChanges里屏幕尺寸和方向相关配置时要自己处理配置变更避免Activity被重建后状态丢失。第二布局资源上使用最小宽度限定符比如values-sw600dp、values-sw720dp同时利用WindowManager获取当前窗口尺寸判断是哪种形态。第三注意屏幕挖孔区和系统栏安全区域用WindowInsets相关API避开displayCutout否则页面顶部内容会被遮挡。如果是跨平台方案比如Cordova、uni-app隐患又不一样这类框架通常用一个原生WebView承载HTML页面当系统屏幕尺寸变化时WebView的viewport需要重新计算资源加载路径要注意H5地图组件在混合开发里经常出现遮挡问题本质是原生Surface和WebView渲染层级的叠加顺序没处理到位这种情况要回到原生层调整WebView的z轴顺序和背景透明设置。这道题的价值在于它逼着你去了解“多屏环境是一个资源属性”而不仅仅是“把布局文件做responsive一点”。能够把系统Configuration、资源限定符、跨平台框架的局限讲清楚很容易在面试官心里留下好印象。5.3 从B卷看成体系的出题逻辑基础、工程、业务三者闭环把整套B卷放在一起看你会发现出题逻辑有很清晰的闭环。选择题考基础是筛掉那些“框架用得很熟但原理模糊”的同学简答题考工程希望你面对并发、跨进程、保活这种实际问题时有成熟方案设计题考业务理解看你能不能根据具体场景做合理的架构选型和性能权衡。所以准备小米这类大厂笔试不建议只刷“面经高频题”而是把题目当成知识地图上的起点。Handler题背后是线程模型、同步屏障、IdleHandlerBinder题背后是Linux的mmap、Binder线程池、死亡回调自定义View题背后是MeasureSpec、invalidate和requestLayout的区别。真正吃透一套题比刷十套答案有用的多。6. 秋招备战如何用一份真题串起完整知识体系6.1 把真题当“知识索引”而不是“答案库”备考最常见的误区是把真题当成标准答案库背完答案就认为可以上考场。但笔试题最大的价值是告诉你“哪些知识点是高频的、哪些能力是岗位必须的”。我的用法是这样的做完一套题后不着急对答案先把自己不确定的题目圈出来再为每道题写三到五行“这道题考的什么底层原理、关联哪些兄弟知识点、我在哪块理解最弱”。比如错了Handler那道题我会在本子上延伸写主线程Looper在哪初始化、子线程Looper如何退出、MessageQueue什么时候会阻塞、IdleHandler被触发的时机是什么、View.post里的Handler是哪个线程。这样一道题背后至少延伸出五个子问题这五个问题又会在其他笔试题里反复出现。坚持把所有考点都做一遍“伸展运动”你的知识树就自然成型了而不是零散记忆。6.2 按板块组织的八周复习计划没有系统的时间安排很容易纠结在个别偏冷考点上。我整理过一份八周复习线按这套真题的板块划分来安排周次复习板块配套练习第1周Java基础、集合、并发刷ConcurrentHashMap和类加载题第2周JVM内存模型、GC、内存优化手写内存泄漏分析第3周四大组件、启动模式、Handler看ActivityThread源码片段第4周View体系、事件分发、自定义View徒手实现一个FlowLayout第5周网络框架、架构模式、协程完成一个MVVM模块第6周性能优化、稳定性用Systrace抓一次卡顿第7周系统协作、厂商适配、多屏在模拟器上适配分屏第8周真题复盘、模拟笔试整套B卷限时完成每周重点看一到两个专题严格控制“只输入不输出”的时间。同一个知识点与其看十篇博客不如自己写一份总结或者给朋友讲一遍这两种方式的记忆留存率差异非常大。6.3 复盘比刷新题更重要一个实用的追问式复习法我自己在备考后期最受益的习惯是“追问式复盘法”。举例来说我如果做错了一道AIDL选择题我不会只看正确选项的解释而是连续追问为什么系统不直接用Message传递跨进程数据AIDL的data和reply两个Parcel对象什么时候被回收Binder调用是同步的如果我发起跨进程调用后服务端卡住了客户端会发生什么每个追问都对应一个技术点也正是笔试和面试中真正考察“有没有想深”的切入点。等到能不看资料把这些问题闭卷讲清楚再新题旧题一起做就会发现正确率提高得很自然。这种“以题带面、以面攻点”的复习方式是这个行业里性价比最高的备考路线没有之一。