Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

四大组件与基础

四大组件是 Android 的根基, 中级面试常深挖生命周期, 启动模式, 跨进程.

一, Activity

生命周期

onCreate → onStart → onResume →(运行)→ onPause → onStop → onDestroy; onRestart 用于从 onStop 返回.

关键场景:

  • A 启动 B: A.onPause → B.onCreate → B.onStart → B.onResume → A.onStop(B 的 onResume 在 A 的 onStop 之前).
  • 透明 / Dialog 主题的 B 覆盖 A: A 只 onPause 不 onStop.
  • 横竖屏旋转: 默认销毁重建, 走完整生命周期 + onSaveInstanceState/onRestoreInstanceState; 配 configChanges 可避免重建只回调 onConfigurationChanged.

启动模式 (LaunchMode)

  • standard: 默认, 每次新建实例, 入当前任务栈.
  • singleTop: 栈顶复用, 栈顶是它则走 onNewIntent, 否则新建.(防通知重复打开)
  • singleTask: 栈内唯一, 已存在则复用并清除其上的 Activity (clearTop), 走 onNewIntent.(主页 / 首页)
  • singleInstance: 独占一个任务栈.(来电, 闹钟)

taskAffinity 指定任务栈归属; Intent flag (FLAG_ACTIVITY_NEW_TASK 等) 可动态控制.

生命周期与任务栈推演

上下文片段: 以下日志放在同一应用的 MainActivity(A) 和 DetailActivity(B) 各生命周期回调中; 省略 Manifest 声明和 Log import. 点击 A 的按钮启动 B, 预期日志顺序为 A.onPause -> B.onCreate -> B.onStart -> B.onResume -> A.onStop. 按返回键后, 预期 B 依次 onPause/onStop/onDestroy, A 依次 onRestart/onStart/onResume.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<View>(R.id.open_detail).setOnClickListener {
            startActivity(Intent(this, DetailActivity::class.java))
        }
    }
}

任务栈初始为 [A], A 启动 standard 模式的 B 后为 [A, B]; 从 B 按 Back 只弹出 B, 恢复 A. 若 B 已在栈内且以 singleTask 启动, 例如栈为 [A, B, C], 再次启动 B 后会清除 C, 变为 [A, B] 并回调 B 的 onNewIntent(). 这是针对该模式的推演, 不应把它外推到所有 flag, 跨 task 或厂商定制场景.

二, Fragment

  • 实例与 View 是两套生命周期: Fragment 实例通常经历 onAttach → onCreate → onCreateView → onViewCreated → onStart → onResume; 离开前台时先 onPause → onStop. View 被销毁时走 onDestroyView, Fragment 实例最终被移除或宿主销毁时才走 onDestroy → onDetach.
  • onDestroyView() 是分界点: Fragment 仍可能留在 FragmentManager 或返回栈中, 但其 View 树, View Binding 和所有仅服务于界面的引用都已失效. 在此处置空 _binding, 取消对旧 View 的回调, 不要在实例字段中持有 View.
  • 返回栈与重建: 执行 replace(...).addToBackStack(...) 后, 被覆盖 Fragment 的 View 通常销毁而实例保留; 返回时实例可复用但会重新执行 onCreateView/onViewCreated. 配置变更或进程重建会由 FragmentManager 恢复 Fragment, 应把可恢复 UI 状态放在 ViewModel/SavedStateHandle, 而非假设旧实例或旧 View 仍存在.
  • 观察 UI 状态: 涉及 View 的 LiveData 观察, Flow 收集和 Adapter 绑定使用 viewLifecycleOwner; 这样收集会在 View 销毁时停止, 在新 View 创建后按其生命周期恢复. 不依赖 View 的 Fragment 级工作才使用 Fragment 自身的 lifecycle.
  • commit vs commitNow vs commitAllowingStateLoss: commit 异步; commitNow 同步; 前两者在 onSaveInstanceState 后调用会抛 IllegalStateException, allowingStateLoss 容忍但可能丢状态.
  • 现代用 FragmentStateAdapter + ViewPager2 替代旧懒加载.
  • 为什么用 Fragment 而非多 Activity: 轻量, 复用, 共享 ViewModel, 单 Activity 架构配合 Navigation.

Activity / Fragment 通信: 先确定状态所有权

方式谁拥有数据生命周期与返回栈适用范围错误选型后果
Fragment Result API发送方产出一次性结果, FragmentManager 暂存至接收方可用接收方在 STARTED 后接收, 正常交付后清除; 不是共享可观察状态, 也不替代返回栈选择器返回值, 一次确认结果用于持续状态会丢失状态来源
共享 ViewModelActivity 或 navigation graph 作用域的 ViewModel随作用域存活; 具体页面是否仍在返回栈由 Navigation 管理多个 Fragment 协同编辑或展示同一 UI 状态作用域过大造成状态串页, 把导航事件当持久状态会重复触发
接口回调父 Fragment 或宿主 Activity回调只在当前对象引用有效时发生, 不保存结果局部, 同步的子组件事件持有已销毁 View/Activity 的引用会泄漏或回调到失效页面
Navigation SavedStateHandle当前 NavBackStackEntry结果写回前一个 entry, 与返回操作配合; entry 弹出后状态不再可取从详情页向前一页返回一个结果错把它当全局共享仓库会让结果依赖特定返回栈

选择前先回答 “这是单向结果还是共享状态, 结果应留在哪个返回栈 entry”. Fragment Result 以同一 key 正常交付后会清除; 若出现重复事件, 通常应检查发送方是否再次调用 setFragmentResult, 是否重注册监听器, 以及业务是否把一次性事件建模成可重复消费的状态. 例如详情页编辑完成可写入前一个 SavedStateHandle, 前页消费后清除该键; 跨多个同级页面的持续筛选条件则归共享 ViewModel 管理. 以上是 API 归属示意, 仍需结合页面销毁和进程重建路径验证.

Fragment 懒加载的边界

旧式 “根据可见回调才创建或请求数据” 容易与 FragmentStateAdapter, 返回栈和 onDestroyView() 交错: Fragment 可已创建但 View 不存在, 或不可见页面仍持有请求结果. 现代做法是以 View 生命周期收集 UI state, 由数据层按真正需求惰性加载, 取消或缓存; 可见性只影响渲染或触发意图, 不应成为唯一的数据正确性条件. 列表布局与 View.post 的时机区别见 UI 体系 - View 与自定义 View.

三, Service

  • 启动方式: startService(独立运行, 需手动 stopSelf/stopService) vs bindService(提供绑定通信; 当没有启动状态且最后一个客户端解绑时, 系统可销毁它). 同一 Service 也可同时被启动和绑定, 此时解绑不会停止其启动状态.
  • 前台服务: 必须 startForeground 显示通知 (Android 8+ 后台限制), 用于音乐, 定位, 下载. Android 14+ 还有前台服务类型限制.
  • IntentService 已废弃: 用 WorkManager 或协程替代后台任务.
  • Service 不是线程: 默认运行在主线程, 耗时操作仍需开子线程, 否则 ANR.

IntentService: 历史串行后台组件

  1. 历史原理: IntentService 在内部工作线程按顺序处理每个 Intent, 任务完成后可自行停止, 用来避免普通 Service 在主线程执行耗时工作.
  2. 限制与状态: 平台 android.app.IntentService 自 Android 11/API 30 废弃; 它不提供任务约束, 可靠重试或现代后台执行保证, Android 8.0 及以上的后台执行限制也使其不适合新任务. 新项目不应继续采用.
  3. 现代替代: 可延迟且需保证执行的工作使用 WorkManager; 用户明确感知, 必须持续运行的工作使用声明正确类型并展示通知的前台服务; 与页面生命周期绑定的短任务使用 ViewModel 作用域协程. 线程协作原语见 多线程并发专题.

回到主线程: runOnUiThread 不是生命周期保护

Activity.runOnUiThread { ... } 会在非主线程时把代码投递到主线程, 调用方已在主线程时可直接执行. 它只解决线程亲和性, 不保证 Activity, Fragment View 或业务请求仍有效: 回调排队期间页面可能销毁. 页面逻辑优先由 ViewModel 暴露可恢复的 UI state, 用生命周期感知收集协程渲染; 临时 View 操作须先确认 View 生命周期有效. runOnUiThread 可用于遗留回调的窄桥接, 不应成为异步架构.

绑定 Service: 客户端与服务端的最小闭环

上下文片段: 将 Service 声明在同一应用 Manifest 中, Activity 在可见期间绑定. 此示例只演示同进程 Binder 通信, 不是跨进程 AIDL 示例. 调用 nextCount() 两次, 预期依次得到 1, 2; 离开页面会解绑.

class CounterService : Service() {
    private var count = 0
    inner class LocalBinder : Binder() { fun nextCount(): Int = ++count }
    override fun onBind(intent: Intent): IBinder = LocalBinder()
}

class CounterActivity : AppCompatActivity() {
    private var binder: CounterService.LocalBinder? = null
    private var bound = false // bindService() 已成功注册该 ServiceConnection
    private var connected = false
    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) { binder = service as CounterService.LocalBinder; connected = true }
        override fun onServiceDisconnected(name: ComponentName) { binder = null; connected = false }
    }
    override fun onStart() { super.onStart(); bound = bindService(Intent(this, CounterService::class.java), connection, BIND_AUTO_CREATE) }
    override fun onStop() { if (bound) { unbindService(connection); bound = false }; binder = null; connected = false; super.onStop() }
}

bound 表示 bindService() 返回成功, 因此必须配对解绑; connected 表示 Binder 已通过 onServiceConnected() 到达, 只有此时才可调用 binder. 存在 “已绑定但尚未连接” 的竞态窗口: 页面可能在回调到达前进入 onStop(), 仍要按 bound 解绑并清理本地引用. onServiceDisconnected() 表示连接意外断开 (例如承载 Service 的进程被杀), 正常调用 unbindService() 不会因此回调.

四, BroadcastReceiver

  • 注册方式:
    • 静态注册 (Manifest): 适合系统或跨应用广播; Android 8.0 起对很多隐式广播做了限制, 普通应用不要依赖静态注册来长期唤醒进程.
    • 动态注册 (registerReceiver): 跟随 Activity/Fragment/Service 生命周期, 常在 onStart/onResume 注册, onStop/onPause 反注册, 用于只在界面可见或组件存活时接收.
  • 分发流程: sendBroadcast/sendOrderedBroadcast → ActivityManager/系统广播队列匹配 IntentFilter → 找到静态/动态 Receiver → 目标进程已在则直接回调,未启动且允许则拉起进程 → 主线程调用 onReceive.
  • IntentFilter 匹配: action 必须匹配; filter 声明 category 时, Intent 中的每个 category 都必须被 filter 包含; data 的 URI, MIME type, scheme/host/path 还须满足声明. 不要以为只写同一个 action 就必然接收, 也不要用宽泛 data 暴露未校验输入.
  • 类型边界:
    • 普通广播: 异步分发, 多个 Receiver 接收顺序不应作为业务依赖.
    • 有序广播: 按优先级依次分发, 前一个 Receiver 可通过 setResult* 改结果, 也可在允许的场景中 abortBroadcast() 终止后续接收; 因此更像一条责任链.

系统广播与 LocalBroadcastManager 的边界

维度BroadcastReceiver / 系统广播机制LocalBroadcastManager
进程与分发范围由系统按 IntentFilter 分发, 可在同一应用, 其他应用和系统组件之间传递仅在同一应用进程内向已注册接收者分发, 不经过系统广播机制
跨进程可以, 目标组件可位于其他进程; 是否可达还受 Intent, 系统限制和 Manifest 影响不可以; 多进程应用的不同进程彼此不可见
注册与接收者支持 Manifest 静态注册和运行时动态注册; 系统还可能在允许时启动目标进程仅支持运行时注册, 不会拉起进程, 进程死亡后没有接收或缓存
安全边界导出的 Receiver, 隐式 Intent 和宽泛 Filter 可能接受外部输入; 应设置 exported, 发送 / 接收权限或显式组件, 并校验 action, data 与 extras不会被其他应用直接发送或接收, 但不是天然绝对安全: 同进程不可信代码, 错误的事件设计和敏感数据日志仍是风险, 也不提供授权或输入校验的替代品
性能与历史场景适合系统事件或跨应用通知; 不应把广播当高频应用内事件总线, 分发, 进程和生命周期成本会放大问题曾用于旧架构的应用内事件总线, 避免跨进程 IPC 并非 “零成本”; 生命周期管理, 全局耦合和高频事件问题依然存在

LocalBroadcastManager 1.1.0 已完全废弃, 新代码不应使用. 应用内持续状态优先由 Repository 暴露 StateFlow/LiveData, 短暂事件可用作用域明确的 SharedFlow 或显式回调; 观察者仍应绑定恰当生命周期, 不把 “本地” 误当成完整的安全模型.

粘性广播: 旧状态快照不是状态流

平台粘性广播 API 仍存在, 但多数应用不应发送或依赖它, 不应杜撰统一的废弃版本. 粘性广播会保存最后一次 Intent, 之后注册的接收者可能立即拿到该旧值; 这会带来数据陈旧, 来源与时序难以判断的问题. 若广播可被外部接收或伪造, 还会扩大敏感数据泄露和错误状态注入的风险. 平台保留的少数系统状态场景应按其文档处理; 应用自己的状态以 Repository + StateFlow/LiveData 等可追踪, 可建模的状态流保存和观察, 而非用 sticky Intent 充当状态仓库.

class BatteryReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // onReceive 运行在主线程,只做轻量解析;耗时任务交给 WorkManager/协程/Service
        val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
        Log.d("BatteryReceiver", "level=$level")
    }
}

private val receiver = BatteryReceiver()

override fun onStart() {
    super.onStart()
    registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
}

override fun onStop() {
    unregisterReceiver(receiver)
    super.onStop()
}

常见错误:

  • 在 onReceive 中做网络 / 数据库等耗时操作, 导致主线程卡顿甚至 ANR; 需要异步任务时用 goAsync() 争取短暂收尾, 或转交 WorkManager / 前台 Service.
  • 动态注册后忘记反注册, 导致组件泄漏或重复收到广播.
  • 把广播当作应用内事件总线, 导致来源不清, 生命周期难控; 中级面试更推荐说明 “跨组件 / 跨应用通知可以用广播, 应用内状态流优先用 Flow”.
  • 忽略 Android 8.0+ 隐式广播限制, 把 Manifest 静态注册当成稳定后台唤醒手段.

面试答题结构: 先说 “注册方式与生命周期”→ 再说 “普通 / 有序广播分发差异”→ 补 “8.0+ 限制与现代替代”→ 最后强调 “onReceive 主线程, 轻量处理, 及时反注册”.

五, ContentProvider

  • 定位: 跨进程数据共享的标准组件, 用 content://authority/path/id 形式的 URI 寻址, 对外暴露 query/insert/update/delete/openFile 等接口. 系统通讯录, 媒体库就是典型例子.
  • 对象关系:
    • ContentResolver: 调用方入口, 不直接依赖 Provider 实现类.
    • ContentProvider: 被调用方组件, 负责权限校验, URI 分发, 数据读写.
    • UriMatcher: 把不同 URI path 映射到不同业务分支, 避免手写大量字符串判断.
  • 跨进程调用流: 调用方 ContentResolver.query(uri) → 系统根据 authority 找到目标 Provider → 必要时启动目标进程并创建 Provider → 通过 Binder 进入 Provider 的 query/insert/update/delete → Provider 访问 SQLite/文件/内存数据 → Cursor/结果返回调用方.
class UserProvider : ContentProvider() {
    private val matcher = UriMatcher(UriMatcher.NO_MATCH).apply {
        addURI("com.example.user", "users", 1)
        addURI("com.example.user", "users/#", 2)
    }

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?,
    ): Cursor? = when (matcher.match(uri)) {
        1 -> queryAllUsers()
        2 -> queryUser(uri.lastPathSegment!!.toLong())
        else -> throw IllegalArgumentException("Unknown uri: $uri")
    }
}

// 调用方只依赖 URI + ContentResolver
val cursor = context.contentResolver.query(
    Uri.parse("content://com.example.user/users/42"),
    null,
    null,
    null,
    null,
)
  • 为什么能早于 Application 初始化: 系统在创建应用进程时会先安装并创建该进程声明的 ContentProvider, 然后再进入 Application.onCreate(). Jetpack App Startup 利用这个时机做库初始化, 但现代工程应收敛自动初始化, 避免启动链路不可见, 冷启动变慢.
  • 权限与边界: 对外暴露 Provider 要配置读写权限, exported, URI grant 或签名级权限; 内部初始化 Provider 不应暴露敏感数据. Provider 方法可能被跨进程并发调用, 数据库访问要考虑线程安全和事务.

ContentObserver 订阅的是 URI 变化

ContentResolver.registerContentObserver(uri, notifyForDescendants, observer) 订阅的是某个 Provider URI 的数据变更通知, Provider 在数据成功修改后通过 notifyChange(uri, null) 告知观察者. 它不读取数据, 也不替代 ContentResolver.query()/insert()/update()/delete() 的访问链路; 收到通知后, 调用方通常重新查询或让 Repository 刷新状态. 观察者必须按注册者生命周期注销, 跨进程调用仍需遵循 Provider 的权限和数据一致性约束.

常见错误:

  • 只记 “CRUD + URI”, 说不清 ContentResolver → authority → Provider → Binder 的调用链.
  • 把 Provider 当普通单例初始化器滥用, 导致 SDK 隐式启动, 冷启动耗时难定位.
  • 忽略 exported/permission/grantUriPermission, 把内部数据暴露给其他应用.

面试答题结构: 先解释 “URI + ContentResolver/Provider 解耦”→ 再画出 “跨进程 Binder 调用链”→ 举 “通讯录/媒体库/App Startup” 例子 → 最后补 “权限, 并发, 启动成本” 的坑.

六, 序列化与 Context

  • Serializable vs Parcelable: Serializable 是 Java 反射序列化, 慢, 产生大量临时对象; Parcelable 是 Android 专为内存 / IPC 设计, 快但代码繁琐 (Kotlin 用 @Parcelize 自动生成).跨进程 / Intent 传对象用 Parcelable.
  • Context 类型: Application Context (全局, 生命周期最长, 不能用于 UI/Dialog), Activity Context (带主题, 可弹窗, 注意泄漏). getApplicationContext 用于单例避免持有 Activity.

进阶补充: 现代组件 API 与版本限制

Activity Result API

startActivityForResult 已不推荐. Activity Result API 把启动和结果回调绑定到 lifecycle, 避免配置变更后的回调丢失.

运行时权限

Android 6.0 后危险权限运行时申请; Android 13 引入通知权限; Android 14 对前台服务类型和后台启动有更多限制. 面试要按版本讲, 不要只背一套老流程.

Fragment 返回栈与状态丢失

commit() 是异步; commitNow() 立即执行但不能加入 back stack. onSaveInstanceState 后再提交可能 state loss. setMaxLifecycle 常用于 ViewPager2 控制页面生命周期.

状态丢失时间线: 系统为恢复保存 Activity 状态 -> 异步网络回调到达 -> 回调试图 commit() 新 Fragment -> 进程若随后被杀, 已保存状态没有这笔事务 -> 恢复后的界面与用户最后看到的界面不一致. 因此不要把 “ 捕获异常后改用 commitAllowingStateLoss()“ 当默认修复. 症状是 Can not perform this action after onSaveInstanceState; 证据是生命周期日志与事务提交时机; 定位到晚到回调后, 应将结果放进 ViewModel/UI state, 在 RESUMED 时由 UI 渲染可恢复状态; 最后旋转, 后台恢复与返回栈均需验证.

前台 Service 类型

前台服务必须有用户可见通知, 新版本要求声明类型, 如 location, mediaPlayback, dataSync. 不能把前台服务当万能保活工具.

追问: 为什么 Fragment 会出现状态丢失? 因为系统已保存 Activity 状态后, 再提交 Fragment 事务无法保证恢复一致性.


版本化平台规则矩阵

后台启动 Activity, 广播接收, 前台服务, 通知权限和 android:exported 都是版本 /targetSdk 敏感规则. 设计与面试回答按 “OS/API, targetSdk 条件, 组件类型, 是否用户可见, 允许的豁免, 商店政策” 记录矩阵, 不背一个跨版本绝对结论. Manifest 暴露组件遵循最小权限, exported 组件还需权限, 输入验证和调用方鉴权. 具体规则统一回到 Android 版本适配.

高频面试题

Q1: singleTask 和 singleInstance 区别? singleTask 在所属任务栈内唯一, 可与其他 Activity 共栈; singleInstance 独占一个任务栈, 栈内只有它一个.

Q2: 横竖屏切换 Activity 生命周期? 如何保存数据? 默认销毁重建 (onPause→onStop→onDestroy→onCreate→…).用 onSaveInstanceState 保存临时数据, 或用 ViewModel (配置变更时存活) 保存. 配 android:configChanges 可避免重建.

Q3: onSaveInstanceState 何时调用? 和 onPause 顺序? 在 Activity 可能被系统销毁前调用 (如旋转, 内存不足, 切后台). Android P 后在 onStop 之后调用. 正常返回键退出不调用 (因为是用户主动销毁).

Q4: Serializable 和 Parcelable 怎么选? 内存传递, IPC, 性能敏感场景用 Parcelable; 持久化到磁盘或简单场景可用 Serializable. Kotlin 用 @Parcelize 减少模板代码.

Q5: 为什么不能用 Application Context 弹 Dialog? Dialog 需要 Activity 的主题和 Window token, Application Context 没有, 会抛 BadTokenException.

Q6: Service 和 Thread 区别? Service 能做耗时操作吗? Service 是组件, 运行在主线程, 不是线程; 它的意义是 “后台运行, 可被系统管理”: 承载它的进程会被系统判为更重要而更不易被杀, 但线程调度优先级不变. 耗时操作必须在 Service 内另开线程, 否则 ANR.

Q7: 8.0 之后后台限制有哪些? 后台 Service 受限 (需前台服务 + 通知), 隐式广播大量禁止静态注册, 后台定位受限. 推荐用 WorkManager 处理可延迟的后台任务.