九个阶段

每个阶段都包含相同的组成部分:为什么重要、你必须知道什么、想深入时学什么、从哪里开始、它为你准备了哪些面试问题,以及一个用来检验自己的练习。

第一阶段: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 是什么样的,你如何进行版本控制?
  • (高级)如何向数百万用户安全地推出一个有风险的变更?

试试这个:在纸上设计一个离线优先的聊天应用。然后列出三种失败情况,例如消息重复发送、顺序错乱或因进程死亡而丢失,以及你的设计如何处理每一种情况。