ContentProvider 基础
本教程共 100 篇 · 第 37 篇 · 更新于 2026-07-28 · 约 9 分钟阅读
37. ContentProvider 基础
本节目标:理解内容提供者(ContentProvider)的作用和数据模型,掌握内容 URI 的结构、CRUD 操作流程,学会创建一个能对外共享数据的 ContentProvider。
内容提供者是什么
打个比方:你的应用里有一本通讯录,别的应用想读里面的联系人。你不能直接让对方翻你的数据库文件—那是你的私有地盘。内容提供者就像一个「前台窗口」,别人要数据得通过这个窗口,你决定给什么、不给什么、谁能访问。
内容提供者(ContentProvider)是四大组件之一,作用是管理一块中央数据存储,并允许其他应用安全地访问。它负责进程间通信和安全控制,让你不用自己处理跨进程的数据传递。
通常在两种场景下用到它:
- 访问别的应用的 Provider:比如读取系统通讯录、相册里的图片。
- 创建自己的 Provider:把自己的数据共享给别的应用(或给自家多个应用共享)。
内容 URI
内容 URI 是访问 Provider 数据的「地址」。结构如下:
content://authority/path/id
各部分含义:
content://:固定前缀,标识这是内容 URI。authority(授权):Provider 的唯一标识名,类似域名,比如com.example.app.provider。path(路径):指向具体的数据表,比如users。id(可选):指向某一行记录,比如users/5表示 id 为 5 的那条。
举个例子,访问用户字典 Provider 的 words 表:
content://user_dictionary/words
如果只想要 id 为 4 的那一行:
val singleUri = ContentUris.withAppendedId(
UserDictionary.Words.CONTENT_URI, 4
)
Note有 id 的叫「基于 id 的 URI」,没有 id 的叫「基于目录的 URI」。删除或更新单行时通常用带 id 的 URI。
协定类
Provider 通常会提供一个「协定类」(Contract Class),把 URI、表名、列名都定义成常量。这样调用方不用硬编码字符串,避免拼写错误。
object MyContract {
// 授权
const val AUTHORITY = "com.example.app.provider"
// 内容 URI
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/users")
object UserTable {
const val TABLE_NAME = "users"
const val _ID = "_id"
const val COLUMN_NAME = "name"
const val COLUMN_AGE = "age"
}
}
Tip大多数 Provider 的表都有一列
_ID作为主键,因为列表控件(ListView、CursorAdapter)依赖它来定位行。即使不显示_ID,查询时也建议带上。
六个核心方法
创建自己的 ContentProvider,需要继承 ContentProvider 并实现六个抽象方法:
| 方法 | 作用 |
|---|---|
onCreate() | Provider 创建时调用,初始化数据库 |
query() | 查询数据,返回 Cursor |
insert() | 插入数据,返回新行的 URI |
update() | 更新数据,返回受影响行数 |
delete() | 删除数据,返回删除行数 |
getType() | 返回 URI 对应的 MIME 类型 |
创建一个 ContentProvider
下面做一个完整的例子:一个存储用户姓名的 Provider,基于 SQLite。
第一步:定义协定类
object UserContract {
const val AUTHORITY = "com.example.app.provider"
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/users")
const val _ID = "_id"
const val COLUMN_NAME = "name"
}
第二步:创建 Provider 类
class UserProvider : ContentProvider() {
companion object {
const val USERS = 1 // 匹配整个 users 表
const val USERS_ID = 2 // 匹配某一行
val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(UserContract.AUTHORITY, "users", USERS)
addURI(UserContract.AUTHORITY, "users/#", USERS_ID)
}
}
private lateinit var dbHelper: UserDbHelper
override fun onCreate(): Boolean {
dbHelper = UserDbHelper(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)) {
USERS -> db.query(
"users", projection, selection,
selectionArgs, null, null, sortOrder
)
USERS_ID -> {
val id = ContentUris.parseId(uri)
db.query(
"users", projection, "_id = ?",
arrayOf(id.toString()), null, null, sortOrder
)
}
else -> throw IllegalArgumentException("未知 URI: $uri")
}
// 让 Cursor 监听数据变化
cursor.setNotificationUri(context!!.contentResolver, uri)
return cursor
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = dbHelper.writableDatabase
val id = db.insert("users", null, values)
val newUri = ContentUris.withAppendedId(uri, id)
// 通知监听这个 URI 的观察者数据变了
context!!.contentResolver.notifyChange(newUri, null)
return newUri
}
override fun update(
uri: Uri, values: ContentValues?,
selection: String?, selectionArgs: Array<String>?
): Int {
val db = dbHelper.writableDatabase
val rows = db.update("users", values, selection, selectionArgs)
context!!.contentResolver.notifyChange(uri, null)
return rows
}
override fun delete(
uri: Uri, selection: String?, selectionArgs: Array<String>?
): Int {
val db = dbHelper.writableDatabase
val rows = db.delete("users", selection, selectionArgs)
context!!.contentResolver.notifyChange(uri, null)
return rows
}
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
USERS -> "vnd.android.cursor.dir/vnd.example.users"
USERS_ID -> "vnd.android.cursor.item/vnd.example.users"
else -> throw IllegalArgumentException("未知 URI: $uri")
}
}
}
UriMatcher 做什么
UriMatcher 负责把传进来的 URI 路由到正确的处理逻辑。# 匹配数字,* 匹配任意字符串:
users匹配整个表 → 返回USERSusers/5匹配单行 → 返回USERS_ID
Note
notifyChange()很重要。它通知所有监听这个 URI 的Cursor或ContentObserver:数据变了,该刷新了。忘了调会导致界面不更新。
第三步:注册到清单
<provider
android:name=".UserProvider"
android:authorities="com.example.app.provider"
android:exported="false" />
exported 设为 false 表示只有自家应用能访问。要共享给别的应用就设 true,但要配合权限保护。
MIME 类型
getType() 返回的 MIME 类型有固定格式:
- 多行(目录):
vnd.android.cursor.dir/子类型 - 单行(条目):
vnd.android.cursor.item/子类型
子类型可以自定义,比如 vnd.example.users。
为什么要用 ContentProvider
你可能会想:数据存 SQLite 不就行了,干嘛搞这么复杂?
关键在于跨进程共享。SQLite 文件是应用私有的,别的应用碰不到。ContentProvider 提供了一套标准接口,封装了 IPC(进程间通信),别的应用用统一的 ContentResolver 就能读写,不用关心数据底层是 SQLite 还是文件还是网络。
而且很多系统功能依赖 ContentProvider:
- 通讯录、日历、媒体库都是通过 Provider 暴露的
- 搜索建议、桌面小部件、同步适配器都需要 Provider
FileProvider(第 66 章会讲)也是基于 ContentProvider
小结
ContentProvider 是跨应用数据共享的标准方案。核心是内容 URI 定位数据、六个方法实现 CRUD、UriMatcher 做路由。日常开发中,访问系统 Provider 比自己创建 Provider 更常见,但理解原理能帮你更好地使用系统数据源。