九个阶段
每个阶段都包含相同的组成部分:为什么重要、你必须知道什么、想深入时学什么、从哪里开始、它为你准备了哪些面试问题,以及一个用来检验自己的练习。
第一阶段:Kotlin 与数据结构算法基础
Kotlin 是 Android 的语言。掌握语法很容易。但了解这门语言为你做了什么、没做什么,则需要更长时间。这一阶段还包括数据结构与算法(DSA),大多数 Android 面试仍然会在编码轮中考察这部分内容。
必须掌握
- 类型:
val和var、类型推断、空安全(?.、?:、!!、let)、智能转换 - 函数:默认参数和命名参数、扩展函数和中缀函数、Lambda、高阶函数、内联函数和内联具体化(reified)
- 类:数据类、密封类、枚举类、对象、伴生对象、值类、接口、可见性
- 泛型:变型(
in、out)、星投影(*)及其存在的原因 - 集合:只读与可变、序列、
map、filter、fold、groupBy - 相等性:
==与===、equals和hashCode - 惯用法:作用域函数、
by lazy、类委托、lateinit、异常 - 设计:面向对象编程、SOLID 原则以及在 Android 中你会遇到的模式(建造者、工厂、观察者、策略、适配器、单例)
- 数据结构与算法基础:大 O 表示法、数组、字符串、哈希映射、链表、树、图、递归、基础动态规划
- 工具:Git、使用 Kotlin DSL 和版本目录的 Gradle 基础
深入学习
- Kotlin 与 Java 互操作(
@JvmStatic、@JvmOverloads、平台类型) - 阅读 Kotlin 生成的字节码
- DSL 构建器、契约(contracts)、上下文参数
- 使用 KSP 进行注解处理
从哪里开始:Kotlin 官方文档,然后阅读 Kotlin 2.4 的新特性,了解语言的发展方向。
面试问题
lateinit与by lazy:分别在什么时候使用?- 扩展函数编译成什么?它能访问私有成员吗?
- 为什么
List<String>可以传递给期望List<Any>的地方,而MutableList<String>不行? - (高级)
inline在调用点改变了什么?为什么reified需要它? - (高级)
by lazy默认如何处理线程?你什么时候会改变它?
试试这个:编写一个包含四种状态的密封 UiState 和一个处理它们的 when 表达式。添加第五种状态。注意编译器做了什么,并解释为什么这就是密封类型的意义所在。
第二阶段:Android 平台
这是人们经常跳过的阶段,也是许多生产环境 Bug 的藏身之处。平台也在不断变化。面向 Android 16 的应用不能再选择退出全屏显示,默认启用预测性返回手势,并且在大屏幕上忽略方向和大小的限制。面向 Android 17 的应用将失去大屏幕的退出选项,并且需要新的权限才能访问本地网络。
必须掌握
- 应用组件:Activity、Service、BroadcastReceiver、ContentProvider
- Intent:显式和隐式 Intent、Intent 过滤器、PendingIntent、深层链接和应用链接
- 生命周期:Activity、Fragment(用于现有代码)和进程的生命周期、进程优先级
- 配置变更和进程死亡:SavedStateHandle、rememberSaveable、保存什么和重新加载什么
- 任务栈、回退栈和启动模式
- Context 类型以及每种类型在何时可以安全持有
- 资源、限定符和清单文件
- 权限:运行时权限和特殊权限、照片选择器、分区存储
- 通知和渠道;前台服务及其类型
- 后台限制:Doze、应用待机、什么可以在后台运行
- 窗口:全屏显示、边距(系统栏、键盘和挖孔屏所占空间)、预测性返回、大屏幕和折叠屏
- 主线程、Looper 和 Handler
深入学习
- Binder 和进程间通信
- Zygote 以及应用进程如何启动
- App Startup 库、多窗口、App Bundle 和功能分发
- 养成阅读每个 Android 版本“行为变更”页面的习惯
从哪里开始:Activity 生命周期和保存 UI 状态指南。
面试问题
- 当用户旋转屏幕然后按下 Home 键时,请带我走一遍 Activity 的生命周期。
- ViewModel 如何在配置变更后存活?它能撑过进程死亡吗?
singleTop与singleTask:分别在什么时候使用?- 启动的服务与绑定的服务,以及什么时候服务必须在前台运行?
- (高级)如何找出你应用中现有的进程死亡 Bug?
试试这个:打开你的应用,填写一个表单,按 Home 键,然后运行 adb shell am kill <你的包名>。从最近任务中再次打开它。表单内容恢复了吗?
第三阶段:协程与 Flow
许多“偶尔出现”的 Android Bug 都是并发 Bug。这也是 AI 编写的代码看起来正确但实际上错误最多的地方。
必须掌握
- 挂起函数;
launch、async、runBlocking - 作用域:
CoroutineScope、viewModelScope、lifecycleScope - 结构化并发:
Job、父协程与子协程、为什么作用域会等待其子协程完成 - 调度器和
withContext;绝不阻塞主线程 - 取消:它是协作式的;
isActive、ensureActive、重新抛出CancellationException - 异常:传播、
CoroutineExceptionHandler、SupervisorJob、supervisorScope、超时 - Flow:冷流、操作符(
map、combine、flatMapLatest、debounce)、catch - StateFlow 和 SharedFlow、
stateIn和shareIn、WhileSubscribed - 安全收集:
repeatOnLifecycle、collectAsStateWithLifecycle - 使用
callbackFlow桥接回调;使用Mutex保护共享状态 - WorkManager 用于即使应用关闭也必须完成的任务
- 使用
runTest和测试调度器进行测试
深入学习
- Continuation 以及编译器生成的状态机
- Channel 和
select flowOn、buffer、conflate和背压(当生产者比消费者快时)- 线程、线程池和 Java 内存模型基础
- 用于旧代码的 RxJava 互操作
从哪里开始:Kotlin 协程指南,然后阅读 Android 的协程最佳实践。
面试问题
launch与async?- StateFlow 与 SharedFlow:分别在什么时候使用?
- 当一个子协程失败时,其他子协程会发生什么?
supervisorScope如何改变这一点? - 在 ViewModel 中应该在哪里发起 API 调用,以及使用哪个调度器?
- (高级)为什么捕获
CancellationException是危险的?(参见 AI 部分的代码片段) - (高级)当生产者快而消费者慢时,Flow 如何处理?
试试这个:在 coroutineScope 内部使用 async 启动两个请求,并让其中一个失败。另一个会发生什么?切换到 supervisorScope 并解释区别。
第四阶段:Compose UI
谷歌现在表示所有 Android UI 都应该用 Compose 构建。View 仍然有效并获得关键修复,但没有新功能。本阶段的目标不是制作屏幕,而是预测屏幕何时会重组以及为什么会重组。
必须掌握
- Composable 函数、Row、Column、Box,以及修饰符顺序为何重要
- Material 3 和主题
- 状态:
remember、mutableStateOf、rememberSaveable - 状态提升和单向数据流(UDF):状态向下流向 UI,事件向上流动
- 重组、跳过和稳定性
- 副作用:
LaunchedEffect、DisposableEffect、rememberCoroutineScope、derivedStateOf、snapshotFlow - Lazy 列表以及键为何重要
- Navigation 3
- 表单、文本输入和图片加载
- 预览和 CompositionLocal
- 语义和无障碍
- 窗口大小类和自适应布局(适用于手机、折叠屏和平板)
- 基本动画
深入学习
- 三个阶段:组合、布局和绘制
- 自定义布局、Modifier.Node 和 Canvas 绘制
- 快照系统和槽表(Compose 在重组之间保存状态的位置)
- Compose 编译器稳定性报告
- 用于现有应用的 View 互操作,用于小部件的 Glance
从哪里开始:Thinking in Compose,然后阅读 Compose 性能指南。
面试问题
- 什么会触发重组?
remember与rememberSaveable?- 什么时候使用
LaunchedEffect,当其键发生变化时会发生什么? - 什么使一个类稳定,为什么这很重要?
- (高级)一个列表在滚动时卡顿。你如何找出原因?
- (高级)Compose 如何在重组之间记住状态?(槽表)
试试这个:在 Layout Inspector 中打开重组计数,滚动一个列表。找到一个重组次数过多的 Composable,并修复它。
第五阶段:架构
谷歌的架构建议是一个很好的基准。他们强烈推荐带有仓库的数据层、单向数据流(UDF)和依赖注入。他们仅在业务逻辑跨多个 ViewModel 重用或 ViewModel 变得过于复杂时才推荐使用领域层。架构应该解决你遇到的问题。一个小型应用不需要每一层。
必须掌握
- 层:UI 层、数据层和可选的领域层
- ViewModel:它拥有什么,它绝不能持有(Context 或 View)
- UI 状态:一个作为 StateFlow 的
uiState,使用数据类和密封类建模 - 一次性事件,例如导航或 Snackbar
- 仓库、数据源和单一可信来源
- MVVM 与 MVI 风格的状态处理
- 依赖注入:首先使用构造器注入、Hilt 模块、作用域和
@HiltViewModel - 层之间的模型映射;对错误进行建模
- SOLID 原则在 Android 代码中的应用
深入学习
- 模块化:功能模块和核心模块、
api和impl模块 - Gradle 约定插件
- Clean Architecture 及其权衡
- 其他 DI 选项以及何时手动 DI 就足够了
- 谷歌的 Now in Android 示例应用作为参考
从哪里开始:应用架构指南和建议页面。
面试问题
- 为什么要暴露 StateFlow 而不是 MutableStateFlow?
- MVVM 与 MVI:各自解决了什么问题?
- Hilt 如何知道如何构建一个 ViewModel?
- 领域层在什么时候才值得引入?
- (高级)如何将一个大型应用拆分成模块,以及如何防止模块之间产生不良依赖?
- (高级)为一个将被其他应用嵌入的 SDK 设计架构。
试试这个:找一个直接调用 Retrofit 的 ViewModel。将调用移到仓库中,编写一个假仓库,并在没有网络的情况下测试该 ViewModel。
第六阶段:数据、网络与离线支持
真正的应用主要处理延迟、丢失或过时的数据。有趣的问题始于网络故障。
必须掌握
- HTTP、REST、状态码和 API 契约
- 使用
kotlinx.serialization或 Moshi 处理 JSON - Retrofit;OkHttp 拦截器、超时和缓存
- Room:实体、DAO、Flow 查询、关系、事务和数据库迁移(在不丢失用户数据的情况下更改模式)
- DataStore(以及旧代码中的 SharedPreferences)
- 离线优先:本地数据库作为唯一可信来源
- Paging 3
- WorkManager 用于同步
- 错误、重试、退避和超时
- 身份验证令牌和安全刷新令牌
- 图片加载和缓存
深入学习
- WebSocket 和使用 FCM 的推送
- GraphQL 和 Ktor 客户端
- SQL 索引和查询性能
- 在多个设备上编辑数据的冲突解决
- 静态数据加密
从哪里开始:数据层指南和离线优先指南。
面试问题
- 你的应用在离线时显示什么?
- 如何安全地编写 Room 迁移?
- 五个请求因令牌过期同时失败。如何只刷新一次令牌?
- 如何构建即时搜索而不在每次按键时发送请求?
- (高级)为一个会离线使用数天的应用设计同步方案。
试试这个:让一个列表屏幕仅从 Room 读取,并在后台从网络刷新。开启飞行模式。屏幕仍然有意义吗?
第七阶段:测试与代码质量
一个在你的手机上成功运行过一次的功能并不算完成。谷歌的建议是优先使用假对象而非模拟对象,并测试 ViewModel、数据层、导航和性能。关键在于选择测试什么,而不是编写更多的测试。
必须掌握
- 本地测试与仪器化测试,以及各自在什么时候值得使用
- JUnit 和清晰的断言
- 假对象与模拟对象
- ViewModel 和仓库测试
- 协程测试:
runTest、测试调度器、替换Dispatchers.Main - Flow 测试
- 使用内存数据库的 Room 测试
- 使用语义的 Compose UI 测试;用于 View 代码的 Espresso
- Lint 和静态分析
- 在每个拉取请求上运行测试的 CI
深入学习
- 截图测试
- Robolectric
- 覆盖率工具,以及为什么覆盖率不是目标
- Gradle Managed Devices 和设备农场
- 发现并修复不稳定的测试
从哪里开始:Android 测试基础。
面试问题
- 假对象还是模拟对象:你更喜欢哪一个,为什么?
- 如何测试使用
viewModelScope的 ViewModel? - 在一个新功能中,你会首先测试什么?
- (高级)你的 UI 测试在 CI 中不稳定。你首先检查什么?
试试这个:故意破坏一个功能,例如在映射器中交换两行代码。是否有任何测试失败?如果没有,编写一个会失败的测试。
第八阶段:生产工程
发布是一项与构建不同的技能。在 2027 年,它还包含几年前不存在的规则:16 KB 页面支持、年度目标 API 截止日期和开发者验证。
必须掌握
- 调试:Logcat、断点、Layout Inspector、Database 和 Network Inspector
- 内存:分析器、堆转储、泄漏、LeakCanary
- ANR:原因以及如何读取跟踪信息
- 渲染:卡顿、帧时序、JankStats
- 启动:冷启动、温启动和热启动
- 基线配置文件和 Macrobenchmark
- R8、keep 规则和应用大小
- 安全:密钥库、网络安全配置、Play Integrity、不在 APK 中泄露密钥
- 发布:签名、App Bundle、Play 测试轨道、分阶段发布、应用内更新
- 监控:崩溃报告(例如 Firebase Crashlytics)、Remote Config 和 Android Vitals
- 平台规则:目标 SDK 升级、16 KB 页面、开发者验证
深入学习
- Perfetto 和自定义跟踪段;日志和网络检查工具,如 Timber 和 Chucker
- 原生崩溃
- Gradle 构建速度和配置缓存
- 功能标志和安全发布
- 隐私审查和 Play 数据安全表单
从哪里开始:性能概览。
这是我最关心的领域,因为“应用感觉很慢”不是一个你能修复的错误报告。你需要先有数据。
面试问题
- 什么导致 ANR,以及如何从生产环境调试 ANR?
- 如何找到内存泄漏?
- 基线配置文件如何加快启动速度?
- (高级)一次发布导致崩溃率上升。带我走过接下来的一个小时。
- (高级)如何将冷启动时间缩短一半,以及如何证明?
试试这个:使用 Macrobenchmark 测量冷启动。添加一个基线配置文件。再次测量并记下两个数字。
第九阶段:系统设计与高级技能
在这个阶段,你不再问“如何构建这个屏幕”,而是开始问“什么会出问题,谁负责”。系统设计不是方框和箭头。它是需求、数据模型、失败案例以及你可以为之辩护的权衡取舍。
必须掌握
- 澄清需求:用户、规模、离线需求、什么最重要
- 应用和后端之间的 API 设计
- 数据模型和缓存策略
- 离线同步和冲突
- 分页和图片加载管道
- 实时更新:WebSocket、FCM、轮询及其权衡
- 后台同步、重试和故障处理
- 身份验证和安全
- 日志记录、指标和监控
- 模块边界和团队所有权
常见设计问题
聊天应用、新闻或社交信息流、相册、离线笔记或待办事项、带过滤器的购物车、地图或行程追踪、视频播放器、文件上传、LRU 缓存、供其他应用使用的 SDK。
深入学习
- Android 内部原理:ART 和垃圾回收、Looper 和 MessageQueue、渲染管线(Choreographer、RenderThread、vsync)
- 领导大型代码库的迁移
- 编写设计文档和指导新人
从哪里开始:应用架构指南和模块化指南。
面试问题
- 设计一个可以离线工作的聊天应用。
- 设计一个像 Instagram 那样图片密集的信息流。
- 设计一个 LRU 缓存。
- (高级)设计一个其他团队将嵌入的 SDK。它的 API 是什么样的,你如何进行版本控制?
- (高级)如何向数百万用户安全地推出一个有风险的变更?
试试这个:在纸上设计一个离线优先的聊天应用。然后列出三种失败情况,例如消息重复发送、顺序错乱或因进程死亡而丢失,以及你的设计如何处理每一种情况。