Android 官方推荐的跨进程共享结构化数据标准方式
ContentProvider是 Android 四大组件之一,是 Android 官方推荐的跨进程共享结构化数据的标准方式。
它底层基于 Binder实现,但对开发者暴露的是类似于数据库操作的接口(CRUD),通过 URI来定位数据。
ContentProvider 像是一个数据中转站:
ContentResolver和 URI访问数据| 角色 | 功能 |
|---|---|
| ContentProvider | 运行在服务端进程,真正执行数据的增、删、改、查 |
| ContentResolver | 运行在客户端进程,通过 Binder 调用远程 Provider 的方法 |
| URI | 数据的地址,格式为 content://authority/路径 |
| Cursor | 查询操作返回的数据集合(游标) |
object BookContract {
const val AUTHORITY = "com.example.provider.book"
val BASE_URI = Uri.parse("content://$AUTHORITY")
val CONTENT_URI = Uri.withAppendedPath(BASE_URI, "books")
object BookEntry {
const val TABLE_NAME = "books"
const val COLUMN_ID = "_id"
const val COLUMN_TITLE = "title"
const val COLUMN_AUTHOR = "author"
}
}
class BookDbHelper(context: Context) : SQLiteOpenHelper(context, "books.db", null, 1) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("""
CREATE TABLE ${BookEntry.TABLE_NAME} (
${BookEntry.COLUMN_ID} INTEGER PRIMARY KEY AUTOINCREMENT,
${BookEntry.COLUMN_TITLE} TEXT,
${BookEntry.COLUMN_AUTHOR} TEXT
)
""")
}
override fun onUpgrade(db: SQLiteDatabase, old: Int, new: Int) {}
}
class BookProvider : ContentProvider() {
private lateinit var dbHelper: BookDbHelper
override fun onCreate(): Boolean {
dbHelper = BookDbHelper(context!!)
return true
}
companion object {
private const val BOOKS = 1
private const val BOOK_ID = 2
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(BookContract.AUTHORITY, "books", BOOKS)
addURI(BookContract.AUTHORITY, "books/#", BOOK_ID)
}
}
// query - 查询
override fun query(uri: Uri, projection: Array?,
selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {
val db = dbHelper.readableDatabase
return when (uriMatcher.match(uri)) {
BOOKS -> db.query(BookEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder)
BOOK_ID -> {
val id = uri.lastPathSegment
db.query(BookEntry.TABLE_NAME, projection, "_id=?", arrayOf(id), null, null, sortOrder)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
// insert - 插入
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = dbHelper.writableDatabase
val id = db.insert(BookEntry.TABLE_NAME, null, values)
context?.contentResolver?.notifyChange(uri, null)
return ContentUris.withAppendedId(BookContract.CONTENT_URI, id)
}
// update - 更新
override fun update(uri: Uri, values: ContentValues?,
selection: String?, selectionArgs: Array?): Int {
val db = dbHelper.writableDatabase
return when (uriMatcher.match(uri)) {
BOOKS -> db.update(BookEntry.TABLE_NAME, values, selection, selectionArgs)
BOOK_ID -> {
val id = uri.lastPathSegment
db.update(BookEntry.TABLE_NAME, values, "_id=?", arrayOf(id))
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
// delete - 删除
override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
val db = dbHelper.writableDatabase
return when (uriMatcher.match(uri)) {
BOOKS -> db.delete(BookEntry.TABLE_NAME, selection, selectionArgs)
BOOK_ID -> {
val id = uri.lastPathSegment
db.delete(BookEntry.TABLE_NAME, "_id=?", arrayOf(id))
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
// getType - 返回 MIME 类型
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
BOOKS -> "vnd.android.cursor.dir/${BookContract.AUTHORITY}.books"
BOOK_ID -> "vnd.android.cursor.item/${BookContract.AUTHORITY}.books"
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
}
exported="true"允许外部应用访问val values = ContentValues().apply {
put(BookEntry.COLUMN_TITLE, "三体")
put(BookEntry.COLUMN_AUTHOR, "刘慈欣")
}
val uri = contentResolver.insert(BookContract.CONTENT_URI, values)
val cursor = contentResolver.query(BookContract.CONTENT_URI, null, null, null, null)
cursor?.use {
while (it.moveToNext()) {
val title = it.getString(it.getColumnIndexOrThrow(BookEntry.COLUMN_TITLE))
}
}
val values = ContentValues().apply { put(BookEntry.COLUMN_TITLE, "三体2") }
val count = contentResolver.update(
ContentUris.withAppendedId(BookContract.CONTENT_URI, 1), values, null, null
)
val count = contentResolver.delete(
ContentUris.withAppendedId(BookContract.CONTENT_URI, 1), null, null
)
服务端在数据变更后调用 notifyChange,通知所有监听者:
// 服务端
context?.contentResolver?.notifyChange(uri, null)
客户端可以注册 ContentObserver实时监听数据变化:
// 客户端
contentResolver.registerContentObserver(uri, true, object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
// 数据更新,刷新 UI
}
})
AndroidManifest.xml中声明 <uses-permission>AUTHORITY构造 URI<uses-permission>权限| 优点 | 缺点 |
|---|---|
| 安全:URI + 权限控制,精细粒度 | 数据类型受限:主要针对表格型数据,不适合流媒体 |
| 标准化:基于 CRUD 操作,学习成本低 | 实现复杂:需要编写 Provider 和 URI 匹配 |
| 数据变更通知:支持 ContentObserver | 耦合性:客户端需要提前知道 URI 格式和列名 |
| 性能好:Binder 线程池并发 | 同步调用:大查询需在子线程执行 |
use扩展函数selection+ selectionArgs)exported="true"必须配合自定义权限或签名级权限notifyChange让 UI 刷新ContentProvider只是一个数据访问的抽象层,定义了增删改查的标准接口。它不关心数据实际存在哪里,你可以自由选择底层存储。
query中能返回 Cursorinsert中能返回新插入数据的 UriCursor与 ContentProvider 接口天然匹配,无需额外转换SQLiteOpenHelper、Room 等)| 底层存储 | 说明 |
|---|---|
| 内存 Map | 用 MutableMap或缓存存储,query返回 MatrixCursor |
| 文件 | JSON、XML 等文件,解析后转成 Cursor返回 |
| SharedPreferences | 键值对形式,可封装成 ContentProvider |
| MMKV | 基于 mmap 的高性能键值对存储(微信开源) |
| 网络 API | query直接请求服务器,不持久化到本地数据库 |
| 数据类型 | 常用底层存储 | 备注 |
|---|---|---|
| 结构化关系型数据(联系人、IM 消息、配置) | SQLite / WCDB(增强版 SQLite) / Room | 最成熟的方案,与 ContentProvider 接口完美契合 |
| 高频写入键值对(开关、缓存、状态) | MMKV(基于 mmap) | 可以封装 ContentProvider 供跨进程访问,但不直接返回 Cursor 较麻烦 |
| 大文件 / 媒体索引 | 文件系统 + SQLite | 数据库存元数据(路径、大小),文件实体放私有目录,结合 openFile暴露 |
| 纯网络 / 云数据 | 内存 / 网络请求 | 不持久化或只缓存,ContentProvider 直接读网络数据 |
ContentProvider是 Android 跨进程共享结构化数据的标准方案。它把数据封装成数据库风格的操作,让客户端像操作本地数据库一样访问远程数据,同时提供完善的权限控制和数据变更通知。
在项目中,它常用于共享联系人、字典、配置信息等。结合 Bundle、Messenger、文件共享,你就掌握了 Android IPC 的完整工具箱。
content://authority/路径notifyChange,客户端注册 ContentObserverreadPermission和 writePermission保护数据安全exported="true",客户端需声明权限