首页 / Android 入门教程 / ContentProvider 基础

Android 入门教程

ContentProvider 基础

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

AndroidAndroid 入门教程ContentProvider内容提供者内容URIUriMatcher

37. ContentProvider 基础

本节目标:理解内容提供者(ContentProvider)的作用和数据模型,掌握内容 URI 的结构、CRUD 操作流程,学会创建一个能对外共享数据的 ContentProvider。

内容提供者是什么

打个比方:你的应用里有一本通讯录,别的应用想读里面的联系人。你不能直接让对方翻你的数据库文件—那是你的私有地盘。内容提供者就像一个「前台窗口」,别人要数据得通过这个窗口,你决定给什么、不给什么、谁能访问。

内容提供者(ContentProvider)是四大组件之一,作用是管理一块中央数据存储,并允许其他应用安全地访问。它负责进程间通信和安全控制,让你不用自己处理跨进程的数据传递。

通常在两种场景下用到它:

  1. 访问别的应用的 Provider:比如读取系统通讯录、相册里的图片。
  2. 创建自己的 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 匹配整个表 → 返回 USERS
  • users/5 匹配单行 → 返回 USERS_ID
Note

notifyChange() 很重要。它通知所有监听这个 URI 的 CursorContentObserver:数据变了,该刷新了。忘了调会导致界面不更新。

第三步:注册到清单

<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 更常见,但理解原理能帮你更好地使用系统数据源。