Android 四大组件
Android 的四大组件——Activity、Service、BroadcastReceiver、ContentProvider——是应用的骨架。它们不是普通的 Java 类,而是由系统管理生命周期、通过 Intent 通信的"框架组件"。理解四大组件,首先要理解框架为什么要这么划分。
为什么是这四个
操作系统面对的核心问题是:如何让多个应用安全、高效地共享有限的硬件资源?Android 的答案是把应用的行为归纳为四种原型:
| 组件 | 对应的用户需求 | 核心特征 |
|---|---|---|
| Activity | "我想看到界面、和应用互动" | 有 UI,一个 Activity ≈ 一个屏幕 |
| Service | "我想让应用在后台做事" | 无 UI,长时间后台运行 |
| BroadcastReceiver | "我想在某件事发生时做出反应" | 事件驱动,响应式 |
| ContentProvider | "我想让多个应用共享同一份数据" | 跨进程数据共享的标准接口 |
四大组件的共同点:
- 全部在 AndroidManifest.xml 中声明(BroadcastReceiver 也可以动态注册)
- 全部由系统实例化,不能自己
new - 全部通过 Intent 激活(ContentProvider 除外,它通过
ContentResolver+ URI) - 全部有自己的生命周期,由系统管理
Activity:用户界面的载体
Activity 是用户与应用交互的窗口。它的生命周期已在前一篇文章中详细讨论,这里聚焦于几个在"四大组件对比"语境下特别重要的点。
启动方式
// 显式启动——指定具体的 Activity 类
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("item_id", 42)
startActivity(intent)
// 隐式启动——声明 Action,由系统匹配能响应的 Activity
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
startActivity(intent)
任务栈(Task)与启动模式
Android 使用任务栈(Back Stack) 管理 Activity 的层叠关系。用户按返回键就是做出栈操作。
四种启动模式决定了 Activity 如何入栈:
| 启动模式 | 行为 | 典型场景 |
|---|---|---|
standard |
每次启动都创建新实例 | 默认行为,多数页面 |
singleTop |
栈顶已有该实例则复用(调 onNewIntent) |
搜索结果页 |
singleTask |
栈内已有则复用(清掉其上的 Activity) | 主页面、浏览器首页 |
singleInstance |
独占一个任务栈 | 来电界面、系统级页面 |
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
singleTask的一个易错点:它不一定创建新的任务栈。当taskAffinity与当前任务相同时,Activity 仍然在当前栈中。只有taskAffinity不同时才会创建新栈。
Service:后台任务的执行者
Service 用于在后台执行不需要 UI 的长时间任务。但最常见的误解是:Service 运行在单独的线程中——这是错的。 Service 默认运行在主线程,耗时操作必须手动开线程。
两种运行模式
┌──────────────────────────────────────────────┐
│ Service 的两种模式 │
├─────────────────────┬────────────────────────┤
│ 启动模式 (Started) │ 绑定模式 (Bound) │
├─────────────────────┼────────────────────────┤
│ startService() │ bindService() │
│ 独立运行 │ 绑定到客户端组件 │
│ 调用者退出不影响 │ 所有绑定者解绑后销毁 │
│ 通过 stopSelf() 停止 │ C-S 模式,可返回 IBinder│
│ 适合下载文件、同步数据 │ 适合音乐播放、跨组件通信 │
└─────────────────────┴────────────────────────┘
生命周期
启动模式: 绑定模式:
onCreate() onCreate()
→ onStartCommand() → onBind()
→ (运行中) → (客户端绑定中)
→ onDestroy() → onUnbind()
→ onDestroy()
关键细节:一个 Service 可以同时处于"启动"和"绑定"两种模式。此时它只有在 stopService() 被调用且所有客户端都 unbind 之后才会销毁。
onStartCommand 的返回值
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 返回值告诉系统:如果进程被杀,如何重启 Service
return START_STICKY // 系统会重启,但 intent 为 null
// return START_NOT_STICKY // 不重启
// return START_REDELIVER_INTENT // 重启并重传最后一个 intent
}
前台服务(Foreground Service)
Android 8.0(API 26)以后,后台 Service 会在几分钟内被系统杀掉。需要长时间运行的任务必须使用前台服务——它会在通知栏显示一个常驻通知,告知用户"有任务正在运行":
class DownloadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("下载中")
.setSmallIcon(R.drawable.ic_download)
.build()
// API 34+ 需要在 Manifest 声明 foregroundServiceType
startForeground(NOTIFICATION_ID, notification)
// 开子线程执行下载...
return START_NOT_STICKY
}
}
<!-- Android 14+ 必须声明前台服务类型 -->
<service
android:name=".DownloadService"
android:foregroundServiceType="dataSync"
android:exported="false" />
IntentService → WorkManager 的演进
| 方案 | 线程 | 适用场景 | 状态 |
|---|---|---|---|
Service + 手动线程 |
主线程(需手动创建) | 需要精细控制 | 仍可用 |
IntentService |
自动工作线程 | 串行后台任务 | API 30 弃用 |
WorkManager |
系统调度的工作线程 | 可靠的后台任务 | ✅ 推荐 |
Coroutine + Service |
协程 | Kotlin 项目 | ✅ 现代方案 |
WorkManager的优势在于:即使应用退出或设备重启,任务也能保证执行。它底层根据 API 级别自动选择JobScheduler或AlarmManager + BroadcastReceiver。
BroadcastReceiver:事件驱动的响应器
BroadcastReceiver 实现的是发布-订阅模式:组件发布广播,注册了对应 IntentFilter 的接收器会收到并处理。
两种注册方式
| 注册方式 | 生命周期 | 适用场景 | 限制 |
|---|---|---|---|
| 静态注册(Manifest) | 常驻,应用未运行也能接收 | 开机完成、应用安装等 | Android 8.0+ 大部分隐式广播不再支持 |
| 动态注册(代码) | 跟随注册组件的生命周期 | 网络变化、电量变化等 | 需要手动 unregisterReceiver |
// 动态注册——推荐方式
class MyActivity : AppCompatActivity() {
private val networkReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val isConnected = intent.getBooleanExtra(
ConnectivityManager.EXTRA_NO_CONNECTIVITY, false
).not()
// 处理网络状态变化
}
}
override fun onStart() {
super.onStart()
registerReceiver(
networkReceiver,
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
)
}
override fun onStop() {
super.onStop()
unregisterReceiver(networkReceiver) // 必须反注册,否则内存泄漏
}
}
Android 8.0+ 的广播限制
从 Android 8.0(API 26)开始,为了减少后台唤醒和省电:
- 隐式广播(不指定目标包名的广播)的大部分静态注册不再生效
- 仍然有效的例外:
BOOT_COMPLETED、LOCALE_CHANGED等少数系统广播 - 显式广播(指定目标组件)不受影响
- 动态注册不受影响
LocalBroadcastManager(已弃用)
LocalBroadcastManager 曾用于应用内广播(不跨进程,更安全),但已在 AndroidX 1.1.0 中被弃用。替代方案:
- LiveData / Flow:响应式数据流
- EventBus / SharedFlow:事件总线
onReceive 的生命周期约束
onReceive() 方法执行完毕后,BroadcastReceiver 就会被销毁。这意味着:
- 不能做异步操作:开的线程可能还没跑完,进程就被杀了
- 不能显示 Dialog:没有 Activity 上下文
- 执行时间约束:前台广播约 10 秒超时,后台广播约 60 秒
如果需要在收到广播后执行耗时操作,应使用 goAsync() 或启动 Service/WorkManager:
override fun onReceive(context: Context, intent: Intent) {
val pendingResult = goAsync() // 延长生命周期
CoroutineScope(Dispatchers.IO).launch {
// 执行耗时操作...
pendingResult.finish() // 必须手动结束
}
}
ContentProvider:跨应用数据共享的桥梁
ContentProvider 是 Android 中唯一的标准化跨进程数据访问接口。它不通过 Intent 激活,而是通过 ContentResolver + URI 访问。
为什么需要 ContentProvider
直接暴露数据库文件让其他应用读写有严重的安全和一致性问题。ContentProvider 提供了一层抽象:
- 权限控制:通过
<provider>的readPermission/writePermission精确控制访问权限 - 统一接口:不管底层是 SQLite、文件还是网络数据,对外都是 CRUD 接口
- 跨进程安全:通过 Binder IPC 实现,系统保证进程隔离
- 变更通知:
ContentResolver.notifyChange()通知 UI 数据已更新
URI 结构
content://com.example.app.provider/users/42
│ │ │ │
│ │ │ └── ID(可选)
│ │ └── 表名 / 路径
│ └── Authority(唯一标识)
└── Scheme(固定为 content)
核心方法
class UserProvider : ContentProvider() {
override fun onCreate(): Boolean {
// 初始化数据源(如打开数据库)
// 注意:在主线程调用,不要做耗时操作
return true
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
// 根据 URI 匹配查询对应的表
val match = uriMatcher.match(uri)
return when (match) {
USERS -> db.query("users", projection, selection, selectionArgs, null, null, sortOrder)
USER_ID -> {
val id = uri.lastPathSegment
db.query("users", projection, "_id=?", arrayOf(id), null, null, sortOrder)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun insert(uri: Uri, values: ContentValues?): Uri? { /* ... */ }
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int { /* ... */ }
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { /* ... */ }
override fun getType(uri: Uri): String? { /* 返回 MIME 类型 */ }
}
ContentProvider 的初始化时机
一个常被忽略的事实:ContentProvider 的 onCreate() 在 Application.onCreate() 之前调用。
App 进程启动流程:
Application.attachBaseContext()
→ ContentProvider.onCreate() ← 先于 Application
→ Application.onCreate()
很多第三方库(如 Firebase、AndroidX Startup)利用这个特性,通过声明一个空的 ContentProvider 来实现无需手动初始化——只要在 Manifest 中声明 <provider>,库的初始化代码就会在 App 启动时自动执行。
AndroidX 的
App Startup库就是对这个模式的规范化:用一个共享的InitializationProvider统一管理所有库的初始化,避免每个库都注册自己的 ContentProvider 带来的启动延迟。
四大组件协作的全景
一个实际场景展示四大组件如何协作——"音乐播放器":
┌─────────────┐ bindService() ┌─────────────────┐
│ Activity │ ──────────────────→ │ Service │
│ (播放器界面) │ ← onBind(IBinder) │ (后台播放音乐) │
│ │ │ │
│ 显示歌曲列表 │ sendBroadcast() │ ┌─────────────┐ │
│ 控制播放暂停 │ ←───────────────── │ │ MediaPlayer │ │
└──────┬───────┘ (播放状态变化广播) │ └─────────────┘ │
│ └────────┬────────┘
│ │
│ query() 读取歌曲元数据
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ ContentProvider │ │ BroadcastReceiver │
│ (歌曲数据库) │ │ (耳机插拔监听) │
│ │ │ │
│ MediaStore 查询 │ │ 耳机拔出 → 暂停 │
└─────────────────┘ └──────────────────┘
生产高频考点总结
| 问题 | 要点 |
|---|---|
| Service 运行在哪个线程 | 主线程。耗时操作必须手动开线程 |
| 启动服务和绑定服务的区别 | 启动:独立运行,stopSelf 停止。绑定:C-S 模式,所有客户端 unbind 后销毁 |
| Android 8.0 后的广播限制 | 隐式广播的静态注册大部分失效,推荐动态注册 |
| ContentProvider 的初始化时机 | 在 Application.onCreate() 之前 |
| 四大组件的共同点 | Manifest 声明、系统实例化、Intent 激活(CP 除外)、系统管理生命周期 |
| IntentService 被废弃后用什么 | WorkManager(可靠后台任务)或 Coroutine + Service |
| 前台服务的必要条件 | 必须显示通知;Android 14+ 需声明 foregroundServiceType |
| BroadcastReceiver 的 onReceive 中能做什么 | 不能异步、不能耗时(10s/60s 限制)、需要耗时则用 goAsync() 或启动 Service |