ContentProvider IPC 全面笔记

Android 官方推荐的跨进程共享结构化数据标准方式

四大组件 CRUD 操作 Binder 底层 URI 定位

目录导航

一、什么是 ContentProvider?

1 定义与定位

ContentProvider是 Android 四大组件之一,是 Android 官方推荐的跨进程共享结构化数据的标准方式。

它底层基于 Binder实现,但对开发者暴露的是类似于数据库操作的接口(CRUD),通过 URI来定位数据。

2 形象理解

ContentProvider 像是一个数据中转站

  • 服务端:把数据放在 ContentProvider 中
  • 客户端:通过 ContentResolverURI访问数据
核心特点:统一的数据访问接口,支持跨应用访问,自带权限控制机制

二、核心角色

1 角色分工表
角色 功能
ContentProvider 运行在服务端进程,真正执行数据的增、删、改、查
ContentResolver 运行在客户端进程,通过 Binder 调用远程 Provider 的方法
URI 数据的地址,格式为 content://authority/路径
Cursor 查询操作返回的数据集合(游标)

三、实现 ContentProvider(服务端)

1 定义数据库和契约类
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"
    }
}
2 数据库帮助类
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) {}
}
3 ContentProvider 子类
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")
        }
    }
}
4 在 AndroidManifest.xml 中注册
注意:
  • exported="true"允许外部应用访问
  • 可自定义读写权限,保护数据安全

四、客户端访问(ContentResolver)

1 插入数据
val values = ContentValues().apply {
    put(BookEntry.COLUMN_TITLE, "三体")
    put(BookEntry.COLUMN_AUTHOR, "刘慈欣")
}
val uri = contentResolver.insert(BookContract.CONTENT_URI, values)
2 查询数据
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))
    }
}
3 更新数据
val values = ContentValues().apply { put(BookEntry.COLUMN_TITLE, "三体2") }
val count = contentResolver.update(
    ContentUris.withAppendedId(BookContract.CONTENT_URI, 1), values, null, null
)
4 删除数据
val count = contentResolver.delete(
    ContentUris.withAppendedId(BookContract.CONTENT_URI, 1), null, null
)

五、数据变更通知

1 服务端通知

服务端在数据变更后调用 notifyChange,通知所有监听者:

// 服务端
context?.contentResolver?.notifyChange(uri, null)
2 客户端监听

客户端可以注册 ContentObserver实时监听数据变化:

// 客户端
contentResolver.registerContentObserver(uri, true, object : ContentObserver(Handler(Looper.getMainLooper())) {
    override fun onChange(selfChange: Boolean) {
        // 数据更新,刷新 UI
    }
})

六、跨应用共享注意事项

1 服务端配置
  • exported="true":服务端 Provider 必须设置此属性以允许外部应用访问
  • 权限声明:如果设置了自定义权限,客户端需要在 AndroidManifest.xml中声明 <uses-permission>
2 客户端配置
  • AUTHORITY 匹配:客户端使用服务端的 AUTHORITY构造 URI
  • 权限申请:声明相应的 <uses-permission>权限

七、ContentProvider 的优缺点

1 优缺点对比表
优点 缺点
安全:URI + 权限控制,精细粒度 数据类型受限:主要针对表格型数据,不适合流媒体
标准化:基于 CRUD 操作,学习成本低 实现复杂:需要编写 Provider 和 URI 匹配
数据变更通知:支持 ContentObserver 耦合性:客户端需要提前知道 URI 格式和列名
性能好:Binder 线程池并发 同步调用:大查询需在子线程执行

八、最佳实践

1 性能优化
  • 耗时操作放子线程:query/insert/update/delete 可能阻塞,应用协程或线程
  • 用完 Cursor 及时关闭:使用 use扩展函数
2 安全规范
  • 防 SQL 注入:使用参数化查询(selection+ selectionArgs
  • 导出 Provider 要加权限exported="true"必须配合自定义权限或签名级权限
3 数据更新
  • 数据变更后通知:调用 notifyChange让 UI 刷新
4 大文件共享
  • 重写 openFile 方法:返回文件描述符,实现跨应用分享大文件(类似 FileProvider)

九、底层存储选型

1 ContentProvider ≠ SQLite

ContentProvider只是一个数据访问的抽象层,定义了增删改查的标准接口。它不关心数据实际存在哪里,你可以自由选择底层存储。

  • 只要在 query中能返回 Cursor
  • insert中能返回新插入数据的 Uri
  • 其他方法合理实现即可
2 为什么常和 SQLite 一起出现?
  1. 官方示例和教程大多使用 SQLite,学习资料丰富
  2. SQLite 返回的 Cursor与 ContentProvider 接口天然匹配,无需额外转换
  3. 大部分结构化数据(配置、联系人、消息)适合用关系型数据库存储
  4. 工具链完善(SQLiteOpenHelper、Room 等)
3 非 SQLite 的存储实现示例
底层存储 说明
内存 Map MutableMap或缓存存储,query返回 MatrixCursor
文件 JSON、XML 等文件,解析后转成 Cursor返回
SharedPreferences 键值对形式,可封装成 ContentProvider
MMKV 基于 mmap 的高性能键值对存储(微信开源)
网络 API query直接请求服务器,不持久化到本地数据库
4 大厂实际选型
数据类型 常用底层存储 备注
结构化关系型数据(联系人、IM 消息、配置) SQLite / WCDB(增强版 SQLite) / Room 最成熟的方案,与 ContentProvider 接口完美契合
高频写入键值对(开关、缓存、状态) MMKV(基于 mmap) 可以封装 ContentProvider 供跨进程访问,但不直接返回 Cursor 较麻烦
大文件 / 媒体索引 文件系统 + SQLite 数据库存元数据(路径、大小),文件实体放私有目录,结合 openFile暴露
纯网络 / 云数据 内存 / 网络请求 不持久化或只缓存,ContentProvider 直接读网络数据
结论:大厂不会只用 SQLite,但 "ContentProvider + SQLite(或其优化版)"是处理结构化数据跨进程共享时最常见、最成熟的组合。具体选择取决于数据的结构、大小、写入频率和是否需持久化。

十、总结

1 核心价值

ContentProvider是 Android 跨进程共享结构化数据的标准方案。它把数据封装成数据库风格的操作,让客户端像操作本地数据库一样访问远程数据,同时提供完善的权限控制和数据变更通知。

在项目中,它常用于共享联系人、字典、配置信息等。结合 Bundle、Messenger、文件共享,你就掌握了 Android IPC 的完整工具箱。

ContentProvider IPC 核心要点