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 等)可动态控制。

二、Fragment

  • 生命周期比 Activity 多:onAttach → onCreate → onCreateView → onViewCreated → onStart → onResume ...→ onDestroyView → onDestroy → onDetach
  • commit vs commitNow vs commitAllowingStateLoss:commit 异步;commitNow 同步;前两者在 onSaveInstanceState 后调用会抛 IllegalStateException,allowingStateLoss 容忍但可能丢状态。
  • 现代用 FragmentStateAdapter + ViewPager2 替代旧懒加载;getViewLifecycleOwner 观察 LiveData 防泄漏。
  • 为什么用 Fragment 而非多 Activity:轻量、复用、共享 ViewModel、单 Activity 架构配合 Navigation。

三、Service

  • 启动方式:startService(独立运行,需手动 stopSelf/stopService)vs bindService(绑定,随调用者销毁,可通信)。
  • 前台服务:必须 startForeground 显示通知(Android 8+ 后台限制),用于音乐、定位、下载。Android 12+ 还有前台服务类型限制。
  • IntentService 已废弃:用 WorkManager 或协程替代后台任务。
  • Service 不是线程:默认运行在主线程,耗时操作仍需开子线程,否则 ANR。

四、BroadcastReceiver

  • 注册方式:
    • 静态注册(Manifest):适合系统或跨应用广播;Android 8.0 起对很多隐式广播做了限制,普通应用不要依赖静态注册来长期唤醒进程。
    • 动态注册(registerReceiver):跟随 Activity/Fragment/Service 生命周期,常在 onStart/onResume 注册、onStop/onPause 反注册,用于只在界面可见或组件存活时接收。
  • 分发流程:sendBroadcast/sendOrderedBroadcast → ActivityManager/系统广播队列匹配 IntentFilter → 找到静态/动态 Receiver → 目标进程已在则直接回调,未启动且允许则拉起进程 → 主线程调用 onReceive
  • 类型边界:
    • 普通广播:异步分发,多个 Receiver 接收顺序不应作为业务依赖。
    • 有序广播:按优先级依次分发,前一个 Receiver 可通过 setResult* 改结果,也可在允许的场景中 abortBroadcast() 终止后续接收;因此更像一条责任链。
    • LocalBroadcastManager / 粘性广播:都已不推荐。应用内事件优先用 StateFlow/SharedFlow、回调、Lifecycle-aware observer 或明确的 Repository 状态流。
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 方法可能被跨进程并发调用,数据库访问要考虑线程安全和事务。

常见错误:

  • 只记“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 控制页面生命周期。

前台 Service 类型

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

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


高频面试题

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 处理可延迟的后台任务。