ContentResolver 与系统 Provider
本教程共 100 篇 · 第 38 篇 · 更新于 2026-07-28 · 约 9 分钟阅读
38. ContentResolver 与系统 Provider
本节目标:学会用 ContentResolver 读写 Provider 数据,掌握查询通讯录、读取相册图片等系统 Provider 的常见操作,理解权限与安全的关系。
上一节我们创建了 Provider,这一节来看怎么使用 Provider。作为消费方,你不用关心 Provider 内部怎么实现,只需要通过 ContentResolver 这个客户端工具来操作。
ContentResolver 是什么
打个比方:Provider 是餐厅后厨,ContentResolver 是服务员。你(应用)想吃啥跟服务员说,服务员去后厨传话,菜做好了端给你。你不需要知道后厨怎么做的。
ContentResolver 是应用访问 ContentProvider 的统一入口。每个 Context 都有一个 contentResolver 实例:
val resolver = context.contentResolver
它提供和 Provider 对应的 CRUD 方法:query()、insert()、update()、delete()。
查询数据
query() 的参数和 SQL 查询一一对应:
val cursor = contentResolver.query(
UserContract.CONTENT_URI, // 相当于 FROM table
projection, // 相当于 SELECT 列名
selection, // 相当于 WHERE 条件
selectionArgs, // 替换条件里的 ? 占位符
sortOrder // 相当于 ORDER BY
)
| query() 参数 | SQL 对应 | 说明 |
|---|---|---|
| uri | FROM table | 指定查哪张表 |
| projection | col1, col2 | 要哪些列 |
| selection | WHERE col = ? | 筛选条件 |
| selectionArgs | (替换 ? 的值) | 防注入的安全传参 |
| sortOrder | ORDER BY col | 排序方式 |
完整查询示例
fun queryUsers(contentResolver: ContentResolver): List<String> {
val projection = arrayOf("_id", "name")
val selection = "name LIKE ?"
val selectionArgs = arrayOf("张%") // 查姓张的
val sortOrder = "_id DESC"
val cursor = contentResolver.query(
UserContract.CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)
val names = mutableListOf<String>()
cursor?.use {
val nameIndex = it.getColumnIndex("name")
while (it.moveToNext()) {
names.add(it.getString(nameIndex))
}
}
return names
}
Tip
cursor?.use { }会自动关闭 Cursor,不用担心忘记释放资源。这是 Kotlin 的use扩展函数,类似 Java 的 try-with-resources。
防止 SQL 注入
千万别把用户输入直接拼到 selection 里:
// 危险!用户可能输入 "1; DROP TABLE users;"
val selection = "name = '$userInput'" // 别这么写
正确做法是用 ? 占位符配合 selectionArgs:
val selection = "name = ?"
val selectionArgs = arrayOf(userInput) // 安全,用户输入不会被当 SQL 执行
插入数据
val values = ContentValues().apply {
put("name", "张三")
put("age", 25)
}
val newUri = contentResolver.insert(UserContract.CONTENT_URI, values)
// newUri 格式: content://com.example.app.provider/users/1
val newId = newUri?.let { ContentUris.parseId(it) }
ContentValues 是一个键值对容器,存放列名和值。
更新数据
val values = ContentValues().apply {
put("age", 26)
}
val selection = "name = ?"
val selectionArgs = arrayOf("张三")
val rowsUpdated = contentResolver.update(
UserContract.CONTENT_URI,
values,
selection,
selectionArgs
)
// rowsUpdated 是受影响的行数
删除数据
val selection = "_id = ?"
val selectionArgs = arrayOf("5")
val rowsDeleted = contentResolver.delete(
UserContract.CONTENT_URI,
selection,
selectionArgs
)
Warning如果要删除单行,也可以用带 id 的 URI,然后 selection 传 null:
val singleUri = ContentUris.withAppendedId(UserContract.CONTENT_URI, 5) contentResolver.delete(singleUri, null, null)
读取系统通讯录
Android 系统内置了很多 Provider,通讯录是最常用的之一。它的授权是 com.android.contacts。
第一步:加权限
<uses-permission android:name="android.permission.READ_CONTACTS" />
READ_CONTACTS 是危险权限,还要在运行时申请(第 83 章会详细讲权限)。
第二步:查询联系人
fun readContacts(context: Context): List<String> {
val contacts = mutableListOf<String>()
val cursor = context.contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME
),
null, null,
"${ContactsContract.Contacts.DISPLAY_NAME} ASC"
)
cursor?.use {
while (it.moveToNext()) {
val name = it.getString(
it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)
)
contacts.add(name)
}
}
return contacts
}
Note
ContactsContract是系统提供的协定类,里面定义了通讯录的所有 URI 和列名。不用自己记字符串,直接用常量。
第三步:运行时申请权限
// 检查是否已有权限
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.READ_CONTACTS
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_CONTACTS),
REQUEST_READ_CONTACTS
)
} else {
// 已有权限,直接查询
val names = readContacts(this)
}
读取 MediaStore 图片
MediaStore 是系统管理媒体文件(图片、视频、音频)的 Provider。读相册图片就用它。
fun queryImages(context: Context): List<Uri> {
val imageUris = mutableListOf<Uri>()
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED
)
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection, null, null, sortOrder
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id
)
imageUris.add(uri)
}
}
return imageUris
}
Warning
- Android 10(API 29)引入了分区存储(Scoped Storage),直接读 MediaStore 受到限制。
READ_EXTERNAL_STORAGE权限在 Android 13 后被更细粒度的权限替代(READ_MEDIA_IMAGES等)。第 66 章会详细讲分区存储。
批量操作
要一次性插入大量数据,一条条调 insert() 很慢。可以用 ContentProviderOperation 做批量操作:
val operations = ArrayList<ContentProviderOperation>()
for (name in listOf("张三", "李四", "王五")) {
operations.add(
ContentProviderOperation.newInsert(UserContract.CONTENT_URI)
.withValue("name", name)
.build()
)
}
// 一次提交所有操作,要么全成功要么全失败(原子操作)
val results = contentResolver.applyBatch(UserContract.AUTHORITY, operations)
Tip
- 批量操作是事务性的,所有操作要么全部成功,要么全部回滚。适合需要保证数据一致性的场景。
用 Intent 间接访问
有时候你没有权限直接访问某个 Provider,可以让有权限的应用帮你选。比如选联系人:
val pickContactIntent = Intent(Intent.ACTION_PICK).apply {
type = ContactsContract.Contacts.CONTENT_TYPE
}
pickContactLauncher.launch(pickContactIntent)
用户在系统通讯录里选一个联系人,结果会通过 URI 返回。你的应用获得的是临时 URI 权限,只能读这一个 URI,不能读整个通讯录。这样既保护了用户隐私,又实现了功能。
Note
- 这种方式不需要
READ_CONTACTS权限,是「按需授权」的设计,用户体验和安全性都更好。
小结
ContentResolver 是访问 Provider 数据的客户端工具,CRUD 方法与 SQL 对应。系统内置了通讯录、MediaStore 等 Provider,访问时注意申请对应权限。能用 Intent 间接访问的就别直接申请全局权限,更安全。下节我们进入 Fragment 片段的学习。