Android面试套路
Android 常见面试题的分层答法、关键词接龙链路与实战追问——从 ANR 到启动模式、从 Handler 到架构选型。
在 关于我在技术的这一面 里我写过:很多 Android 面试并不是在考「你能不能写出业务」,而是在用一套关键词接龙快速给候选人建模。这篇就是把那些套路整理成一份可查阅的笔记——既方便自己复习,也方便面试时按层级往下追问。
下面先给一张分层答题表:基础层答「会不会用」,进阶层答「为什么」,深入层答「系统 / 框架怎么协作」。再按常见链路展开具体问答。
分层答题总表
| 问题类型 | 问题示例 | 基础层(会用) | 进阶层(原理/机制) | 深入层(框架/系统) |
|---|---|---|---|---|
| ANR | 什么是 ANR?什么时候触发? | ANR 是应用无响应。常见阈值:输入事件约 5s、前台 Service 约 20s、BroadcastReceiver 约 10s(具体以系统版本为准) | system_server 侧监控超时;主线程消息队列长时间得不到处理 | AMS / InputDispatcher 协作;traces.txt 主线程堆栈;Binder 阻塞、锁竞争、IO 阻塞如何区分 |
| 集合并发修改 | forEach 里能否 remove 元素? | 直接 remove 会抛 ConcurrentModificationException | Iterator fail-fast:modCount 与 expectedModCount 不一致 | 用 Iterator.remove()、CopyOnWriteArrayList、并发容器或显式锁;见 Java Lock |
| 多线程/并发 | synchronized 与 ReentrantLock 区别? | synchronized 关键字自动释放;ReentrantLock 需 unlock(),支持 tryLock、公平锁、可中断 | 监视器锁 vs AQS;悲观锁思路 | 偏向锁 → 轻量级锁 → 重量级锁膨胀;CAS 与乐观锁 |
| Handler / Looper / MessageQueue | 为什么 Handler 要绑定 Looper? | Handler 发消息,Looper 循环取消息分发 | 每个线程最多一个 Looper;MessageQueue 按时间排序阻塞取消息 | 主线程 ActivityThread.main() 启动 Looper;IdleHandler;同步屏障与 Choreographer |
| BroadcastReceiver | onReceive 能做耗时操作吗? | 不行,超时可能 ANR | onReceive 在主线程,阻塞即卡 UI | 转 goAsync()、WorkManager、前台 Service;有序广播与粘性广播差异 |
| Activity 生命周期 | 哪一步不宜做耗时操作? | onCreate / onStart / onResume 都在主线程,不宜阻塞 | 启动流程涉及 ActivityThread、Instrumentation | ActivityClientRecord、WindowManager 布局与首帧绘制;冷启动链路 |
| ContentProvider | 初始化能做耗时操作吗? | 不行,Application 启动阶段在主线程初始化 | ContentProvider.onCreate() 早于 Application.onCreate() | 懒加载、异步初始化框架(如 App Startup)规避启动阻塞 |
| 内存泄漏 / GC | Handler 为什么容易泄漏? | 非静态内部类持有外部 Activity 引用 | 静态 Handler + WeakReference<Activity> | 引用链:Message → Handler → Activity;GC Roots 与可达性分析 |
| UI 阻塞 | 主线程耗时如何避免 ANR? | 子线程、线程池、Coroutine、WorkManager | 结果回主线程用 Handler / runOnUiThread / withContext(Main) | Choreographer vs VSYNC;掉帧与 Systrace / Perfetto |
| 启动模式 | singleTask 和 singleInstance 区别? | standard / singleTop / singleTask / singleInstance 四种 | singleTask 栈内复用并 clearTop;singleInstance 独占任务栈 | ActivityStack、TaskRecord;Intent Flags 与 documentLaunchMode |
| View 绘制 | measure / layout / draw 顺序? | 自上而下 measure → layout → draw | MeasureSpec 模式与 wrap_content 二次测量 | 硬件加速、RenderThread、过度绘制排查 |
| Binder / IPC | 为什么跨进程要 Binder? | AIDL、Messenger、ContentProvider 都是 IPC 手段 | 对比 Socket、共享内存:Binder 一次拷贝、有身份校验 | Binder 驱动、BBinder / BpBinder;线程池与 oneway 调用 |
| 架构模式 | MVP 和 MVVM 怎么选? | MVP:Presenter 回调 View;MVVM:View 观察 ViewModel | 数据流单向性、可测试性、生命周期绑定 | MVI + UDF;与 SSOT 的关系 |
| 异步选型 | RxJava 和 Coroutines 怎么取舍? | 老项目 Rx 多;新项目倾向 Coroutines + Flow | 结构化并发、viewModelScope 自动取消 | 见 异步演进 与 Kotlin Coroutine |
关键词接龙:怎么从一道题聊到架构师
面试里常见的不止单题,而是一条链路。上位者不需要每个细节都精通,只要备好题型,顺着关键词往下接即可。下面几条是 Android / Java 岗最高频的链子。
链路一:集合 → 并发
ArrayList forEach remove → ConcurrentModificationException → fail-fast / fail-safe → synchronized / ReentrantLock → AQS / CAS → ConcurrentHashMap → 分段锁(JDK 7)/ CAS + synchronized 桶锁(JDK 8+)
若从 HashMap 切入,链子是:
HashMap 1.7 vs 1.8 → 数组 + 链表 → 红黑树 → 头插法扩容成环(1.7) → hash 扰动、负载因子、树化阈值 → 线程不安全 → ConcurrentHashMap
链路二:主线程 → 消息机制 → ANR
主线程不能做什么 → Handler / Looper / MessageQueue → 消息延时与屏障 → 耗时操作为什么 ANR → traces.txt 怎么看 → Binder 慢调用 / 锁等待 / IO
链路三:四大组件 → 启动 → 进程
Activity 生命周期 → 启动模式 + Task 栈 → Application / ContentProvider 初始化顺序 → 冷启动优化 → 多进程与 Binder
链路四:UI → 性能
布局嵌套过深 → measure 两次问题 → RecyclerView 卡顿 → Bitmap 采样 / 复用 / 池化 → 内存抖动 → GC → 掉帧 → Systrace / Android Profiler
链路五:架构 → 工程化
God Activity → MVP / MVVM / MVI → Repository + SSOT → 依赖注入 Hilt → 模块化 / 路由 → 测试金字塔
经典起手式:ArrayList 能在 forEach 里 remove 吗?
这是 技术那一面 里提到的经典起手式,也是很多面试官用来快速分流的第一题。
fun main() {
val data = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8)
data.forEach {
data.remove(it) // 这样会出现问题吗?
}
}标准答法
不可以。 遍历过程中结构性修改集合,会抛 ConcurrentModificationException。
追问:为什么?
forEach 底层走迭代器。迭代器每次 next() 会检查 modCount == expectedModCount;你对 data 调 remove() 会改 modCount,检查失败即 fail-fast 抛异常。
再追问:怎么安全删除?
| 方式 | 适用场景 |
|---|---|
iterator.remove() | 单线程遍历中删当前元素 |
倒序 for (i in list.lastIndex downTo 0) | 按索引删,避免错位 |
removeIf { }(Java 8+) | 按条件批量删 |
CopyOnWriteArrayList | 读多写少、并发读 |
| 新建集合收集「要保留的」 | 函数式 filter,不改原集合 |
建模意义
- 答「可以」→ 基础并发意识薄弱,通常不必深聊。
- 答「不可以」且能说出
ConcurrentModificationException→ 可接 锁 链路。 - 能主动提到
CopyOnWriteArrayList与适用边界 → 有实战经验。
启动模式:A → B → C → B 返回栈是什么?
technical-side 里那道 SingleInstance 题,是 Android 面试里很典型的「背了定义、一举例就宕机」类型。
设定: ActivityA、ActivityC 为 standard,ActivityB 为 singleInstance。启动顺序 A → B → C → B,再逐个按返回键。
各模式速记
| 模式 | 行为 |
|---|---|
standard | 每次 startActivity 新建实例,入当前 Task |
singleTop | 栈顶已是该 Activity 则复用,触发 onNewIntent |
singleTask | 栈内已有则清顶复用;可能把实例提到前台 |
singleInstance | 独占一个 Task,全局仅此实例 |
本题推演
- A → B:A 在 Task1;B 是
singleInstance,系统为 B 新建 Task2,B 独占 Task2。 - B → C:从 B 启动 C(
standard),C 进入 Task2(与 B 同栈,因为 B 所在 Task 成为前台)。 - C → B:B 已在 Task2 栈内,
singleInstance不会新建 B,而是把 B 之上的 C 出栈(或触发onNewIntent把 B 带到前台)。此时 Task2 栈:B(C 被干掉)。 - 按返回:在 B 按返回 → Task2 空 → 回到 Task1 的 A。在 A 按返回 → 退出。
要点: singleInstance 的 Activity 所在 Task 里,通常只有它自己是「根」,后续 standard 启动的页面会进这个 Task,但不会再新建第二个 B 实例。
面试时画图比背文字靠谱——画两个 Task 框,比干念 API 清晰得多。
Handler 机制:必问三板斧
1. 结构
- Handler:发消息、处理消息(
sendMessage/post/handleMessage)。 - Looper:死循环取消息,分发给 Handler。
- MessageQueue:单链表优先级队列,按
when阻塞在nativePollOnce。
2. 主线程 Handler 从哪来?
ActivityThread.main() → Looper.prepareMainLooper() → Looper.loop()
主线程的 Looper 在 ActivityThread 启动时创建,因此主线程 Handler 默认就有消息循环。
3. 内存泄漏怎么答?
非静态内部类 Handler 隐式持有外部类引用;若 Message 在队列里延时未处理,Activity 已 finish 仍无法回收。
修法: 静态内部类 Handler + WeakReference 持 Activity;或在 onDestroy 里 removeCallbacksAndMessages(null)。
4. 常见追问
| 追问 | 要点 |
|---|---|
post 和 sendMessage 区别? | 最终都进队列;post(Runnable) 封装成 Message,callback 字段执行 |
| 子线程能 new Handler 吗? | 可以,但必须先 Looper.prepare() + Looper.loop(),否则无消息循环 |
| 同步屏障是什么? | 插入屏障 Message 后,同步消息暂停,优先处理异步消息;Choreographer 利用此机制优先处理帧回调 |
ANR:怎么答才像做过线上排查
触发条件(记大概,别死背数字)
| 场景 | 常见超时 |
|---|---|
| 输入分发(点击等) | 约 5s |
BroadcastReceiver | 约 10s(前台)/ 60s(后台,视版本) |
| 前台 Service | 约 20s |
ContentProvider 发布 | 约 10s |
排查步骤
- 拿 traces.txt 或 Bugly / Firebase 上的 ANR 堆栈。
- 看 主线程 在等什么:
BLOCKED(锁)、Native(IO / Binder)、Runnable(自己的耗时逻辑)。 - 对照 Binder 线程 是否也有阻塞——有时是同步 Binder 调用拖死主线程。
- 复现路径 + Systrace / StrictMode 辅助定位。
修法清单
- IO / 网络 / 大 JSON 解析 → 后台线程或 Coroutine
Dispatchers.IO。 - 主线程锁等待 → 调整锁粒度、避免主线程持锁等子线程。
- 启动阶段初始化过重 → 懒加载、App Startup、异步预热。
- 广播 / Provider 里做重活 → 异步化或拆进程。
View 与渲染:RecyclerView 卡顿怎么聊
面试官问「列表滑动掉帧你怎么查」,可以按这条线答:
- 是不是主线程干了活——在
onBindViewHolder里解码大图、算复杂布局。 - 布局层级——
ConstraintLayout扁平化;避免wrap_content嵌套导致多次 measure。 - 图片——采样
inSampleSize、RGB_565、Glide 尺寸与缓存策略。 - 对象创建——滑动时频繁
new触发 GC 抖动,看 Memory Profiler 锯齿。 - 预取与缓存——
setItemViewCacheSize、RecycledViewPool共享、DiffUtil 减少全量刷新。 - 工具——GPU 过度绘制、Systrace 看
Choreographer#doFrame是否超过 16.6ms。
Binder 与跨进程:问到哪一层
基础: Android 四大组件跨进程靠 Binder;AIDL 生成 Stub / Proxy。
进阶: 一次拷贝(相比传统 IPC);由 binder 驱动在内核完成;调用方通过 BpBinder 代理,服务端 BBinder 处理。
深入: transact 同步调用默认占 Binder 线程池;主线程里同步跨进程大流量容易卡 UI;oneway 异步但不保证顺序。
业务里常举的例子:ActivityManager 与 system_server 通信、ContentProvider 跨进程查询、Messenger 轻量 IPC。
架构与 Jetpack:高级工程师常问什么
| 主题 | 基础答法 | 可加深 |
|---|---|---|
| MVVM | View 观察 ViewModel 的 LiveData / StateFlow | viewModelScope 生命周期;配置变更数据保留 |
| MVI | 单一 State + Intent / Event,单向数据流 | 与 Compose 契合;状态可预测、易测试 |
| Repository | 统一数据入口,屏蔽网络 / 本地 | SSOT 原则 |
| Room | SQLite 封装 + Flow 观察 | 迁移、多表关联、与 RemoteMediator 分页 |
| Compose | 声明式 UI、重组 | 稳定性、remember、derivedStateOf 减少重组 |
| Hilt | 编译期 DI,减少手写 Module | 作用域:Singleton / ActivityRetained / ViewModel |
不必在面试里站队「Compose 一定取代 XML」——把团队熟练度、招聘、存量代码、设计稿复杂度讲清楚,比喊口号加分。
异步编程:Callback → Rx → Coroutines
Android 岗几乎都会问异步。建议按 异步演进 的脉络答:
| 范式 | 一句话 |
|---|---|
| Callback | 简单场景够用;链长了嵌套地狱 |
| RxJava | 流式组合强;学习曲线陡,生命周期要管好 |
| Coroutines | 同步写法写异步;结构化并发 + Flow 是当下默认推荐 |
面试金句: 主线程只做 UI;IO 切 Dispatchers.IO;结果回 UI 用 withContext(Main) 或 flowOn;在 viewModelScope 里启动,页面销毁自动取消。
内存与稳定性
OOM 与泄漏
- 泄漏: 静态变量持 Activity、匿名内部类、未注销监听、单例持 Context(应持
ApplicationContext)。 - OOM: 大图、缓存无上限、泄漏堆积。工具:LeakCanary、Heap Dump + MAT。
- GC: 记住「从 GC Roots 不可达则回收」;频繁分配小对象 → 年轻代 GC 频繁 → 卡顿。
Crash 与线上质量
- 混淆堆栈 → 用 mapping 还原。
- Crash 率突增 → 看版本、渠道、系统分布;能否热修 vs 回滚。
- 灰度 5% 指标异常 → 停扩量、保留现场、对比基线。
给面试双方的一点实话
对面试官
- 引导式追问比「连珠炮」更能区分真懂和背题。对方答出关键词后,往他熟悉的项目场景引,比硬抠红黑树左旋右旋体面得多。
- 偶尔答不上来是状态问题,连续答不上来才是盲区——见 技术那一面。
- 用关键词链建模很快,但别用一条链否定一个人;Android 工程师的价值不只在背八股。
对候选人
- 准备2~3 个深挖项目:启动优化、卡顿治理、架构重构、线上事故——比散点背题更能打。
- 八股要会分层答:先 30 秒基础答案,看对方眼神再决定要不要展开 Binder 驱动。
- 真不会的,诚实说「这块我回去补」不丢人;硬编更容易被接龙击穿。
速查:还可以往哪些方向接
若时间充裕,下面这些在 Android 中高级面试里出现频率也很高,可按同样「基础 → 进阶 → 深入」准备:
| 方向 | 示例问题 |
|---|---|
| Kotlin | inline、reified、协程挂起原理、sealed class 使用场景 |
| 网络 | HTTPS 握手、证书锁定、Retrofit 动态代理、超时与重试策略 |
| 存储 | SharedPreferences 陷阱、DataStore vs MMKV、Room 事务 |
| 安全 | 加固、反编译、密钥存放、组件导出风险 |
| Gradle | 编译慢怎么优化、多 Module 依赖、版本对齐 |
| 兼容性 | 分区存储、通知权限、厂商 ROM 差异 |
| 音视频 | 硬解软解、Surface 渲染、RTC 弱网策略(若岗位相关) |
面试是缘分,也是抽样。把套路看懂,是为了知道自己该补哪一块——不是为了在一场对话里证明你什么都会。