首页 / Android 入门教程 / ContentResolver 与系统 Provider

Android 入门教程

ContentResolver 与系统 Provider

本教程共 100 篇 · 第 38 篇 · 更新于 2026-07-28 · 约 9 分钟阅读

AndroidAndroid 入门教程ContentResolver系统Provider通讯录MediaStore

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 对应说明
uriFROM table指定查哪张表
projectioncol1, col2要哪些列
selectionWHERE col = ?筛选条件
selectionArgs(替换 ? 的值)防注入的安全传参
sortOrderORDER 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 片段的学习。