Android ContentProvider 完全入门指南
1. 什么是 ContentProvider?
ContentProvider(内容提供者)是 Android 四大组件之一,它的核心职责是在不同应用之间安全地共享数据。举个例子:你写的 App 想读取手机通讯录中的联系人,或者想在相册中保存一张照片,这些操作都需要通过 ContentProvider 来完成。
简单理解:ContentProvider 就像一个"数据仓库管理员",它把应用的数据(比如数据库、文件)封装起来,只通过一套统一的接口(增删改查)对外提供。这样既实现了数据共享,又保证了数据安全,调用方无法直接接触到原始数据库或文件系统。
2. 为什么需要 ContentProvider?------作用与应用场景
2.1 跨应用数据共享(跨进程通信)
Android 系统为每个应用分配独立的用户 ID 和沙盒环境,正常情况下 A 应用无法访问 B 应用的私有数据。ContentProvider 通过 Binder 机制实现了跨进程的数据访问,让 A 应用可以安全地读取或修改 B 应用暴露的数据。
2.2 统一的数据访问接口
不管底层的真实数据是 SQLite 数据库、文件、网络还是 XML,ContentProvider 对外都表现为一套形如 content:// 的 URI,调用方只需使用 ContentResolver 的 query()、insert()、update()、delete() 方法,就像操作数据库一样简单。
2.3 数据封装与权限控制
提供者可以自由决定哪些数据可以被外部访问、可以读写还是只读,甚至可以对不同 URI 设置不同权限(读权限、写权限)。权限检查会自动执行,无需我们额外编码。
2.4 与系统服务无缝集成
Android 系统本身就提供了大量 ContentProvider,例如:
-
通讯录:
ContactsContract -
通话记录:
CallLog -
短信:
Telephony.Sms -
媒体库(图片/音频/视频):
MediaStore -
日历:
CalendarContract -
文件共享(安全方式):
FileProvider
了解 ContentProvider 后,你就能轻松实现"一键同步联系人"、"选择系统图片"等功能。
3. 核心概念速览
3.1 Content URI(统一资源标识符)
每个 ContentProvider 的数据集都用一个 URI 来标识,格式如下:
text
content://[authority]/[path]/[id]
-
scheme :固定为
content://,表示这是一个 ContentProvider。 -
authority :唯一标识提供者的字符串,通常取包名全称(如
com.example.app.provider),保证不冲突。 -
path :指向具体的数据表或数据类型(如
notes)。 -
id (可选):指向某条具体记录的数字 ID(如
5)。
例子:
-
所有笔记:
content://com.example.app.provider/notes -
ID 为 1 的笔记:
content://com.example.app.provider/notes/1
3.2 MIME 类型
ContentProvider 会为每一个 URI 返回对应的 MIME 类型,帮助调用方识别数据类型。标准格式:
-
对于多条记录 (列表):
vnd.android.cursor.dir/vnd.<authority>.<path> -
对于单条记录 :
vnd.android.cursor.item/vnd.<authority>.<path>
例如:笔记列表的 MIME 可能是 vnd.android.cursor.dir/vnd.com.example.app.provider.notes。
3.3 ContentResolver(内容解析器)
调用方不直接与 ContentProvider 打交道,而是通过 ContentResolver 来发送请求。在任何 Context(Activity、Service)中都可以通过 getContentResolver() 获取它。它提供了完全对应数据库操作的四个方法:
kotlin
contentResolver.query(uri, projection, selection, selectionArgs, sortOrder)
contentResolver.insert(uri, contentValues)
contentResolver.update(uri, contentValues, selection, selectionArgs)
contentResolver.delete(uri, selection, selectionArgs)
3.4 Cursor
查询返回的结果是一个 Cursor 对象,它类似于数据库中的游标,指向结果集的某行。你可以遍历它并取出各列数据。
4. 如何使用系统提供的 ContentProvider(动手实践)
以读取系统通讯录为例,你需要先动态申请 READ_CONTACTS 权限(略),然后:
kotlin
// 在 Activity 或 Fragment 中
val resolver = contentResolver
val uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI
// 要查询哪些列
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
)
val cursor = resolver.query(uri, projection, null, null, null)
// 遍历结果
if (cursor != null) {
while (cursor.moveToNext()) {
val name = cursor.getString(cursor.getColumnIndex(projection[0]))
val number = cursor.getString(cursor.getColumnIndex(projection[1]))
Log.d("Contacts", "姓名:$name,电话:$number")
}
cursor.close() // 用完后务必关闭
}
这就利用了系统通话记录 ContentProvider 获取到了联系人的姓名和号码。
5. 创建自定义 ContentProvider
如果你想让自己的应用数据提供给其他 App 使用,或者只是想在应用内部更规范地管理数据(比如配合 CursorLoader),可以自己写一个。
5.1 定义数据库与常量
通常 ContentProvider 底层是一个 SQLite 数据库。我们先创建数据库契约类和 SQLiteOpenHelper。
NoteContract.kt
kotlin
object NoteContract {
const val AUTHORITY = "com.example.noteprovider.provider"
const val PATH_NOTES = "notes"
const val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/$PATH_NOTES")
object NoteEntry {
const val TABLE_NAME = "notes"
const val _ID = "_id"
const val COLUMN_TITLE = "title"
const val COLUMN_CONTENT = "content"
}
}
5.2 创建数据库帮助类
kotlin
class NoteDatabaseHelper(context: Context) : SQLiteOpenHelper(
context, "notes.db", null, 1
) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL("""
CREATE TABLE ${NoteContract.NoteEntry.TABLE_NAME} (
${NoteContract.NoteEntry._ID} INTEGER PRIMARY KEY AUTOINCREMENT,
${NoteContract.NoteEntry.COLUMN_TITLE} TEXT,
${NoteContract.NoteEntry.COLUMN_CONTENT} TEXT
)
""")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS ${NoteContract.NoteEntry.TABLE_NAME}")
onCreate(db)
}
}
5.3 继承 ContentProvider 实现核心方法
kotlin
class NoteProvider : ContentProvider() {
private lateinit var dbHelper: NoteDatabaseHelper
// URI 匹配器
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
// 匹配整个表
addURI(NoteContract.AUTHORITY, NoteContract.PATH_NOTES, CODE_NOTES)
// 匹配单条记录 notes/#
addURI(NoteContract.AUTHORITY, "${NoteContract.PATH_NOTES}/#", CODE_NOTE_ID)
}
companion object {
private const val CODE_NOTES = 100
private const val CODE_NOTE_ID = 101
}
override fun onCreate(): Boolean {
dbHelper = NoteDatabaseHelper(context!!)
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val db = dbHelper.readableDatabase
return when (uriMatcher.match(uri)) {
CODE_NOTES -> {
db.query(
NoteContract.NoteEntry.TABLE_NAME,
projection, selection, selectionArgs,
null, null, sortOrder
)
}
CODE_NOTE_ID -> {
val id = uri.lastPathSegment // 获取路径最后的数字
db.query(
NoteContract.NoteEntry.TABLE_NAME,
projection,
"${NoteContract.NoteEntry._ID}=?",
arrayOf(id),
null, null, sortOrder
)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = dbHelper.writableDatabase
val matchedCode = uriMatcher.match(uri)
if (matchedCode == CODE_NOTES) {
val newId = db.insert(NoteContract.NoteEntry.TABLE_NAME, null, values)
if (newId > 0) {
val newUri = ContentUris.withAppendedId(NoteContract.CONTENT_URI, newId)
// 通知数据变更
context?.contentResolver?.notifyChange(newUri, null)
return newUri
}
}
throw SQLException("Insert failed for URI: $uri")
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
val db = dbHelper.writableDatabase
return when (uriMatcher.match(uri)) {
CODE_NOTES -> {
val rows = db.update(NoteContract.NoteEntry.TABLE_NAME, values, selection, selectionArgs)
if (rows > 0) context?.contentResolver?.notifyChange(uri, null)
rows
}
CODE_NOTE_ID -> {
val id = uri.lastPathSegment
val rows = db.update(
NoteContract.NoteEntry.TABLE_NAME,
values,
"${NoteContract.NoteEntry._ID}=?",
arrayOf(id)
)
if (rows > 0) context?.contentResolver?.notifyChange(uri, null)
rows
}
else -> throw IllegalArgumentException("Update not supported for $uri")
}
}
override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<out String>?
): Int {
val db = dbHelper.writableDatabase
return when (uriMatcher.match(uri)) {
CODE_NOTES -> {
val rows = db.delete(NoteContract.NoteEntry.TABLE_NAME, selection, selectionArgs)
if (rows > 0) context?.contentResolver?.notifyChange(uri, null)
rows
}
CODE_NOTE_ID -> {
val id = uri.lastPathSegment
val rows = db.delete(
NoteContract.NoteEntry.TABLE_NAME,
"${NoteContract.NoteEntry._ID}=?",
arrayOf(id)
)
if (rows > 0) context?.contentResolver?.notifyChange(uri, null)
rows
}
else -> throw IllegalArgumentException("Delete not supported for $uri")
}
}
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
CODE_NOTES -> "vnd.android.cursor.dir/vnd.com.example.noteprovider.notes"
CODE_NOTE_ID -> "vnd.android.cursor.item/vnd.com.example.noteprovider.notes"
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
}
5.4 在 AndroidManifest.xml 中注册
xml
<provider
android:name=".NoteProvider"
android:authorities="com.example.noteprovider.provider"
android:exported="true" <!-- 是否允许外部应用访问,true 表示允许 -->
android:readPermission="com.example.permission.READ_NOTES"
android:writePermission="com.example.permission.WRITE_NOTES" />
当 exported 设为 true 时,可以配合自定义权限保护数据;即便是 exported="false",应用内部仍可自由访问。
5.5 在另一个应用中调用自定义 Provider
假如另一个应用已经声明了所需权限并获取授权后:
kotlin
val resolver = contentResolver
val uri = Uri.parse("content://com.example.noteprovider.provider/notes")
val values = ContentValues().apply {
put("title", "新笔记")
put("content", "这是通过 ContentProvider 插入的内容")
}
val newUri = resolver.insert(uri, values) // 插入
val cursor = resolver.query(uri, null, null, null, null) // 查询
// ... 使用 cursor
cursor?.close()
6. 使用 ContentProvider 的注意事项(新手避坑指南)
6.1 异步操作是必须的
ContentProvider 的生命周期方法(query、insert 等)默认运行在主线程 !如果你在 UI 线程中直接调用 ContentResolver 查询大量数据,可能会导致界面卡顿甚至 ANR。请务必搭配 CursorLoader 、协程 或 AsyncTask(已弃用)进行异步加载。
6.2 使用完后一定要关闭 Cursor
忘记关闭 Cursor 会导致内存泄漏。推荐使用 Kotlin 的 use {} 扩展函数:
kotlin
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
// 处理数据
}
}
6.3 注意数据变化通知
当数据发生改变时,应调用 contentResolver.notifyChange(uri, null)。这样已注册的观察者(如 CursorAdapter、Loader)会自动刷新数据,避免显示过期内容。
6.4 使用 FileProvider 安全共享文件
从 Android 7.0 开始,严禁使用 file:// URI 在应用间分享文件,否则会抛出 FileUriExposedException。必须使用 FileProvider,它本质上也是一个特殊的 ContentProvider。
-
配置
res/xml/file_paths.xml -
在清单中声明
androidx.core.content.FileProvider -
通过
FileProvider.getUriForFile()生成content://URI
6.5 Android 11 及以上的包可见性
如果 targetSdkVersion >= 30,其他应用要访问你的 ContentProvider,必须默认对第三方不可见。如果希望被访问,需要在清单中添加 queries 元素声明,或者将你自己的提供者设置为 android:exported="true" 且对外授予适当权限。
7. 完整实战:一个笔记应用的数据共享
假设你已经按照第5节创建了 NoteProvider 并注册在应用 A 中,应用 A 可以直接通过 URI 访问;应用 B 想读取应用 A 的笔记,则:
-
应用 A 清单中设置了
exported="true",并自定权限。 -
应用 B 清单中声明
<uses-permission android:name="com.example.permission.READ_NOTES"/>并动态申请。 -
应用 B 使用
ContentResolver执行 query/insert 等操作。
示例代码与前面类似,不再重复。
8. ContentProvider 与其他数据存储方案的对比
| 存储方式 | 适用场景 | 跨应用? | 效率 | 学习成本 |
|---|---|---|---|---|
| SharedPreferences | 简单键值对 | 仅限内部(或MODE_WORLD_READABLE已废弃) | 高 | 低 |
| SQLite 数据库 | 复杂关系数据 | 否(除非包装成 ContentProvider) | 中 | 中 |
| ContentProvider | 需要共享或与系统集成 | 是 | 中(IPC开销) | 较高 |
| Room 库 | 应用内 SQLite 抽象 | 否(但可配合 ContentProvider) | 高 | 中 |
| 文件存储 | 内部/外部文件 | 需 FileProvider | 高 | 低 |
| DataStore | 替代 SharedPreferences | 否 | 高 | 低 |
所以,只要你有跨应用数据共享的需求,或者想利用系统 Loader 机制自动刷新 UI,就选 ContentProvider。
9. 总结
-
ContentProvider 是 Android 实现数据共享的标准机制,所有对数据的访问都通过 URI 进行。
-
调用方使用
ContentResolver,提供者继承ContentProvider实现增删改查。 -
系统已提供大量内置提供者(联系人、媒体库等),直接使用即可。
-
自定义 ContentProvider 通常基于 SQLite,结合
UriMatcher区分不同 URI。 -
开发中注意权限管理、关闭 Cursor、发送数据变更通知,以及 Android 版本适配。
延伸阅读推荐: