[Android 从零到一] ContentProvider 与跨进程通信:让数据在应用间安全流动
为什么要学 ContentProvider?
在 Android 开发中,每个应用都运行在独立的进程和沙盒里。数据默认是隔离的——你的 App 没法直接读取微信的聊天记录,美团也看不到支付宝的账单。
但有时候,我们确实需要在应用之间共享数据:
- 通讯录 App 想让其他应用读取联系人
- 文件管理器想暴露文件给第三方
- 系统日历想让所有应用查看日程
ContentProvider 就是 Android 提供的标准数据共享机制。它像一座桥梁,让数据在进程间安全、可控地流动。
一、ContentProvider 的本质
1.1 四大组件之一
ContentProvider 是 Android 四大组件之一(Activity、Service、BroadcastReceiver、ContentProvider)。它的核心职责:
- 封装数据:把底层存储(数据库、文件、网络)包装成统一接口
- 跨进程访问:通过 Binder 机制实现 IPC(进程间通信)
- 权限控制:精确控制谁能读、谁能写
1.2 核心概念:URI
ContentProvider 使用 Content URI 来标识数据,格式如下:
content://<authority>/<path>/<id>
| 部分 | 说明 | 示例 |
|---|---|---|
content:// |
固定 scheme | — |
| authority | 唯一标识 Provider | com.example.app.provider |
| path | 数据类型 | users、orders |
| id | 具体记录 | 42 |
实际例子:
content://com.example.app.provider/users → 所有用户
content://com.example.app.provider/users/42 → ID=42 的用户
1.3 核心方法
class MyProvider : ContentProvider() {
override fun onCreate(): Boolean { /* 初始化 */ }
override fun query(uri: Uri, ...): Cursor? { /* 查询 */ }
override fun insert(uri: Uri, values: ContentValues?): Uri? { /* 插入 */ }
override fun update(uri: Uri, values: ContentValues?, ...): Int { /* 更新 */ }
override fun delete(uri: Uri, ...): Int { /* 删除 */ }
override fun getType(uri: Uri): String? { /* 返回 MIME 类型 */ }
}
这和 CRUD 操作一一对应,本质上就是一套标准的 RESTful 数据接口。
二、动手实现一个 ContentProvider
2.1 场景:共享笔记数据
假设我们有一个笔记应用,想把笔记数据共享给其他应用(比如桌面 Widget 或快捷工具)。
第一步:定义 URI 和数据库
object NoteContract {
const val AUTHORITY = "com.example.notes.provider"
val BASE_URI: Uri = Uri.parse("content://$AUTHORITY")
object Notes {
val CONTENT_URI: Uri = Uri.withAppendedPath(BASE_URI, "notes")
const val TABLE_NAME = "notes"
const val COLUMN_ID = "_id"
const val COLUMN_TITLE = "title"
const val COLUMN_CONTENT = "content"
const val COLUMN_CREATED = "created_at"
}
}
第二步:创建 Provider
class NoteProvider : ContentProvider() {
private lateinit var dbHelper: NoteDbHelper
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(NoteContract.AUTHORITY, "notes", NOTES_ALL)
addURI(NoteContract.AUTHORITY, "notes/#", NOTES_SINGLE)
}
companion object {
private const val NOTES_ALL = 1
private const val NOTES_SINGLE = 2
}
override fun onCreate(): Boolean {
dbHelper = NoteDbHelper(context!!)
return true
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
val db = dbHelper.readableDatabase
val cursor = when (uriMatcher.match(uri)) {
NOTES_ALL -> db.query(
NoteContract.Notes.TABLE_NAME,
projection, selection, selectionArgs, null, null, sortOrder
)
NOTES_SINGLE -> {
val id = ContentUris.parseId(uri)
db.query(
NoteContract.Notes.TABLE_NAME,
projection, "_id=?", arrayOf(id.toString()),
null, null, sortOrder
)
}
else -> throw IllegalArgumentException("未知 URI: $uri")
}
// 关键:注册通知 URI,数据变化时自动刷新
cursor.setNotificationUri(context!!.contentResolver, uri)
return cursor
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = dbHelper.writableDatabase
val id = db.insert(NoteContract.Notes.TABLE_NAME, null, values)
return if (id > 0) {
val newUri = ContentUris.withAppendedId(NoteContract.Notes.CONTENT_URI, id)
context!!.contentResolver.notifyChange(newUri, null)
newUri
} else null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val db = dbHelper.writableDatabase
val count = when (uriMatcher.match(uri)) {
NOTES_ALL -> db.delete(NoteContract.Notes.TABLE_NAME, selection, selectionArgs)
NOTES_SINGLE -> {
val id = ContentUris.parseId(uri)
db.delete(NoteContract.Notes.TABLE_NAME, "_id=?", arrayOf(id.toString()))
}
else -> throw IllegalArgumentException("未知 URI: $uri")
}
if (count > 0) context!!.contentResolver.notifyChange(uri, null)
return count
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
val db = dbHelper.writableDatabase
val count = db.update(NoteContract.Notes.TABLE_NAME, values, selection, selectionArgs)
if (count > 0) context!!.contentResolver.notifyChange(uri, null)
return count
}
override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) {
NOTES_ALL -> "vnd.android.cursor.dir/vnd.com.example.notes.note"
NOTES_SINGLE -> "vnd.android.cursor.item/vnd.com.example.notes.note"
else -> null
}
}
第三步:注册 Provider
<manifest>
<application>
<provider
android:name=".NoteProvider"
android:authorities="com.example.notes.provider"
android:exported="true"
android:readPermission="com.example.notes.READ"
android:writePermission="com.example.notes.WRITE" />
</application>
<!-- 声明自定义权限 -->
<permission android:name="com.example.notes.READ"
android:protectionLevel="normal" />
<permission android:name="com.example.notes.WRITE"
android:protectionLevel="signature" />
</manifest>
三、ContentResolver:客户端怎么用
其他应用通过 ContentResolver 来访问 Provider:
// 查询所有笔记
val cursor = contentResolver.query(
NoteContract.Notes.CONTENT_URI,
arrayOf("title", "content", "created_at"),
null, null,
"created_at DESC"
)
cursor?.use {
while (it.moveToNext()) {
val title = it.getString(0)
val content = it.getString(1)
Log.d("Note", "$title: $content")
}
}
// 插入一条笔记
val values = ContentValues().apply {
put("title", "学习笔记")
put("content", "今天学了 ContentProvider")
put("created_at", System.currentTimeMillis())
}
contentResolver.insert(NoteContract.Notes.CONTENT_URI, values)
3.1 搭配 ContentObserver 监听变化
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
// 数据变了,重新查询
loadNotes()
}
}
contentResolver.registerContentObserver(
NoteContract.Notes.CONTENT_URI, true, observer
)
// 记得取消注册
// contentResolver.unregisterContentObserver(observer)
这就是「数据变化 → 自动刷新」的完整链路。
四、跨进程通信(IPC)原理
4.1 ContentProvider 底层是什么?
ContentProvider 的跨进程能力基于 Binder 机制:
客户端 App 服务端 App
┌──────────┐ ┌──────────┐
│ContentResolver│ │ContentProvider│
│ ↓ │ │ ↑ │
│ Binder │ ──── IPC ───→│ Binder │
│ Proxy │ │ Stub │
└──────────┘ └──────────┘
- 客户端通过
ContentResolver发起请求 - 请求经 Binder 驱动跨进程传输到 Provider 所在进程
- Provider 处理请求后,结果再跨进程返回
整个过程对开发者是透明的——你感觉就像在操作本地数据。
4.2 为什么不用 SharedPreferences?
| 对比 | SharedPreferences | ContentProvider |
|---|---|---|
| 跨进程 | ❌ 不支持 | ✅ 天然支持 |
| 数据结构化 | ❌ KV 键值对 | ✅ URI + Cursor |
| 权限控制 | ❌ 粗粒度 | ✅ 读写分离 |
| 性能 | 适合小数据 | 适合结构化数据 |
4.3 其他 IPC 方式对比
| 方式 | 适用场景 | 复杂度 |
|---|---|---|
| ContentProvider | 数据共享 | ⭐ 低 |
| AIDL | 高频复杂调用 | ⭐⭐⭐ 高 |
| Messenger | 简单消息传递 | ⭐⭐ 中 |
| Bundle (Intent) | 一次性数据传递 | ⭐ 低 |
| Socket | 自定义协议 | ⭐⭐⭐ 高 |
ContentProvider 的优势在于:标准化 + 低门槛 + 系统级支持。
五、进阶:实用技巧与最佳实践
5.1 批量操作
逐条插入效率低,用 ContentProviderOperation 批量执行:
val ops = ArrayList<ContentProviderOperation>()
for (note in noteList) {
ops.add(ContentProviderOperation.newInsert(NoteContract.Notes.CONTENT_URI).apply {
withValue("title", note.title)
withValue("content", note.content)
withValue("created_at", System.currentTimeMillis())
}.build())
}
// 一次性提交,全部在一个事务里
val results = contentResolver.applyBatch(NoteContract.AUTHORITY, ops)
5.2 安全最佳实践
<!-- 1. 不导出则关闭 -->
<provider android:exported="false" ... />
<!-- 2. 精细权限控制 -->
<provider
android:readPermission="com.example.READ"
android:writePermission="com.example.WRITE"
android:grantUriPermissions="true">
<grant-uri-permission android:pathPrefix="/notes" />
</provider>
// 3. 运行时验证:在 Provider 内部再次校验
override fun query(...): Cursor? {
context.enforceCallingOrSelfPermission(
"com.example.notes.READ", "需要读取权限"
)
// ...
}
5.3 配合 CursorLoader / Flow
在 Compose 时代,可以用 Flow 包装 Cursor:
fun observeNotes(): Flow<List<Note>> = callbackFlow {
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
trySend(queryAllNotes())
}
}
contentResolver.registerContentObserver(
NoteContract.Notes.CONTENT_URI, true, observer
)
trySend(queryAllNotes()) // 首次发送
awaitClose {
contentResolver.unregisterContentObserver(observer)
}
}
5.4 常见踩坑
| 坑 | 说明 |
|---|---|
| 主线程操作 | Provider 方法在 Binder 线程池执行,不要假设是主线程 |
| 大图片传输 | 不要用 Cursor 传 Bitmap,改用文件 URI |
| 忘记 notifyChange | 数据变了不通知,UI 不会刷新 |
| URI 冲突 | authority 要和包名绑定,避免全局冲突 |
六、系统内置 Provider 速查
Android 系统自带很多 Provider,常用的:
// 读取联系人
val cursor = contentResolver.query(
ContactsContract.Contacts.CONTENT_URI, null, null, null, null
)
// 读取短信(需要权限)
val cursor = contentResolver.query(
Uri.parse("content://sms/inbox"), null, null, null, "date DESC"
)
// 读取媒体文件
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Images.Media.DISPLAY_NAME),
null, null, null
)
⚠️ Android 10+ 的分区存储对 MediaStore 访问有重大限制,注意适配。
总结
| 知识点 | 要点 |
|---|---|
| 是什么 | 标准化的跨进程数据共享组件 |
| 核心概念 | Content URI、ContentResolver、Cursor |
| 底层原理 | 基于 Binder IPC |
| 安全 | exported + permission + grantUriPermissions |
| 最佳实践 | 批量操作、Flow 封装、notifyChange |
ContentProvider 是 Android 生态中数据流通的基石。理解了它,你就能打通应用间的数据壁垒,也能更好地使用系统提供的联系人、媒体、日历等数据。
下一篇:Paging 3 分页——让长列表加载如丝般顺滑