Skip to content

Android ContentProvider 内容提供者 - 全面详解

ContentProvider 是 Android 四大组件之一,用于在应用间安全地共享数据。


目录

  1. ContentProvider 基础概念
  2. CRUD 操作详解
  3. UriMatcher 使用
  4. ContentObserver 监听变化
  5. 跨进程数据共享
  6. 权限控制
  7. 常用 ContentProvider
  8. 自定义 ContentProvider
  9. 面试考点

1. ContentProvider 基础概念

1.1 什么是 ContentProvider

ContentProvider(内容提供者)是 Android 四大组件之一,为应用提供了一种标准化的数据共享接口。

核心作用:

  • ✅ 应用间数据共享
  • ✅ 统一的数据访问接口
  • ✅ 权限控制和数据安全
  • ✅ 数据抽象和封装
  • ✅ 支持远程查询(跨进程)

1.2 为什么需要 ContentProvider

问题场景:没有 ContentProvider 时

┌─────────────┐     ┌─────────────┐
│   App A     │     │   App B     │
│             │     │             │
│ 直接访问数据库 │ ❌  │ 数据库私有    │
│             │     │             │
└─────────────┘     └─────────────┘

问题:
- 无法直接访问其他应用的数据库
- 数据库结构变化会导致兼容性问题
- 没有统一的权限控制机制

使用 ContentProvider 后:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   App A     │────▶│ ContentProvider│────▶│  App B      │
│             │     │   (App C)    │     │             │
│   query     │     │              │     │  query      │
│  insert     │     │  数据库      │     │  update     │
│             │     │              │     │             │
└─────────────┘     └─────────────┘     └─────────────┘

优势:
- 统一的数据访问接口
- 权限控制
- 数据抽象(App B 不需要知道 App C 的数据库结构)

1.3 ContentProvider 工作原理

┌────────────────────────────────────────────────────────┐
│              ContentProvider 工作流程                   │
│                                                       │
│   ┌──────────┐    URI    ┌─────────────┐             │
│   │ Consumer │ ───────>  │ Provider    │             │
│   │ (Client) │ 查询请求   │ (Content)   │             │
│   └──────────┘           └──────┬──────┘             │
│                                │                     │
│                          解析 URI                      │
│                                │                     │
│                                ▼                     │
│                          ┌──────────┐               │
│                          │ Database │               │
│                          │  /File   │               │
│                          │  /Memory │               │
│                          └──────────┘               │
│                                │                     │
│                                ▼                     │
│                          ┌──────────┐               │
│                          │  Cursor  │               │
│                          │  (结果)  │               │
│                          └────┬─────┘               │
│                               │                     │
│                               ▼                     │
│   ┌──────────┐              返回数据                 │
│   │ Consumer │ ◀──────────────────────────          │
│   └──────────┘                                      │
│                                                       │
└────────────────────────────────────────────────────────┘

1.4 ContentProvider 的 URI 结构

ContentProvider 通过 URI(统一资源标识符)来定位数据。

URI 格式:

content://authority/path/id

例如:
content://com.example.provider/books/123
        │           │        │    │
        │           │        │    └─ ID(可选)
        │           │        └───── 路径
        │           └────────────── 权威标识符
        └────────────────────────── 方案(固定为 content)

解析示例:

kotlin
val uri = Uri.parse("content://com.example.provider/books/123")

uri.scheme        // "content"
uri.authority     // "com.example.provider"
uri.path          // "/books/123"
uri.pathSegments  // ["books", "123"]
uri.lastPathSegment // "123"

2. CRUD 操作详解

2.1 ContentProvider 核心方法

kotlin
class MyContentProvider : ContentProvider() {
    
    // 1. 查询数据
    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        // 实现查询逻辑
        return database.query(...)
    }
    
    // 2. 插入数据
    override fun insert(
        uri: Uri,
        values: ContentValues?
    ): Uri? {
        // 实现插入逻辑
        val id = database.insert(...)
        return Uri.withAppendedPath(uri, id.toString())
    }
    
    // 3. 更新数据
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        // 实现更新逻辑
        return database.update(...)
    }
    
    // 4. 删除数据
    override fun delete(
        uri: Uri,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        // 实现删除逻辑
        return database.delete(...)
    }
    
    // 5. 返回 MIME 类型
    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            BOOKS -> "vnd.android.cursor.dir/vnd.com.example.book"
            BOOK -> "vnd.android.cursor.item/vnd.com.example.book"
            else -> null
        }
    }
    
    // 6. 初始化
    override fun onCreate(): Boolean {
        // 初始化数据库
        return true
    }
}

2.2 查询操作(Query)

ContentResolver 查询:

kotlin
// 方式 1:查询所有数据
fun queryAllBooks(context: Context) {
    val cursor = context.contentResolver.query(
        BooksContract.CONTENT_URI,
        null,  // 所有列
        null,  // 无筛选条件
        null,  // 无筛选参数
        null   // 无排序
    )
    
    cursor?.use {
        while (it.moveToNext()) {
            val id = it.getInt(it.getColumnIndexOrThrow(BooksContract._ID))
            val title = it.getString(it.getColumnIndexOrThrow(BooksContract.TITLE))
            val author = it.getString(it.getColumnIndexOrThrow(BooksContract.AUTHOR))
            Log.d("Book", "$id: $title by $author")
        }
    }
}

// 方式 2:查询指定列
fun queryBookTitles(context: Context) {
    val projection = arrayOf(
        BooksContract.TITLE,
        BooksContract.AUTHOR
    )
    
    val cursor = context.contentResolver.query(
        BooksContract.CONTENT_URI,
        projection,
        null,
        null,
        null
    )
}

// 方式 3:带筛选条件查询
fun queryBooksByAuthor(context: Context, author: String) {
    val selection = "${BooksContract.AUTHOR} = ?"
    val selectionArgs = arrayOf(author)
    
    val cursor = context.contentResolver.query(
        BooksContract.CONTENT_URI,
        null,
        selection,
        selectionArgs,
        null
    )
}

// 方式 4:带排序查询
fun queryBooksSorted(context: Context) {
    val sortOrder = "${BooksContract.TITLE} ASC, ${BooksContract.CREATED_DATE} DESC"
    
    val cursor = context.contentResolver.query(
        BooksContract.CONTENT_URI,
        null,
        null,
        null,
        sortOrder
    )
}

// 方式 5:查询指定 ID 的书籍
fun queryBookById(context: Context, id: Long) {
    val bookUri = ContentUris.withAppendedId(
        BooksContract.CONTENT_URI,
        id
    )
    
    val cursor = context.contentResolver.query(
        bookUri,
        null,
        null,
        null,
        null
    )
}

2.3 插入操作(Insert)

kotlin
// 插入单条数据
fun insertBook(context: Context, title: String, author: String): Uri? {
    val values = ContentValues().apply {
        put(BooksContract.TITLE, title)
        put(BooksContract.AUTHOR, author)
        put(BooksContract.PAGE_COUNT, 300)
        put(BooksContract.CREATED_DATE, System.currentTimeMillis())
    }
    
    return context.contentResolver.insert(
        BooksContract.CONTENT_URI,
        values
    )
}

// 插入多条数据(使用 Transaction)
fun insertMultipleBooks(context: Context, books: List<Book>) {
    context.contentResolver.apply {
        acquireContentProviderTransaction()
        try {
            for (book in books) {
                val values = ContentValues().apply {
                    put(BooksContract.TITLE, book.title)
                    put(BooksContract.AUTHOR, book.author)
                }
                insert(BooksContract.CONTENT_URI, values)
            }
        } finally {
            finishContentProviderTransaction()
        }
    }
}

// 使用 BulkInsertCallback 批量插入
fun bulkInsertBooks(context: Context, books: List<Book>) {
    val values = books.map { book ->
        ContentValues().apply {
            put(BooksContract.TITLE, book.title)
            put(BooksContract.AUTHOR, book.author)
        }
    }.toTypedArray()
    
    context.contentResolver.bulkInsert(
        BooksContract.CONTENT_URI,
        values
    )
}

2.4 更新操作(Update)

kotlin
// 更新指定 ID 的数据
fun updateBook(context: Context, id: Long, newTitle: String): Int {
    val values = ContentValues().apply {
        put(BooksContract.TITLE, newTitle)
        put(BooksContract.UPDATED_DATE, System.currentTimeMillis())
    }
    
    val selection = "${BooksContract._ID} = ?"
    val selectionArgs = arrayOf(id.toString())
    
    return context.contentResolver.update(
        ContentUris.withAppendedId(BooksContract.CONTENT_URI, id),
        values,
        null,
        null
    )
}

// 更新多条数据
fun updateBooksByAuthor(context: Context, author: String, newAuthor: String): Int {
    val values = ContentValues().apply {
        put(BooksContract.AUTHOR, newAuthor)
    }
    
    val selection = "${BooksContract.AUTHOR} = ?"
    val selectionArgs = arrayOf(author)
    
    return context.contentResolver.update(
        BooksContract.CONTENT_URI,
        values,
        selection,
        selectionArgs
    )
}

2.5 删除操作(Delete)

kotlin
// 删除指定 ID 的数据
fun deleteBook(context: Context, id: Long): Int {
    val bookUri = ContentUris.withAppendedId(BooksContract.CONTENT_URI, id)
    return context.contentResolver.delete(bookUri, null, null)
}

// 删除多条数据
fun deleteBooksByAuthor(context: Context, author: String): Int {
    val selection = "${BooksContract.AUTHOR} = ?"
    val selectionArgs = arrayOf(author)
    
    return context.contentResolver.delete(
        BooksContract.CONTENT_URI,
        selection,
        selectionArgs
    )
}

// 删除所有数据
fun deleteAllBooks(context: Context): Int {
    return context.contentResolver.delete(
        BooksContract.CONTENT_URI,
        null,
        null
    )
}

2.6 使用 Kotlin Flow 进行协程查询

kotlin
// 将 Cursor 转换为 Flow
fun Context.booksFlow(): Flow<List<Book>> = flow {
    val cursor = contentResolver.query(
        BooksContract.CONTENT_URI,
        null,
        null,
        null,
        null
    )
    
    val books = mutableListOf<Book>()
    cursor?.use {
        while (it.moveToNext()) {
            val book = Book(
                id = it.getLong(it.getColumnIndexOrThrow(BooksContract._ID)),
                title = it.getString(it.getColumnIndexOrThrow(BooksContract.TITLE)),
                author = it.getString(it.getColumnIndexOrThrow(BooksContract.AUTHOR))
            )
            books.add(book)
        }
    }
    
    emit(books)
}

// 在 ViewModel 中使用
class BooksViewModel(application: Application) : ViewModel() {
    private val context = application.applicationContext
    
    private val _books = MutableStateFlow<List<Book>>(emptyList())
    val books: StateFlow<List<Book>> = _books.asStateFlow()
    
    init {
        loadBooks()
    }
    
    private fun loadBooks() {
        viewModelScope.launch {
            context.booksFlow().collect { books ->
                _books.value = books
            }
        }
    }
}

3. UriMatcher 使用

3.1 什么是 UriMatcher

UriMatcher 用于解析和匹配 ContentProvider 的 URI,根据匹配结果返回不同的代码,便于在 ContentProvider 中进行相应的处理。

3.2 添加 URI 匹配规则

kotlin
class BooksContentProvider : ContentProvider() {
    
    private lateinit var database: SQLiteDatabase
    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
    
    companion object {
        // 定义匹配代码
        private const val BOOKS = 1      // 匹配 /books
        private const val BOOK = 2       // 匹配 /books/123
        private const val AUTHORS = 3    // 匹配 /authors
        private const val AUTHOR = 4     // 匹配 /authors/456
    }
    
    override fun onCreate(): Boolean {
        // 初始化数据库
        database = dbHelper.writableDatabase
        
        // 添加匹配规则
        uriMatcher.addURI(
            BooksContract.AUTHORITY,      // 权威标识符
            BooksContract.PATH_BOOKS,     // 路径
            BOOKS                         // 匹配代码
        )
        
        uriMatcher.addURI(
            BooksContract.AUTHORITY,
            BooksContract.PATH_BOOKS + "/#",  // # 表示数字 ID
            BOOK
        )
        
        uriMatcher.addURI(
            BooksContract.AUTHORITY,
            BooksContract.PATH_AUTHORS,
            AUTHORS
        )
        
        uriMatcher.addURI(
            BooksContract.AUTHORITY,
            BooksContract.PATH_AUTHORS + "/#",
            AUTHOR
        )
        
        return true
    }
    
    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        return when (uriMatcher.match(uri)) {
            BOOKS -> {
                // 查询所有书籍
                database.query(
                    BooksContract.TABLE_BOOKS,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder
                )
            }
            BOOK -> {
                // 查询指定 ID 的书籍
                val id = uri.lastPathSegment?.toLongOrNull() ?: return null
                val newSelection = "(${selection ?: ""}) AND ${BooksContract._ID} = ?"
                val newSelectionArgs = if (selectionArgs != null) {
                    selectionArgs + id.toString()
                } else {
                    arrayOf(id.toString())
                }
                
                database.query(
                    BooksContract.TABLE_BOOKS,
                    projection,
                    newSelection,
                    newSelectionArgs,
                    null,
                    null,
                    sortOrder
                )
            }
            AUTHORS -> {
                // 查询所有作者
                database.query(
                    AuthorsContract.TABLE_AUTHORS,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder
                )
            }
            AUTHOR -> {
                // 查询指定 ID 的作者
                val id = uri.lastPathSegment?.toLongOrNull() ?: return null
                // ... 类似 BOOK 的处理
                null
            }
            else -> {
                throw IllegalArgumentException("Unknown URI: $uri")
            }
        }
    }
    
    // insert, update, delete 方法同样使用 uriMatcher 判断
}

3.3 UriMatcher 匹配规则

kotlin
// 精确匹配
uriMatcher.addURI("com.example", "books", BOOKS)
// 匹配:content://com.example/books

// 通配符匹配(# 表示数字)
uriMatcher.addURI("com.example", "books/#", BOOK)
// 匹配:content://com.example/books/123

// 通配符匹配(* 表示任意字符)
uriMatcher.addURI("com.example", "books/*/authors", BOOK_AUTHORS)
// 匹配:content://com.example/books/123/authors

// 多级路径匹配
uriMatcher.addURI("com.example", "books/*/chapters/*", BOOK_CHAPTER)
// 匹配:content://com.example/books/123/chapters/456

3.4 自定义 UriMatcher

kotlin
class CustomUriMatcher : UriMatcher(NO_MATCH) {
    
    companion object {
        const val USERS = 100
        const val USER = 101
        const val USER_POSTS = 102
        const val USER_POST = 103
    }
    
    fun addRules(authority: String) {
        addURI(authority, "users", USERS)
        addURI(authority, "users/#", USER)
        addURI(authority, "users/*/posts", USER_POSTS)
        addURI(authority, "users/*/posts/#", USER_POST)
    }
    
    fun extractUserId(uri: Uri): Long? {
        return when (match(uri)) {
            USER, USER_POSTS, USER_POST -> {
                uri.pathSegments[1].toLongOrNull()
            }
            else -> null
        }
    }
    
    fun extractPostId(uri: Uri): Long? {
        return if (match(uri) == USER_POST) {
            uri.pathSegments[3].toLongOrNull()
        } else {
            null
        }
    }
}

4. ContentObserver 监听变化

4.1 什么是 ContentObserver

ContentObserver 用于监听 ContentProvider 的数据变化,当数据发生增删改时自动通知观察者。

4.2 注册 ContentObserver

kotlin
class BookActivity : AppCompatActivity() {
    
    private var contentObserver: ContentObserver? = null
    
    override fun onStart() {
        super.onStart()
        
        // 创建 ContentObserver
        contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
            override fun onChange(selfChange: Boolean) {
                // 数据变化时调用
                refreshBooks()
            }
            
            override fun onChange(selfChange: Boolean, uri: Uri?) {
                // 带 URI 的变化通知
                if (uri == BooksContract.CONTENT_URI) {
                    refreshBooks()
                }
            }
        }
        
        // 注册观察者
        contentResolver.registerContentObserver(
            BooksContract.CONTENT_URI,
            true,  // 是否立即通知
            contentObserver
        )
    }
    
    override fun onStop() {
        super.onStop()
        
        // 注销观察者
        contentObserver?.let {
            contentResolver.unregisterContentObserver(it)
        }
    }
    
    private fun refreshBooks() {
        // 重新加载数据
        loadBooks()
    }
}

4.3 通知观察者

在 ContentProvider 中通知数据变化:

kotlin
class BooksContentProvider : ContentProvider() {
    
    override fun insert(
        uri: Uri,
        values: ContentValues?
    ): Uri? {
        val id = database.insert(
            BooksContract.TABLE_BOOKS,
            null,
            values
        )
        
        // 插入成功后通知观察者
        val insertUri = ContentUris.withAppendedId(uri, id)
        context?.contentResolver?.notifyChange(insertUri, null)
        
        return insertUri
    }
    
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        val count = database.update(
            BooksContract.TABLE_BOOKS,
            values,
            selection,
            selectionArgs
        )
        
        // 更新成功后通知观察者
        if (count > 0) {
            context?.contentResolver?.notifyChange(uri, null)
        }
        
        return count
    }
    
    override fun delete(
        uri: Uri,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        val count = database.delete(
            BooksContract.TABLE_BOOKS,
            selection,
            selectionArgs
        )
        
        // 删除成功后通知观察者
        if (count > 0) {
            context?.contentResolver?.notifyChange(uri, null)
        }
        
        return count
    }
}

4.4 使用 LiveData 监听

kotlin
// 使用 LiveData 包装 ContentObserver
fun Context.observeBooks(): LiveData<List<Book>> {
    return object : LiveData<List<Book>>() {
        private val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
            override fun onChange(selfChange: Boolean) {
                postValue(loadBooks())
            }
        }
        
        override fun onActive() {
            super.onActive()
            contentResolver.registerContentObserver(
                BooksContract.CONTENT_URI,
                true,
                contentObserver
            )
        }
        
        override fun onInactive() {
            super.onInactive()
            contentResolver.unregisterContentObserver(contentObserver)
        }
        
        private fun loadBooks(): List<Book> {
            // 查询书籍
            return emptyList()
        }
    }
}

// 在 Activity 中使用
bookLiveData.observe(this) { books ->
    adapter.submitList(books)
}

4.5 使用 Flow 监听

kotlin
fun Context.booksFlow(): StateFlow<List<Book>> {
    return MutableStateFlow(emptyList<Book>()).apply {
        val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
            override fun onChange(selfChange: Boolean) {
                val books = contentResolver.query(
                    BooksContract.CONTENT_URI,
                    null,
                    null,
                    null,
                    null
                )?.use { cursor ->
                    cursor.toBooksList()
                } ?: emptyList()
                
                value = books
            }
        }
        
        // 注册观察者
        contentResolver.registerContentObserver(
            BooksContract.CONTENT_URI,
            true,
            observer
        )
    }
}

5. 跨进程数据共享

5.1 跨进程通信原理

┌─────────────────┐              ┌─────────────────┐
│   App A         │              │   App B         │
│                 │              │                 │
│  ContentResolver│              │  ContentProvider│
│                 │              │                 │
│    query()      │────────────▶ │   query()       │
│                 │   IPC (Binder)              │
│   Cursor        │◀──────────── │   Cursor        │
│                 │              │                 │
└─────────────────┘              └─────────────────┘

5.2 设置跨进程访问

在 Manifest 中声明:

xml
<provider
    android:name=".BooksContentProvider"
    android:authorities="com.example.provider.books"
    android:exported="true"        <!-- 允许跨进程访问 -->
    android:grantUriPermissions="true"
    android:permission="com.example.READ_BOOKS_PERMISSION">
    
    <path-permission
        android:path="/books"
        android:readPermission="com.example.READ_BOOKS_PERMISSION"
        android:writePermission="com.example.WRITE_BOOKS_PERMISSION" />
</provider>

<!-- 定义权限 -->
<permission
    android:name="com.example.READ_BOOKS_PERMISSION"
    android:protectionLevel="normal" />
<permission
    android:name="com.example.WRITE_BOOKS_PERMISSION"
    android:protectionLevel="normal" />

其他应用访问:

kotlin
// App B 访问 App A 的 ContentProvider

// 1. 声明使用权限
// App B 的 AndroidManifest.xml
<uses-permission android:name="com.example.READ_BOOKS_PERMISSION" />
<uses-permission android:name="com.example.WRITE_BOOKS_PERMISSION" />

// 2. 查询数据
fun queryBooksFromOtherApp(context: Context) {
    val uri = Uri.parse("content://com.example.provider.books/books")
    
    val cursor = context.contentResolver.query(
        uri,
        arrayOf("title", "author"),
        null,
        null,
        null
    )
    
    cursor?.use {
        while (it.moveToNext()) {
            val title = it.getString(0)
            val author = it.getString(1)
            Log.d("Books", "$title by $author")
        }
    }
}

// 3. 插入数据
fun insertBookToOtherApp(context: Context, title: String, author: String) {
    val uri = Uri.parse("content://com.example.provider.books/books")
    val values = ContentValues().apply {
        put("title", title)
        put("author", author)
    }
    
    context.contentResolver.insert(uri, values)
}

5.3 跨进程 Cursor

跨进程 Cursor 的使用注意事项:

kotlin
// ⚠️ 注意:跨进程返回的 Cursor 不能直接使用,需要转换

// 方式 1:使用 copy()
val cursor = contentResolver.query(uri, projection, null, null, null)
val localCursor = cursor?.copy()
localCursor?.use {
    // 安全使用
}

// 方式 2:转换为 List
val cursor = contentResolver.query(uri, projection, null, null, null)
val data = cursor?.toList()
cursor?.close()

// 方式 3:使用 ContentResolver 的便捷方法
val books = queryBooksAsList(context, uri)

6. 权限控制

6.1 权限保护级别

xml
<!-- normal:安装时自动授予 -->
<permission
    android:name="com.example.NORMAL_PERMISSION"
    android:protectionLevel="normal" />

<!-- dangerous:用户手动授予 -->
<permission
    android:name="com.example.DANGEROUS_PERMISSION"
    android:protectionLevel="dangerous" />

<!-- signature:签名一致才能获取 -->
<permission
    android:name="com.example.SIGNATURE_PERMISSION"
    android:protectionLevel="signature" />

<!-- signatureOrSystem:签名一致或系统应用 -->
<permission
    android:name="com.example.SIGNATURE_OR_SYSTEM_PERMISSION"
    android:protectionLevel="signatureOrSystem" />

6.2 ContentProvider 权限设置

xml
<provider
    android:name=".SecureContentProvider"
    android:authorities="com.example.secure.provider"
    android:exported="true"
    android:permission="com.example.ACCESS_SECURE_DATA">
    
    <!-- 路径级别的权限 -->
    <path-permission
        android:path="/public"
        android:readPermission="com.example.READ_PUBLIC"
        android:writePermission="com.example.WRITE_PUBLIC" />
    
    <path-permission
        android:path="/private"
        android:readPermission="com.example.READ_PRIVATE"
        android:writePermission="com.example.WRITE_PRIVATE" />
</provider>

6.3 运行时权限检查

kotlin
class SecureContentProvider : ContentProvider() {
    
    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        // 检查权限
        if (!hasReadPermission()) {
            throw SecurityException("Permission denied for reading")
        }
        
        return database.query(...)
    }
    
    override fun insert(
        uri: Uri,
        values: ContentValues?
    ): Uri? {
        // 检查写权限
        if (!hasWritePermission()) {
            throw SecurityException("Permission denied for writing")
        }
        
        return super.insert(uri, values)
    }
    
    private fun hasReadPermission(): Boolean {
        return context?.checkCallingOrSelfPermission(
            "com.example.READ_PRIVATE"
        ) == PackageManager.PERMISSION_GRANTED
    }
    
    private fun hasWritePermission(): Boolean {
        return context?.checkCallingOrSelfPermission(
            "com.example.WRITE_PRIVATE"
        ) == PackageManager.PERMISSION_GRANTED
    }
}

7. 常用 ContentProvider

7.1 ContactsProvider(联系人)

kotlin
// 查询联系人
fun queryContacts(context: Context): List<Contact> {
    val projection = arrayOf(
        ContactsContract.Contacts._ID,
        ContactsContract.Contacts.DISPLAY_NAME,
        ContactsContract.Contacts.HAS_PHONE_NUMBER
    )
    
    val contacts = mutableListOf<Contact>()
    
    val cursor = context.contentResolver.query(
        ContactsContract.Contacts.CONTENT_URI,
        projection,
        null,
        null,
        "${ContactsContract.Contacts.DISPLAY_NAME} ASC"
    )
    
    cursor?.use {
        while (it.moveToNext()) {
            val id = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
            val name = it.getString(it.getColumnIndexOrThrow(ContactsContract.DISPLAY_NAME))
            val hasPhone = it.getInt(it.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER)) == 1
            
            if (hasPhone) {
                val phones = queryPhoneNumbers(context, id)
                contacts.add(Contact(name, phones))
            }
        }
    }
    
    return contacts
}

// 查询电话号码
fun queryPhoneNumbers(context: Context, contactId: String): List<String> {
    val phones = mutableListOf<String>()
    
    val cursor = context.contentResolver.query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER),
        "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
        arrayOf(contactId),
        null
    )
    
    cursor?.use {
        while (it.moveToNext()) {
            val number = it.getString(it.getColumnIndexOrThrow(
                ContactsContract.CommonDataKinds.Phone.NUMBER
            ))
            phones.add(number)
        }
    }
    
    return phones
}

// 添加联系人
fun addContact(
    context: Context,
    name: String,
    phone: String,
    email: String?
) {
    val rawContactId = context.contentResolver.insert(
        ContactsContract.RawContacts.CONTENT_URI,
        ContentValues().apply {
            put(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
            put(ContactsContract.RawContacts.ACCOUNT_NAME, null)
        }
    )?.lastPathSegment?.toLongOrNull()
    
    if (rawContactId != null) {
        // 设置姓名
        context.contentResolver.insert(
            ContactsContract.Data.CONTENT_URI,
            ContentValues().apply {
                put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
                put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.MIMETYPE)
                put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, name)
            }
        )
        
        // 设置电话
        context.contentResolver.insert(
            ContactsContract.Data.CONTENT_URI,
            ContentValues().apply {
                put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
                put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.MIMETYPE)
                put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
                put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
            }
        )
        
        // 设置邮箱
        if (email != null) {
            context.contentResolver.insert(
                ContactsContract.Data.CONTENT_URI,
                ContentValues().apply {
                    put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
                    put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.MIMETYPE)
                    put(ContactsContract.CommonDataKinds.Email.DATA, email)
                }
            )
        }
    }
}

7.2 MediaStore(媒体存储)

kotlin
// 查询图片
fun queryImages(context: Context): List<Uri> {
    val projection = arrayOf(
        MediaStore.Images.Media._ID,
        MediaStore.Images.Media.DISPLAY_NAME,
        MediaStore.Images.Media.DATA
    )
    
    val selection = MediaStore.Images.Media.IS_DELETED + " = 0"
    
    val imageUris = mutableListOf<Uri>()
    
    val cursor = context.contentResolver.query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        projection,
        selection,
        null,
        "${MediaStore.Images.Media.DATE_ADDED} DESC"
    )
    
    cursor?.use {
        while (it.moveToNext()) {
            val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
            val uri = ContentUris.withAppendedId(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                id
            )
            imageUris.add(uri)
        }
    }
    
    return imageUris
}

// 保存图片
fun saveImage(context: Context, bitmap: Bitmap, title: String): Uri? {
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, title)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
    }
    
    val uri = context.contentResolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        values
    )
    
    uri?.let {
        context.contentResolver.openOutputStream(it)?.use { outputStream ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
        }
    }
    
    return uri
}

// 查询视频
fun queryVideos(context: Context): List<Video> {
    val projection = arrayOf(
        MediaStore.Video.Media._ID,
        MediaStore.Video.Media.DISPLAY_NAME,
        MediaStore.Video.Media.DURATION,
        MediaStore.Video.Media.SIZE
    )
    
    val videos = mutableListOf<Video>()
    
    val cursor = context.contentResolver.query(
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
        projection,
        null,
        null,
        null
    )
    
    cursor?.use {
        while (it.moveToNext()) {
            val id = it.getLong(0)
            val name = it.getString(1)
            val duration = it.getLong(2)
            val size = it.getLong(3)
            
            videos.add(Video(id, name, duration, size))
        }
    }
    
    return videos
}

7.3 CallLogProvider(通话记录)

kotlin
// 查询通话记录
fun queryCallLog(context: Context): List<CallRecord> {
    val projection = arrayOf(
        CallLog.Calls._ID,
        CallLog.Calls.NUMBER,
        CallLog.Calls.CACHED_NAME,
        CallLog.Calls.TYPE,
        CallLog.Calls.DATE
    )
    
    val records = mutableListOf<CallRecord>()
    
    val cursor = context.contentResolver.query(
        CallLog.Calls.CONTENT_URI,
        projection,
        null,
        null,
        "${CallLog.Calls.DATE} DESC"
    )
    
    cursor?.use {
        while (it.moveToNext()) {
            val number = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
            val name = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.CACHED_NAME))
            val type = it.getInt(it.getColumnIndexOrThrow(CallLog.Calls.TYPE))
            val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
            
            val callType = when (type) {
                CallLog.Calls.INCOMING_TYPE -> "来电"
                CallLog.Calls.OUTGOING_TYPE -> "去电"
                CallLog.Calls.MISSED_TYPE -> "未接"
                else -> "未知"
            }
            
            records.add(CallRecord(number, name, callType, date))
        }
    }
    
    return records
}

8. 自定义 ContentProvider

8.1 完整的 ContentProvider 实现

定义 Contract 类:

kotlin
class BooksContract {
    companion object {
        // 权威标识符
        const val AUTHORITY = "com.example.provider.books"
        
        // Content URI
        val CONTENT_URI: Uri = Uri.parse(
            "content://$AUTHORITY/books"
        )
        
        // 路径
        const val PATH_BOOKS = "books"
        const val PATH_AUTHORS = "authors"
        
        // 表名
        const val TABLE_BOOKS = "books"
        const val TABLE_AUTHORS = "authors"
        
        // 列名
        const val _ID = "_id"
        const val TITLE = "title"
        const val AUTHOR = "author"
        const val PAGE_COUNT = "page_count"
        const val CREATED_DATE = "created_date"
        const val UPDATED_DATE = "updated_date"
        
        // MIME 类型
        const val MIME_BOOKS = "vnd.android.cursor.dir/vnd.com.example.book"
        const val MIME_BOOK = "vnd.android.cursor.item/vnd.com.example.book"
    }
}

创建数据库帮助类:

kotlin
class DatabaseHelper(context: Context) : 
    SQLiteOpenHelper(context, "books.db", null, DATABASE_VERSION) {
    
    companion object {
        const val DATABASE_VERSION = 1
        
        private const val CREATE_BOOKS = """
            CREATE TABLE ${BooksContract.TABLE_BOOKS} (
                ${BooksContract._ID} INTEGER PRIMARY KEY AUTOINCREMENT,
                ${BooksContract.TITLE} TEXT NOT NULL,
                ${BooksContract.AUTHOR} TEXT NOT NULL,
                ${BooksContract.PAGE_COUNT} INTEGER,
                ${BooksContract.CREATED_DATE} INTEGER,
                ${BooksContract.UPDATED_DATE} INTEGER
            )
        """
    }
    
    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL(CREATE_BOOKS)
    }
    
    override fun onUpgrade(
        db: SQLiteDatabase?,
        oldVersion: Int,
        newVersion: Int
    ) {
        // 处理数据库升级
    }
}

实现 ContentProvider:

kotlin
class BooksContentProvider : ContentProvider() {
    
    private var database: SQLiteDatabase? = null
    private var context: Context? = null
    
    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
        addURI(BooksContract.AUTHORITY, BooksContract.PATH_BOOKS, BOOKS)
        addURI(BooksContract.AUTHORITY, BooksContract.PATH_BOOKS + "/#", BOOK)
    }
    
    companion object {
        private const val BOOKS = 1
        private const val BOOK = 2
    }
    
    override fun onCreate(): Boolean {
        context = context
        database = DatabaseHelper(context).writableDatabase
        return true
    }
    
    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        val db = database ?: return null
        
        return when (uriMatcher.match(uri)) {
            BOOKS -> db.query(
                BooksContract.TABLE_BOOKS,
                projection,
                selection,
                selectionArgs,
                null,
                null,
                sortOrder
            )
            BOOK -> {
                val id = uri.lastPathSegment?.toLongOrNull() ?: return null
                db.query(
                    BooksContract.TABLE_BOOKS,
                    projection,
                    "(${selection ?: ""}) AND ${BooksContract._ID} = ?",
                    if (selectionArgs != null) selectionArgs + id.toString()
                    else arrayOf(id.toString()),
                    null,
                    null,
                    sortOrder
                )
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
    }
    
    override fun insert(
        uri: Uri,
        values: ContentValues?
    ): Uri? {
        if (uriMatcher.match(uri) != BOOKS || values == null) {
            return null
        }
        
        val id = database?.insertOrThrow(
            BooksContract.TABLE_BOOKS,
            null,
            values
        )
        
        context?.contentResolver?.notifyChange(
            ContentUris.withAppendedId(uri, id),
            null
        )
        
        return ContentUris.withAppendedId(uri, id)
    }
    
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        return when (uriMatcher.match(uri)) {
            BOOKS -> database?.update(
                BooksContract.TABLE_BOOKS,
                values,
                selection,
                selectionArgs
            )
            BOOK -> {
                val id = uri.lastPathSegment?.toLongOrNull() ?: return 0
                database?.update(
                    BooksContract.TABLE_BOOKS,
                    values,
                    "${BooksContract._ID} = ?",
                    arrayOf(id.toString())
                )
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }?.let {
            context?.contentResolver?.notifyChange(uri, null)
            it
        } ?: 0
    }
    
    override fun delete(
        uri: Uri,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        return when (uriMatcher.match(uri)) {
            BOOKS -> database?.delete(
                BooksContract.TABLE_BOOKS,
                selection,
                selectionArgs
            )
            BOOK -> {
                val id = uri.lastPathSegment?.toLongOrNull() ?: return 0
                database?.delete(
                    BooksContract.TABLE_BOOKS,
                    "${BooksContract._ID} = ?",
                    arrayOf(id.toString())
                )
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }?.let {
            context?.contentResolver?.notifyChange(uri, null)
            it
        } ?: 0
    }
    
    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            BOOKS -> BooksContract.MIME_BOOKS
            BOOK -> BooksContract.MIME_BOOK
            else -> null
        }
    }
}

在 Manifest 中注册:

xml
<provider
    android:name=".BooksContentProvider"
    android:authorities="com.example.provider.books"
    android:exported="false"
    android:grantUriPermissions="true" />

9. 面试考点

9.1 基础题

Q1: ContentProvider 的作用是什么?

A: ContentProvider 用于在应用间安全地共享数据,提供统一的数据访问接口,支持权限控制和跨进程通信。

Q2: ContentProvider 的四大核心方法?

A:

  • query(): 查询数据,返回 Cursor
  • insert(): 插入数据,返回 URI
  • update(): 更新数据,返回更新行数
  • delete(): 删除数据,返回删除行数

Q3: UriMatcher 的作用?

A: UriMatcher 用于解析和匹配 ContentProvider 的 URI,根据匹配结果返回不同的代码,便于进行相应的数据处理。

9.2 进阶题

Q4: ContentObserver 如何使用?

A: ContentObserver 用于监听 ContentProvider 的数据变化,在数据增删改时自动通知观察者,实现数据同步。

Q5: 如何保护 ContentProvider 的访问权限?

A:

  • 设置 android:exported 控制是否跨进程访问
  • 使用 android:permission 设置访问权限
  • 使用 path-permission 设置路径级别权限
  • 在代码中手动检查权限

Q6: ContentProvider 跨进程返回的 Cursor 有什么特殊性?

A: 跨进程返回的是 CursorWindow,不能直接修改,需要使用 copy() 方法转换为本地 Cursor。

9.3 高级题

Q7: ContentProvider 的实现原理?

A: ContentProvider 通过 Binder 实现跨进程通信,客户端通过 ContentResolver 发送请求,服务端 ContentProvider 接收请求并操作数据源。

Q8: 如何高效地批量插入数据?

A:

  • 使用 bulkInsert() 方法
  • 使用 Transaction 事务
  • 实现 BulkInsertCallback 接口

Q9: ContentProvider 和直接数据库访问的区别?

A:

  • ContentProvider 提供统一接口,直接数据库访问暴露实现细节
  • ContentProvider 支持跨进程,数据库访问仅限本进程
  • ContentProvider 有权限控制,数据库访问无权限控制

总结

ContentProvider 是 Android 数据共享的核心机制,掌握要点:

  1. 理解 URI 的作用和结构
  2. 掌握 CRUD 操作的实现
  3. 熟练使用 UriMatcher 进行 URI 匹配
  4. 使用 ContentObserver 监听数据变化
  5. 注意跨进程访问的安全性和性能
  6. 了解常用系统 ContentProvider 的使用