《Android 核心组件深度系列 · 第 4 篇 ContentProvider》

Android 核心组件深度系列·第4篇 ContentProvider

带你彻底搞懂 Android 中最神秘的四大组件之一:ContentProvider,从底层原理到实战演示,一篇通透。


一、前言

在 Android 四大组件中,ContentProvider 是最常被忽略、也最容易"似懂非懂"的一个。

很多开发者知道它是"用来跨进程共享数据的",但却从未真正用过。甚至有人觉得:"我又不需要跨应用共享数据,学它干什么?"

但实际上,Android 系统的核心功能都离不开它:

  • 打开通讯录 → ContactsContract.Contacts
  • 读取相册 → MediaStore.Images
  • 访问短信 → Telephony.Sms
  • 甚至应用安装卸载的广播,底层也是通过 PackageManager 的 ContentProvider 实现的

今天我们就来彻底拆解 ContentProvider 的工作原理、适用场景、实现方式与实战案例

这是 Android 四大组件系列的收官篇,学完它,你对 Android 的架构理解将上升一个台阶。


二、ContentProvider 是什么

2.1 核心概念

一句话定义:

ContentProvider 是 Android 系统提供的一种统一数据访问接口,允许不同应用之间通过 URI 的形式,安全地访问、增删改查数据。

你可以把它理解成:

"一个为外部世界暴露数据库内容的安全中间层"

2.2 工作原理图解

sql 复制代码
应用 A                     Binder IPC                    应用 B
------                     -----------                    ------
ContentResolver  <------>  ContentProvider
     ↓                            ↓
查询 URI                      操作 SQLite
content://authority/table      返回 Cursor

关键角色:

角色 说明
ContentProvider 数据提供者,封装数据操作逻辑
ContentResolver 数据访问者,统一的访问入口
URI 数据定位符,格式:content://authority/path
Cursor 数据结果集,类似数据库游标

2.3 与其他跨进程方案对比

方案 适用场景 优点 缺点
ContentProvider 跨应用数据共享 统一接口、权限控制、数据监听 性能开销大
Intent + Parcelable 简单数据传递 简单快速 数据量受限(1MB)
AIDL 复杂跨进程调用 灵活度高 实现复杂
Messenger 消息传递 自动排队 单线程处理
文件共享 大文件传输 直接读写 权限管理麻烦

什么时候用 ContentProvider?

✅ 需要向其他应用暴露数据

✅ 需要细粒度的权限控制

✅ 需要数据变化监听(ContentObserver)

✅ 需要支持标准的 CRUD 操作

❌ 不推荐用于:

  • 同一应用内的数据访问(用 Room + Repository)
  • 简单的配置共享(用 SharedPreferences)
  • 频繁的小数据传递(用 Intent 或 EventBus)

三、ContentProvider 核心机制

3.1 URI 的组成

一个标准的 ContentProvider URI 由以下部分组成:

bash 复制代码
content://com.example.app/notes/5
   ↓         ↓               ↓    ↓
scheme   authority         path  id
部分 说明 示例
scheme 协议,固定为 content:// content://
authority 唯一标识符,类似域名 com.example.app
path 数据表或资源路径 notes
id 具体数据项(可选) 5

3.2 UriMatcher 的作用

UriMatcher 用于匹配不同的 URI,返回对应的操作代码。

kotlin 复制代码
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    // content://com.example.app/notes
    addURI("com.example.app", "notes", NOTES)
    
    // content://com.example.app/notes/5
    addURI("com.example.app", "notes/#", NOTE_ID)
}

// 使用
when (uriMatcher.match(uri)) {
    NOTES -> // 处理所有笔记
    NOTE_ID -> // 处理单条笔记
    else -> throw IllegalArgumentException("Unknown URI: $uri")
}

3.3 MIME 类型

getType() 方法返回 URI 对应的 MIME 类型,用于标识数据格式。

标准格式:

arduino 复制代码
vnd.android.cursor.dir/vnd.company.type  // 多条记录
vnd.android.cursor.item/vnd.company.type // 单条记录

示例:

kotlin 复制代码
override fun getType(uri: Uri): String? {
    return when (uriMatcher.match(uri)) {
        NOTES -> "vnd.android.cursor.dir/vnd.com.example.notes"
        NOTE_ID -> "vnd.android.cursor.item/vnd.com.example.notes"
        else -> throw IllegalArgumentException("Unknown URI: $uri")
    }
}

四、完整实战示例:笔记应用(这部分感谢ChatGPT和Claude老师的分工协作:)

我们将实现一个完整的笔记 ContentProvider,支持增删改查、URI 匹配、权限控制。

4.1 创建数据库

kotlin 复制代码
// NoteDatabaseHelper.kt
package com.example.contentproviderdemo

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class NoteDatabaseHelper(context: Context) 
    : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
    
    companion object {
        private const val DATABASE_NAME = "Notes.db"
        private const val DATABASE_VERSION = 1
        private const val TABLE_NOTES = "notes"
    }
    
    override fun onCreate(db: SQLiteDatabase) {
        val createTable = """
            CREATE TABLE $TABLE_NOTES (
                _id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                content TEXT,
                created_at INTEGER DEFAULT (strftime('%s', 'now')),
                updated_at INTEGER DEFAULT (strftime('%s', 'now'))
            )
        """.trimIndent()
        
        db.execSQL(createTable)
        
        // 创建索引以优化查询性能
        db.execSQL("CREATE INDEX idx_title ON $TABLE_NOTES(title)")
        db.execSQL("CREATE INDEX idx_created_at ON $TABLE_NOTES(created_at)")
    }
    
    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        // 实际项目中应该做数据迁移
        db.execSQL("DROP TABLE IF EXISTS $TABLE_NOTES")
        onCreate(db)
    }
}

4.2 实现 ContentProvider

kotlin 复制代码
// NoteProvider.kt
package com.example.contentproviderdemo

import android.content.ContentProvider
import android.content.ContentUris
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.database.SQLException
import android.net.Uri
import android.util.Log

class NoteProvider : ContentProvider() {
    
    private lateinit var dbHelper: NoteDatabaseHelper
    
    companion object {
        private const val TAG = "NoteProvider"
        private const val AUTHORITY = "com.example.contentproviderdemo"
        private const val TABLE_NOTES = "notes"
        
        // URI 匹配码
        private const val NOTES = 1
        private const val NOTE_ID = 2
        
        // 公开的 URI
        val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/$TABLE_NOTES")
        
        private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(AUTHORITY, TABLE_NOTES, NOTES)
            addURI(AUTHORITY, "$TABLE_NOTES/#", NOTE_ID)
        }
    }
    
    /**
     * ContentProvider 的生命周期:
     * 1. onCreate() 在 Application.onCreate() 之前调用
     * 2. 不会被销毁,生命周期与应用进程相同
     * 3. 所有方法可能在多线程并发调用,需要确保线程安全
     */
    override fun onCreate(): Boolean {
        Log.d(TAG, "onCreate called")
        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
        
        val cursor = when (uriMatcher.match(uri)) {
            NOTES -> {
                // 查询所有笔记
                db.query(
                    TABLE_NOTES,
                    projection,
                    selection,
                    selectionArgs,
                    null,
                    null,
                    sortOrder ?: "created_at DESC"
                )
            }
            NOTE_ID -> {
                // 查询指定 ID 的笔记
                val id = uri.lastPathSegment
                db.query(
                    TABLE_NOTES,
                    projection,
                    "_id=?",
                    arrayOf(id),
                    null,
                    null,
                    sortOrder
                )
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
        
        // 注册监听器,当数据变化时通知观察者
        cursor?.setNotificationUri(context?.contentResolver, uri)
        
        Log.d(TAG, "query: $uri, count: ${cursor?.count}")
        return cursor
    }
    
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        if (values == null) {
            throw IllegalArgumentException("ContentValues cannot be null")
        }
        
        val db = dbHelper.writableDatabase
        
        val rowId = when (uriMatcher.match(uri)) {
            NOTES -> {
                // 自动添加时间戳
                if (!values.containsKey("created_at")) {
                    values.put("created_at", System.currentTimeMillis() / 1000)
                }
                if (!values.containsKey("updated_at")) {
                    values.put("updated_at", System.currentTimeMillis() / 1000)
                }
                
                db.insert(TABLE_NOTES, null, values)
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
        
        if (rowId > 0) {
            val noteUri = ContentUris.withAppendedId(CONTENT_URI, rowId)
            // 通知数据变化
            context?.contentResolver?.notifyChange(noteUri, null)
            Log.d(TAG, "insert: $noteUri")
            return noteUri
        }
        
        throw SQLException("Failed to insert row into $uri")
    }
    
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        val db = dbHelper.writableDatabase
        
        val count = when (uriMatcher.match(uri)) {
            NOTES -> {
                // 删除多条记录
                db.delete(TABLE_NOTES, selection, selectionArgs)
            }
            NOTE_ID -> {
                // 删除单条记录
                val id = uri.lastPathSegment
                db.delete(TABLE_NOTES, "_id=?", arrayOf(id))
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
        
        if (count > 0) {
            // 通知数据变化
            context?.contentResolver?.notifyChange(uri, null)
            Log.d(TAG, "delete: $uri, count: $count")
        }
        
        return count
    }
    
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        if (values == null || values.size() == 0) {
            throw IllegalArgumentException("ContentValues cannot be empty")
        }
        
        val db = dbHelper.writableDatabase
        
        // 自动更新时间戳
        if (!values.containsKey("updated_at")) {
            values.put("updated_at", System.currentTimeMillis() / 1000)
        }
        
        val count = when (uriMatcher.match(uri)) {
            NOTES -> {
                // 更新多条记录
                db.update(TABLE_NOTES, values, selection, selectionArgs)
            }
            NOTE_ID -> {
                // 更新单条记录
                val id = uri.lastPathSegment
                db.update(TABLE_NOTES, values, "_id=?", arrayOf(id))
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
        
        if (count > 0) {
            // 通知数据变化
            context?.contentResolver?.notifyChange(uri, null)
            Log.d(TAG, "update: $uri, count: $count")
        }
        
        return count
    }
    
    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            NOTES -> "vnd.android.cursor.dir/vnd.com.example.notes"
            NOTE_ID -> "vnd.android.cursor.item/vnd.com.example.notes"
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
    }
}

4.3 在 Manifest 中注册

xml 复制代码
<!-- AndroidManifest.xml -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
    <!-- 定义自定义权限 -->
    <permission
        android:name="com.example.contentproviderdemo.READ_NOTES"
        android:protectionLevel="normal"
        android:label="读取笔记权限"
        android:description="@string/read_notes_permission_desc" />
    
    <permission
        android:name="com.example.contentproviderdemo.WRITE_NOTES"
        android:protectionLevel="normal"
        android:label="写入笔记权限"
        android:description="@string/write_notes_permission_desc" />
    
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.AppCompat">
        
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <!-- 注册 ContentProvider -->
        <provider
            android:name=".NoteProvider"
            android:authorities="com.example.contentproviderdemo"
            android:exported="true"
            android:readPermission="com.example.contentproviderdemo.READ_NOTES"
            android:writePermission="com.example.contentproviderdemo.WRITE_NOTES" />
        
    </application>
    
</manifest>

关键属性说明:

属性 说明
android:name ContentProvider 类名
android:authorities 唯一标识符,用于 URI
android:exported 是否允许其他应用访问
android:readPermission 读权限
android:writePermission 写权限
android:grantUriPermissions 是否允许临时授权

4.4 客户端调用示例

kotlin 复制代码
// MainActivity.kt
package com.example.contentproviderdemo

import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.example.contentproviderdemo.databinding.ActivityMainBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    private val noteUri = Uri.parse("content://com.example.contentproviderdemo/notes")
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        binding.btnInsert.setOnClickListener {
            insertNote()
        }
        
        binding.btnQuery.setOnClickListener {
            queryNotes()
        }
        
        binding.btnUpdate.setOnClickListener {
            updateNote()
        }
        
        binding.btnDelete.setOnClickListener {
            deleteNote()
        }
        
        binding.btnBatchInsert.setOnClickListener {
            batchInsertNotes()
        }
    }
    
    // 插入笔记
    private fun insertNote() {
        lifecycleScope.launch(Dispatchers.IO) {
            val values = ContentValues().apply {
                put("title", "第一次使用 ContentProvider")
                put("content", "真没想到原理这么清晰")
            }
            
            val uri = contentResolver.insert(noteUri, values)
            
            withContext(Dispatchers.Main) {
                Log.d("MainActivity", "插入成功:$uri")
                binding.tvResult.text = "插入成功:$uri"
            }
        }
    }
    
    // 查询所有笔记
    private fun queryNotes() {
        lifecycleScope.launch(Dispatchers.IO) {
            val cursor = contentResolver.query(
                noteUri,
                arrayOf("_id", "title", "content", "created_at"),
                null,
                null,
                "created_at DESC"
            )
            
            val notes = mutableListOf<String>()
            cursor?.use {
                while (it.moveToNext()) {
                    val id = it.getLong(it.getColumnIndexOrThrow("_id"))
                    val title = it.getString(it.getColumnIndexOrThrow("title"))
                    val content = it.getString(it.getColumnIndexOrThrow("content"))
                    val createdAt = it.getLong(it.getColumnIndexOrThrow("created_at"))
                    
                    notes.add("[$id] $title\n$content\n创建时间:$createdAt")
                    Log.d("MainActivity", "笔记 $id: $title - $content")
                }
            }
            
            withContext(Dispatchers.Main) {
                binding.tvResult.text = if (notes.isEmpty()) {
                    "暂无笔记"
                } else {
                    notes.joinToString("\n\n")
                }
            }
        }
    }
    
    // 更新笔记
    private fun updateNote() {
        lifecycleScope.launch(Dispatchers.IO) {
            val values = ContentValues().apply {
                put("title", "更新后的标题")
                put("content", "更新后的内容")
            }
            
            // 更新 ID 为 1 的笔记
            val count = contentResolver.update(
                Uri.withAppendedPath(noteUri, "1"),
                values,
                null,
                null
            )
            
            withContext(Dispatchers.Main) {
                Log.d("MainActivity", "更新了 $count 条记录")
                binding.tvResult.text = "更新了 $count 条记录"
            }
        }
    }
    
    // 删除笔记
    private fun deleteNote() {
        lifecycleScope.launch(Dispatchers.IO) {
            // 删除 ID 为 1 的笔记
            val count = contentResolver.delete(
                Uri.withAppendedPath(noteUri, "1"),
                null,
                null
            )
            
            withContext(Dispatchers.Main) {
                Log.d("MainActivity", "删除了 $count 条记录")
                binding.tvResult.text = "删除了 $count 条记录"
            }
        }
    }
    
    // 批量插入(性能优化)
    private fun batchInsertNotes() {
        lifecycleScope.launch(Dispatchers.IO) {
            val startTime = System.currentTimeMillis()
            
            val operations = ArrayList<android.content.ContentProviderOperation>()
            
            for (i in 1..100) {
                val values = ContentValues().apply {
                    put("title", "批量笔记 $i")
                    put("content", "这是第 $i 条批量插入的笔记")
                }
                
                operations.add(
                    android.content.ContentProviderOperation.newInsert(noteUri)
                        .withValues(values)
                        .build()
                )
            }
            
            try {
                contentResolver.applyBatch(
                    "com.example.contentproviderdemo",
                    operations
                )
                
                val endTime = System.currentTimeMillis()
                val duration = endTime - startTime
                
                withContext(Dispatchers.Main) {
                    Log.d("MainActivity", "批量插入 100 条,耗时:${duration}ms")
                    binding.tvResult.text = "批量插入 100 条成功\n耗时:${duration}ms"
                }
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    binding.tvResult.text = "批量插入失败:${e.message}"
                }
            }
        }
    }
}

4.5 布局文件

xml 复制代码
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">
    
    <Button
        android:id="@+id/btnInsert"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="插入笔记" />
    
    <Button
        android:id="@+id/btnQuery"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="查询所有笔记" />
    
    <Button
        android:id="@+id/btnUpdate"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="更新笔记(ID=1)" />
    
    <Button
        android:id="@+id/btnDelete"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="删除笔记(ID=1)" />
    
    <Button
        android:id="@+id/btnBatchInsert"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="批量插入 100 条" />
    
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:layout_marginTop="16dp">
        
        <TextView
            android:id="@+id/tvResult"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="操作结果将显示在这里"
            android:textSize="14sp" />
        
    </ScrollView>
    
</LinearLayout>

五、数据变化监听(ContentObserver)

ContentProvider 的一大特性是支持数据变化监听,当数据发生变化时,会自动通知所有注册的观察者。

5.1 注册 ContentObserver

kotlin 复制代码
class MainActivity : AppCompatActivity() {
    
    private val noteUri = Uri.parse("content://com.example.contentproviderdemo/notes")
    
    // 创建观察者
    private val noteObserver = object : android.database.ContentObserver(
        android.os.Handler(android.os.Looper.getMainLooper())
    ) {
        override fun onChange(selfChange: Boolean, uri: Uri?) {
            super.onChange(selfChange, uri)
            Log.d("ContentObserver", "数据发生变化:$uri")
            
            // 重新加载数据
            loadNotes()
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 注册观察者
        contentResolver.registerContentObserver(
            noteUri,
            true,  // notifyForDescendants: 是否监听子 URI
            noteObserver
        )
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // 取消注册
        contentResolver.unregisterContentObserver(noteObserver)
    }
    
    private fun loadNotes() {
        lifecycleScope.launch(Dispatchers.IO) {
            val cursor = contentResolver.query(noteUri, null, null, null, null)
            
            val notes = mutableListOf<String>()
            cursor?.use {
                while (it.moveToNext()) {
                    val title = it.getString(it.getColumnIndexOrThrow("title"))
                    notes.add(title)
                }
            }
            
            withContext(Dispatchers.Main) {
                Log.d("Notes", "当前笔记:$notes")
            }
        }
    }
}

5.2 工作原理

kotlin 复制代码
1. 注册观察者
   contentResolver.registerContentObserver(uri, observer)
   
2. 数据变化时通知
   contentResolver.notifyChange(uri, null)
   
3. 观察者收到通知
   observer.onChange(uri)
   
4. 重新查询数据
   loadData()

六、权限控制

6.1 权限类型

权限类型 说明
readPermission 控制读操作(query)
writePermission 控制写操作(insert/update/delete)
permission 同时控制读写
grantUriPermissions 临时授权

6.2 定义自定义权限

xml 复制代码
<!-- AndroidManifest.xml -->
<manifest>
    
    <!-- 定义读权限 -->
    <permission
        android:name="com.example.contentproviderdemo.READ_NOTES"
        android:protectionLevel="normal"
        android:label="读取笔记权限"
        android:description="@string/read_notes_permission_desc" />
    
    <!-- 定义写权限 -->
    <permission
        android:name="com.example.contentproviderdemo.WRITE_NOTES"
        android:protectionLevel="dangerous"
        android:label="写入笔记权限"
        android:description="@string/write_notes_permission_desc" />
    
    <application>
        <provider
            android:name=".NoteProvider"
            android:authorities="com.example.contentproviderdemo"
            android:exported="true"
            android:readPermission="com.example.contentproviderdemo.READ_NOTES"
            android:writePermission="com.example.contentproviderdemo.WRITE_NOTES" />
    </application>
    
</manifest>

protectionLevel 说明:

级别 说明
normal 系统自动授予,不需要用户确认
dangerous 需要用户手动授予(运行时权限)
signature 只有相同签名的应用才能获得
signatureOrSystem 系统应用或相同签名的应用

6.3 其他应用申请权限

静态声明:

xml 复制代码
<!-- 其他应用的 Manifest -->
<manifest>
    <uses-permission android:name="com.example.contentproviderdemo.READ_NOTES" />
    <uses-permission android:name="com.example.contentproviderdemo.WRITE_NOTES" />
</manifest>

运行时申请(如果是 dangerous 级别):

kotlin 复制代码
class ClientActivity : AppCompatActivity() {
    
    companion object {
        private const val REQUEST_CODE = 100
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 检查权限
        if (ContextCompat.checkSelfPermission(
                this,
                "com.example.contentproviderdemo.READ_NOTES"
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            // 请求权限
            ActivityCompat.requestPermissions(
                this,
                arrayOf(
                    "com.example.contentproviderdemo.READ_NOTES",
                    "com.example.contentproviderdemo.WRITE_NOTES"
                ),
                REQUEST_CODE
            )
        } else {
            // 已有权限,直接访问
            accessNotes()
        }
    }
    
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        
        if (requestCode == REQUEST_CODE) {
            if (grantResults.isNotEmpty() && 
                grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 权限已授予
                accessNotes()
            } else {
                // 权限被拒绝
                Toast.makeText(this, "权限被拒绝", Toast.LENGTH_SHORT).show()
            }
        }
    }
    
    private fun accessNotes() {
        val uri = Uri.parse("content://com.example.contentproviderdemo/notes")
        val cursor = contentResolver.query(uri, null, null, null, null)
        // 处理数据...
        cursor?.close()
    }
}

6.4 临时授权(Grant URI Permissions)

有时我们希望临时授予某个应用访问特定 URI 的权限,而不是永久授权。

配置 Provider:

xml 复制代码
<provider
    android:name=".NoteProvider"
    android:authorities="com.example.contentproviderdemo"
    android:exported="true"
    android:grantUriPermissions="true">
    
    <!-- 只允许临时授权特定路径 -->
    <grant-uri-permission android:pathPattern="/notes/.*" />
</provider>

授予临时权限:

kotlin 复制代码
// 发送方
val intent = Intent(Intent.ACTION_VIEW).apply {
    data = Uri.parse("content://com.example.contentproviderdemo/notes/1")
    // 授予临时读写权限
    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or 
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
}
startActivity(intent)

接收方:

kotlin 复制代码
// 接收方可以直接访问,无需声明权限
val uri = intent.data
val cursor = contentResolver.query(uri, null, null, null, null)

七、性能优化

7.1 批量操作

单条插入和批量插入的性能差异巨大。

错误示例(逐条插入):

kotlin 复制代码
// 插入 100 条数据,耗时约 500ms
for (i in 1..100) {
    val values = ContentValues().apply {
        put("title", "笔记 $i")
        put("content", "内容 $i")
    }
    contentResolver.insert(noteUri, values)
}

正确示例(批量插入):

kotlin 复制代码
// 插入 100 条数据,耗时约 50ms
val operations = ArrayList<ContentProviderOperation>()

for (i in 1..100) {
    val values = ContentValues().apply {
        put("title", "笔记 $i")
        put("content", "内容 $i")
    }
    
    operations.add(
        ContentProviderOperation.newInsert(noteUri)
            .withValues(values)
            .build()
    )
}

contentResolver.applyBatch("com.example.contentproviderdemo", operations)

性能对比:

复制代码
逐条插入 100 条:~500ms
批量插入 100 条:~50ms
性能提升:10 倍!

7.2 异步查询

永远不要在主线程执行查询操作。

错误示例:

kotlin 复制代码
// 主线程查询 - 会导致 ANR!
val cursor = contentResolver.query(noteUri, null, null, null, null)

正确示例(使用协程):

kotlin 复制代码
lifecycleScope.launch(Dispatchers.IO) {
    val cursor = contentResolver.query(noteUri, null, null, null, null)
    
    val notes = mutableListOf<String>()
    cursor?.use {
        while (it.moveToNext()) {
            val title = it.getString(it.getColumnIndexOrThrow("title"))
            notes.add(title)
        }
    }
    
    withContext(Dispatchers.Main) {
        updateUI(notes)
    }
}

或使用 Flow(推荐):

kotlin 复制代码
fun queryNotesFlow(): Flow<List<Note>> = flow {
    val cursor = contentResolver.query(noteUri, null, null, null, null)
    
    val notes = mutableListOf<Note>()
    cursor?.use {
        while (it.moveToNext()) {
            notes.add(Note(
                id = it.getLong(it.getColumnIndexOrThrow("_id")),
                title = it.getString(it.getColumnIndexOrThrow("title")),
                content = it.getString(it.getColumnIndexOrThrow("content"))
            ))
        }
    }
    
    emit(notes)
}.flowOn(Dispatchers.IO)

// 使用
lifecycleScope.launch {
    queryNotesFlow().collect { notes ->
        updateUI(notes)
    }
}

7.3 索引优化

在数据库创建时添加索引可以大幅提升查询性能。

kotlin 复制代码
override fun onCreate(db: SQLiteDatabase) {
    // 创建表
    db.execSQL("""
        CREATE TABLE notes (
            _id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            content TEXT,
            created_at INTEGER,
            updated_at INTEGER
        )
    """)
    
    // 为常用查询字段创建索引
    db.execSQL("CREATE INDEX idx_title ON notes(title)")
    db.execSQL("CREATE INDEX idx_created_at ON notes(created_at)")
    
    // 组合索引(如果经常同时查询这两个字段)
    db.execSQL("CREATE INDEX idx_title_created ON notes(title, created_at)")
}

索引的作用:

yaml 复制代码
无索引查询 1000 条数据:~200ms
有索引查询 1000 条数据:~20ms
性能提升:10 倍!

7.4 Cursor 使用优化

kotlin 复制代码
// 错误:忘记关闭 Cursor 导致内存泄漏
val cursor = contentResolver.query(uri, null, null, null, null)
while (cursor?.moveToNext() == true) {
    // 处理数据
}
// 忘记关闭!

// 正确:使用 use 自动关闭
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.use {
    while (it.moveToNext()) {
        // 处理数据
    }
} // 自动关闭

// 更好:提前获取列索引,避免重复查询
cursor?.use {
    val idIndex = it.getColumnIndexOrThrow("_id")
    val titleIndex = it.getColumnIndexOrThrow("title")
    val contentIndex = it.getColumnIndexOrThrow("content")
    
    while (it.moveToNext()) {
        val id = it.getLong(idIndex)
        val title = it.getString(titleIndex)
        val content = it.getString(contentIndex)
        // 处理数据
    }
}

八、实战场景

8.1 跨应用共享文件

使用 FileProvider 共享文件给其他应用。

kotlin 复制代码
class MyFileProvider : ContentProvider() {
    
    companion object {
        private const val AUTHORITY = "com.example.fileprovider"
        private const val FILES = 1
        private const val FILE_ID = 2
        
        private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(AUTHORITY, "files", FILES)
            addURI(AUTHORITY, "files/*", FILE_ID)
        }
    }
    
    override fun onCreate(): Boolean = true
    
    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
        val file = when (uriMatcher.match(uri)) {
            FILE_ID -> {
                val fileName = uri.lastPathSegment ?: throw IllegalArgumentException()
                File(context?.filesDir, fileName)
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
        
        if (!file.exists()) {
            throw FileNotFoundException("File not found: ${file.path}")
        }
        
        val accessMode = when (mode) {
            "r" -> ParcelFileDescriptor.MODE_READ_ONLY
            "w" -> ParcelFileDescriptor.MODE_WRITE_ONLY
            "rw" -> ParcelFileDescriptor.MODE_READ_WRITE
            else -> throw IllegalArgumentException("Unsupported mode: $mode")
        }
        
        return ParcelFileDescriptor.open(file, accessMode)
    }
    
    override fun query(uri: Uri, projection: Array<out String>?, 
                      selection: String?, selectionArgs: Array<out String>?, 
                      sortOrder: String?): Cursor? {
        // 返回文件元数据
        val file = when (uriMatcher.match(uri)) {
            FILE_ID -> {
                val fileName = uri.lastPathSegment ?: return null
                File(context?.filesDir, fileName)
            }
            else -> return null
        }
        
        val matrixCursor = MatrixCursor(arrayOf(
            OpenableColumns.DISPLAY_NAME,
            OpenableColumns.SIZE
        ))
        
        matrixCursor.addRow(arrayOf(
            file.name,
            file.length()
        ))
        
        return matrixCursor
    }
    
    override fun insert(uri: Uri, values: ContentValues?): Uri? = null
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
    override fun update(uri: Uri, values: ContentValues?, selection: String?, 
                       selectionArgs: Array<out String>?): Int = 0
    override fun getType(uri: Uri): String? = null
}

使用示例:

kotlin 复制代码
// 分享文件给其他应用
val file = File(filesDir, "photo.jpg")
val uri = Uri.parse("content://com.example.fileprovider/files/photo.jpg")

val intent = Intent(Intent.ACTION_SEND).apply {
    type = "image/*"
    putExtra(Intent.EXTRA_STREAM, uri)
    flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
startActivity(Intent.createChooser(intent, "分享图片"))

// 其他应用读取文件
val inputStream = contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close()

8.2 搜索建议集成(SearchView)

为 SearchView 提供搜索建议。

kotlin 复制代码
class SearchSuggestionProvider : ContentProvider() {
    
    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        val query = uri.lastPathSegment ?: return null
        
        // 创建结果 Cursor
        val cursor = MatrixCursor(arrayOf(
            BaseColumns._ID,
            SearchManager.SUGGEST_COLUMN_TEXT_1,
            SearchManager.SUGGEST_COLUMN_TEXT_2,
            SearchManager.SUGGEST_COLUMN_INTENT_DATA
        ))
        
        // 查询数据库
        val db = dbHelper.readableDatabase
        val results = db.query(
            "notes",
            arrayOf("_id", "title", "content"),
            "title LIKE ?",
            arrayOf("%$query%"),
            null, null,
            "created_at DESC",
            "10"  // 限制 10 条
        )
        
        results.use {
            while (it.moveToNext()) {
                val id = it.getLong(0)
                val title = it.getString(1)
                val content = it.getString(2)
                
                cursor.addRow(arrayOf(
                    id,
                    title,
                    content.take(50),  // 截取前 50 个字符作为描述
                    "content://com.example.contentproviderdemo/notes/$id"
                ))
            }
        }
        
        return cursor
    }
    
    // 其他方法...
}

在 Manifest 中配置:

xml 复制代码
<provider
    android:name=".SearchSuggestionProvider"
    android:authorities="com.example.search.suggestion"
    android:exported="false" />

<activity android:name=".SearchActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEARCH" />
    </intent-filter>
    
    <meta-data
        android:name="android.app.searchable"
        android:resource="@xml/searchable" />
</activity>

searchable.xml:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_name"
    android:hint="搜索笔记"
    android:searchSuggestAuthority="com.example.search.suggestion"
    android:searchSuggestIntentAction="android.intent.action.VIEW"
    android:searchSuggestSelection=" ?" />

8.3 监听系统媒体库变化

kotlin 复制代码
class MediaObserverActivity : AppCompatActivity() {
    
    private val mediaObserver = object : ContentObserver(
        Handler(Looper.getMainLooper())
    ) {
        override fun onChange(selfChange: Boolean, uri: Uri?) {
            super.onChange(selfChange, uri)
            Log.d("MediaObserver", "媒体库发生变化:$uri")
            // 重新加载图片列表
            loadImages()
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 监听图片库
        contentResolver.registerContentObserver(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            true,
            mediaObserver
        )
        
        loadImages()
    }
    
    override fun onDestroy() {
        super.onDestroy()
        contentResolver.unregisterContentObserver(mediaObserver)
    }
    
    private fun loadImages() {
        lifecycleScope.launch(Dispatchers.IO) {
            val projection = arrayOf(
                MediaStore.Images.Media._ID,
                MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.DATE_ADDED
            )
            
            val cursor = contentResolver.query(
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                projection,
                null,
                null,
                "${MediaStore.Images.Media.DATE_ADDED} DESC"
            )
            
            val images = mutableListOf<String>()
            cursor?.use {
                val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
                val nameColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
                
                while (it.moveToNext()) {
                    val id = it.getLong(idColumn)
                    val name = it.getString(nameColumn)
                    val contentUri = ContentUris.withAppendedId(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                        id
                    )
                    images.add("$name: $contentUri")
                }
            }
            
            withContext(Dispatchers.Main) {
                Log.d("Images", "加载了 ${images.size} 张图片")
            }
        }
    }
}

九、安全最佳实践

9.1 防止 SQL 注入

危险示例:

kotlin 复制代码
// 永远不要这样做!
override fun query(...): Cursor? {
    val sql = "SELECT * FROM notes WHERE title='$selection'"
    return db.rawQuery(sql, null)
}

// 攻击者可以这样注入:
// selection = "' OR '1'='1"
// 最终 SQL: SELECT * FROM notes WHERE title='' OR '1'='1'
// 结果:返回所有数据!

安全示例:

kotlin 复制代码
// 使用参数化查询
override fun query(...): Cursor? {
    return db.query(
        "notes",
        projection,
        "title=?",  // 使用占位符
        arrayOf(selection),  // 参数数组,自动转义
        null, null, sortOrder
    )
}

9.2 控制 exported 属性

xml 复制代码
<!-- 内部使用,不暴露给其他应用 -->
<provider
    android:name=".InternalProvider"
    android:authorities="com.example.internal"
    android:exported="false" />

<!-- 需要暴露给其他应用,添加权限控制 -->
<provider
    android:name=".PublicProvider"
    android:authorities="com.example.public"
    android:exported="true"
    android:permission="com.example.CUSTOM_PERMISSION" />

9.3 数据加密

对敏感数据进行加密存储。

kotlin 复制代码
class EncryptedNoteProvider : ContentProvider() {
    
    private lateinit var cipher: Cipher
    private lateinit var secretKey: SecretKey
    
    override fun onCreate(): Boolean {
        // 初始化加密密钥
        val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
            load(null)
        }
        
        if (!keyStore.containsAlias("note_key")) {
            val keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES,
                "AndroidKeyStore"
            )
            keyGenerator.init(
                KeyGenParameterSpec.Builder(
                    "note_key",
                    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
                )
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .build()
            )
            secretKey = keyGenerator.generateKey()
        } else {
            secretKey = keyStore.getKey("note_key", null) as SecretKey
        }
        
        cipher = Cipher.getInstance("AES/GCM/NoPadding")
        
        return true
    }
    
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        if (values == null) return null
        
        // 加密敏感字段
        val content = values.getAsString("content")
        if (content != null) {
            val encryptedContent = encrypt(content)
            values.put("content", encryptedContent)
        }
        
        // 插入数据库
        val db = dbHelper.writableDatabase
        val rowId = db.insert("notes", null, values)
        
        return if (rowId > 0) {
            ContentUris.withAppendedId(CONTENT_URI, rowId)
        } else {
            null
        }
    }
    
    override fun query(...): Cursor? {
        val cursor = db.query(...)
        
        // 解密数据(这里简化了,实际需要包装 Cursor)
        return cursor
    }
    
    private fun encrypt(data: String): String {
        cipher.init(Cipher.ENCRYPT_MODE, secretKey)
        val encryptedBytes = cipher.doFinal(data.toByteArray())
        val iv = cipher.iv
        
        // 将 IV 和加密数据拼接
        val combined = iv + encryptedBytes
        return Base64.encodeToString(combined, Base64.DEFAULT)
    }
    
    private fun decrypt(encryptedData: String): String {
        val combined = Base64.decode(encryptedData, Base64.DEFAULT)
        
        // 提取 IV
        val iv = combined.sliceArray(0 until 12)
        val encryptedBytes = combined.sliceArray(12 until combined.size)
        
        val spec = GCMParameterSpec(128, iv)
        cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
        
        val decryptedBytes = cipher.doFinal(encryptedBytes)
        return String(decryptedBytes)
    }
}

十、调试技巧

10.1 使用 adb 命令测试

查询数据:

css 复制代码
adb shell content query --uri content://com.example.contentproviderdemo/notes

插入数据:

bash 复制代码
adb shell content insert \
  --uri content://com.example.contentproviderdemo/notes \
  --bind title:s:"测试标题" \
  --bind content:s:"测试内容"

更新数据:

bash 复制代码
adb shell content update \
  --uri content://com.example.contentproviderdemo/notes \
  --where "_id=1" \
  --bind title:s:"新标题"

删除数据:

css 复制代码
adb shell content delete \
  --uri content://com.example.contentproviderdemo/notes \
  --where "_id=1"

10.2 查看数据库文件

shell 复制代码
# 进入设备 shell
adb shell

# 切换到应用目录
run-as com.example.contentproviderdemo

# 查看数据库
cd databases
ls -l

# 使用 sqlite3 查看
sqlite3 Notes.db
sqlite> .tables
sqlite> SELECT * FROM notes;
sqlite> .quit

10.3 使用 Android Studio Database Inspector

  1. 运行应用
  2. 打开 View → Tool Windows → App Inspection
  3. 选择 Database Inspector
  4. 可以实时查看数据库内容、执行 SQL 查询

10.4 日志调试

在 ContentProvider 中添加详细日志:

kotlin 复制代码
override fun query(...): Cursor? {
    Log.d(TAG, "query called")
    Log.d(TAG, "uri: $uri")
    Log.d(TAG, "projection: ${projection?.joinToString()}")
    Log.d(TAG, "selection: $selection")
    Log.d(TAG, "selectionArgs: ${selectionArgs?.joinToString()}")
    Log.d(TAG, "sortOrder: $sortOrder")
    
    val cursor = db.query(...)
    Log.d(TAG, "query result count: ${cursor?.count}")
    
    return cursor
}

十一、常见问题与避坑

问题 1:URI 不匹配

症状:

arduino 复制代码
java.lang.IllegalArgumentException: Unknown URI: content://...

原因:

  • URI 拼写错误
  • authority 不匹配
  • 没有在 UriMatcher 中添加匹配规则

解决:

kotlin 复制代码
// 检查 authority 是否一致
val AUTHORITY = "com.example.contentproviderdemo"

// Manifest
<provider android:authorities="com.example.contentproviderdemo" />

// UriMatcher
addURI("com.example.contentproviderdemo", "notes", NOTES)

// 调用
val uri = Uri.parse("content://com.example.contentproviderdemo/notes")

问题 2:权限被拒绝

症状:

kotlin 复制代码
java.lang.SecurityException: Permission Denial

原因:

  • 没有声明权限
  • 没有申请运行时权限(dangerous 级别)
  • Provider 的 exported=false

解决:

xml 复制代码
<!-- 客户端声明权限 -->
<uses-permission android:name="com.example.contentproviderdemo.READ_NOTES" />

<!-- 如果是 dangerous 级别,还需要运行时申请 -->
kotlin 复制代码
if (ContextCompat.checkSelfPermission(...) != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(...)
}

问题 3:Cursor 未关闭导致内存泄漏

错误:

kotlin 复制代码
val cursor = contentResolver.query(uri, null, null, null, null)
while (cursor?.moveToNext() == true) {
    // 处理数据
}
// 忘记关闭!

正确:

kotlin 复制代码
val cursor = contentResolver.query(uri, null, null, null, null)
cursor?.use {
    while (it.moveToNext()) {
        // 处理数据
    }
} // 自动关闭

问题 4:主线程查询导致 ANR

错误:

kotlin 复制代码
// 主线程查询大量数据
val cursor = contentResolver.query(uri, null, null, null, null)

正确:

kotlin 复制代码
lifecycleScope.launch(Dispatchers.IO) {
    val cursor = contentResolver.query(uri, null, null, null, null)
    // 处理数据...
}

问题 5:onCreate() 执行耗时操作

错误:

kotlin 复制代码
override fun onCreate(): Boolean {
    // 初始化数据库
    dbHelper = NoteDatabaseHelper(context!!)
    
    // 错误:执行耗时操作
    Thread.sleep(5000)  // 阻塞应用启动!
    
    return true
}

正确:

kotlin 复制代码
override fun onCreate(): Boolean {
    // 只做必要的初始化
    dbHelper = NoteDatabaseHelper(context!!)
    
    // 耗时操作放到后台
    Thread {
        // 预加载数据等操作
    }.start()
    
    return true
}

问题 6:多线程并发问题

问题: ContentProvider 的方法可能在多线程并发调用,如果没有做好同步,可能导致数据不一致。

解决:

kotlin 复制代码
class NoteProvider : ContentProvider() {
    
    // 方案一:使用 synchronized
    @Synchronized
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        // 插入逻辑
    }
    
    // 方案二:SQLiteDatabase 本身是线程安全的
    // 只要使用同一个 SQLiteOpenHelper 实例即可
    private lateinit var dbHelper: NoteDatabaseHelper
    
    override fun onCreate(): Boolean {
        dbHelper = NoteDatabaseHelper(context!!)  // 单例
        return true
    }
}

十二、ContentProvider vs 其他方案

场景对比

场景 1:同一应用内的数据访问

diff 复制代码
推荐:Room + Repository
不推荐:ContentProvider

原因:
- Room 提供了更好的类型安全
- 编译时检查 SQL 语法
- 支持 LiveData/Flow 观察数据变化
- 没有跨进程开销

场景 2:跨应用数据共享

diff 复制代码
推荐:ContentProvider
备选:AIDL(如果需要复杂的接口)

原因:
- 标准化的 CRUD 接口
- 系统级权限控制
- 数据变化监听
- URI 定位灵活

场景 3:简单的配置共享

diff 复制代码
推荐:SharedPreferences
不推荐:ContentProvider

原因:
- SharedPreferences 更简单
- 自动持久化
- 适合小量键值对

场景 4:大文件传输

diff 复制代码
推荐:FileProvider(ContentProvider 的子类)
备选:直接文件共享 + 权限管理

原因:
- 支持流式读写
- 自动处理权限
- 与 Intent 无缝集成

性能对比

操作 ContentProvider Room SharedPreferences
单条查询 ~10ms ~5ms ~1ms
批量查询(100条) ~50ms ~30ms N/A
单条插入 ~15ms ~8ms ~2ms
批量插入(100条) ~80ms ~50ms N/A
跨进程开销

十三、总结

核心要点回顾

ContentProvider 的本质:

一个为外部世界暴露数据的标准化接口,基于 Binder 实现跨进程通信。

六个核心方法:

方法 作用
onCreate() 初始化 Provider
query() 查询数据
insert() 插入数据
update() 更新数据
delete() 删除数据
getType() 返回 MIME 类型

三大核心组件:

组件 说明
ContentProvider 数据提供者
ContentResolver 数据访问者
URI 数据定位符

使用场景:

✅ 跨应用数据共享

✅ 系统级数据访问(通讯录、媒体库)

✅ 需要细粒度权限控制

✅ 需要数据变化监听

❌ 同应用内数据访问 → 用 Room

❌ 简单配置共享 → 用 SharedPreferences

❌ 复杂业务逻辑 → 用 AIDL

最佳实践清单

  • 使用 UriMatcher 匹配 URI

  • 正确实现 getType() 返回 MIME 类型

  • 在数据变化时调用 notifyChange()

  • 使用 ContentObserver 监听数据变化

  • 添加权限控制(readPermission/writePermission)

  • 使用参数化查询防止 SQL 注入

  • 批量操作使用 applyBatch()

  • 异步执行查询操作(协程/Flow)

  • 正确关闭 Cursor(使用 use)

  • onCreate() 中避免耗时操作

  • 为常用查询字段创建索引

  • 敏感数据加密存储

  • 正确设置 exported 属性

架构演进

Android 四大组件已完结:

  1. Activity - 用户界面
  2. Service - 后台任务
  3. BroadcastReceiver - 消息通知
  4. ContentProvider - 数据共享

现代 Android 开发趋势:

虽然四大组件是 Android 的基础,但现代应用开发正在向更高层次的架构演进:

markdown 复制代码
传统方式:
Activity → ContentProvider → SQLite

现代方式:
Jetpack Compose → ViewModel → Repository → Room
                                    ↓
                           可选:ContentProvider(仅用于跨应用)

什么时候还需要 ContentProvider?

  1. 系统集成:需要被系统或其他应用访问(如输入法、壁纸、同步适配器)
  2. 数据共享:明确需要向第三方应用暴露数据
  3. 权限控制:需要细粒度的读写权限分离
  4. 标准接口:需要符合 Android 标准的 CRUD 接口

什么时候不需要?

  1. 应用内部:单应用的数据访问,直接用 Room
  2. 简单配置:键值对存储,用 SharedPreferences/DataStore
  3. 网络数据:API 调用,用 Retrofit + Repository
  4. 实时通信:应用内事件,用 Flow/LiveData

十四、完整示例代码总结

项目结构

md 复制代码
app/src/main/
├── java/com/example/contentproviderdemo/
│   ├── MainActivity.kt                    # 主界面
│   ├── NoteProvider.kt                    # ContentProvider 实现
│   ├── NoteDatabaseHelper.kt              # 数据库帮助类
│   ├── Note.kt                            # 数据模型
│   └── NoteRepository.kt                  # 可选:Repository 层
├── res/
│   ├── layout/
│   │   └── activity_main.xml              # 主界面布局
│   ├── values/
│   │   └── strings.xml                    # 字符串资源
│   └── xml/
│       └── searchable.xml                 # 可选:搜索配置
└── AndroidManifest.xml                     # 应用配置

Note.kt(数据模型)

kotlin 复制代码
package com.example.contentproviderdemo

data class Note(
    val id: Long = 0,
    val title: String,
    val content: String,
    val createdAt: Long = System.currentTimeMillis() / 1000,
    val updatedAt: Long = System.currentTimeMillis() / 1000
)

NoteRepository.kt(可选,推荐用于应用内访问)

kotlin 复制代码
package com.example.contentproviderdemo

import android.content.ContentValues
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn

class NoteRepository(private val context: Context) {
    
    private val contentUri = Uri.parse("content://com.example.contentproviderdemo/notes")
    
    // 查询所有笔记(返回 Flow)
    fun getAllNotes(): Flow<List<Note>> = flow {
        val cursor = context.contentResolver.query(
            contentUri,
            arrayOf("_id", "title", "content", "created_at", "updated_at"),
            null,
            null,
            "created_at DESC"
        )
        
        val notes = mutableListOf<Note>()
        cursor?.use {
            val idIndex = it.getColumnIndexOrThrow("_id")
            val titleIndex = it.getColumnIndexOrThrow("title")
            val contentIndex = it.getColumnIndexOrThrow("content")
            val createdAtIndex = it.getColumnIndexOrThrow("created_at")
            val updatedAtIndex = it.getColumnIndexOrThrow("updated_at")
            
            while (it.moveToNext()) {
                notes.add(Note(
                    id = it.getLong(idIndex),
                    title = it.getString(titleIndex),
                    content = it.getString(contentIndex),
                    createdAt = it.getLong(createdAtIndex),
                    updatedAt = it.getLong(updatedAtIndex)
                ))
            }
        }
        
        emit(notes)
    }.flowOn(Dispatchers.IO)
    
    // 根据 ID 查询笔记
    suspend fun getNoteById(id: Long): Note? {
        val uri = Uri.withAppendedPath(contentUri, id.toString())
        val cursor = context.contentResolver.query(
            uri,
            arrayOf("_id", "title", "content", "created_at", "updated_at"),
            null,
            null,
            null
        )
        
        return cursor?.use {
            if (it.moveToFirst()) {
                Note(
                    id = it.getLong(it.getColumnIndexOrThrow("_id")),
                    title = it.getString(it.getColumnIndexOrThrow("title")),
                    content = it.getString(it.getColumnIndexOrThrow("content")),
                    createdAt = it.getLong(it.getColumnIndexOrThrow("created_at")),
                    updatedAt = it.getLong(it.getColumnIndexOrThrow("updated_at"))
                )
            } else {
                null
            }
        }
    }
    
    // 插入笔记
    suspend fun insertNote(note: Note): Uri? {
        val values = ContentValues().apply {
            put("title", note.title)
            put("content", note.content)
        }
        
        return context.contentResolver.insert(contentUri, values)
    }
    
    // 更新笔记
    suspend fun updateNote(note: Note): Int {
        val uri = Uri.withAppendedPath(contentUri, note.id.toString())
        val values = ContentValues().apply {
            put("title", note.title)
            put("content", note.content)
        }
        
        return context.contentResolver.update(uri, values, null, null)
    }
    
    // 删除笔记
    suspend fun deleteNote(id: Long): Int {
        val uri = Uri.withAppendedPath(contentUri, id.toString())
        return context.contentResolver.delete(uri, null, null)
    }
    
    // 搜索笔记
    fun searchNotes(keyword: String): Flow<List<Note>> = flow {
        val cursor = context.contentResolver.query(
            contentUri,
            arrayOf("_id", "title", "content", "created_at", "updated_at"),
            "title LIKE ? OR content LIKE ?",
            arrayOf("%$keyword%", "%$keyword%"),
            "created_at DESC"
        )
        
        val notes = mutableListOf<Note>()
        cursor?.use {
            val idIndex = it.getColumnIndexOrThrow("_id")
            val titleIndex = it.getColumnIndexOrThrow("title")
            val contentIndex = it.getColumnIndexOrThrow("content")
            val createdAtIndex = it.getColumnIndexOrThrow("created_at")
            val updatedAtIndex = it.getColumnIndexOrThrow("updated_at")
            
            while (it.moveToNext()) {
                notes.add(Note(
                    id = it.getLong(idIndex),
                    title = it.getString(titleIndex),
                    content = it.getString(contentIndex),
                    createdAt = it.getLong(createdAtIndex),
                    updatedAt = it.getLong(updatedAtIndex)
                ))
            }
        }
        
        emit(notes)
    }.flowOn(Dispatchers.IO)
}

strings.xml

xml 复制代码
<resources>
    <string name="app_name">ContentProvider Demo</string>
    <string name="read_notes_permission_desc">允许应用读取笔记数据</string>
    <string name="write_notes_permission_desc">允许应用写入笔记数据</string>
    <string name="insert_note">插入笔记</string>
    <string name="query_notes">查询所有笔记</string>
    <string name="update_note">更新笔记</string>
    <string name="delete_note">删除笔记</string>
    <string name="batch_insert">批量插入</string>
    <string name="result_placeholder">操作结果将显示在这里</string>
</resources>

十五、扩展阅读与下一步

相关技术栈

学完 ContentProvider,你可以继续深入以下主题:

1. Android Jetpack 组件:

  • Room:现代化的 SQLite ORM
  • WorkManager:后台任务调度
  • DataStore:替代 SharedPreferences
  • Paging 3:分页加载大数据集

2. 跨进程通信:

  • AIDL(Android Interface Definition Language)
  • Messenger:基于消息的 IPC
  • Binder 机制:深入理解 Android IPC 底层

3. 数据同步:

  • SyncAdapter:数据同步适配器
  • AccountManager:账户管理
  • Cloud Firestore:云端数据同步

4. 安全与加密:

  • Android Keystore:密钥管理
  • EncryptedSharedPreferences:加密配置
  • SQLCipher:加密数据库

推荐学习路径

md 复制代码
第一阶段:Android 四大组件(已完成)
├── Activity
├── Service
├── BroadcastReceiver
└── ContentProvider

第二阶段:Jetpack 架构组件
├── ViewModel + LiveData
├── Room 数据库
├── Navigation 组件
├── WorkManager
└── DataStore

第三阶段:现代 Android 开发
├── Jetpack Compose
├── Kotlin Coroutines + Flow
├── Hilt 依赖注入
├── Retrofit + OkHttp
└── 模块化架构

第四阶段:高级主题
├── 性能优化
├── 内存管理
├── 安全加固
└── 单元测试 + UI 测试

官方文档


十六、互动与反馈

你学到了什么?

完成这篇文章后,你应该能够:

  • ✅ 理解 ContentProvider 的工作原理
  • ✅ 实现一个完整的 ContentProvider
  • ✅ 使用 UriMatcher 匹配不同的 URI
  • ✅ 实现权限控制和安全防护
  • ✅ 使用 ContentObserver 监听数据变化
  • ✅ 优化性能(批量操作、异步查询、索引)
  • ✅ 调试和排查常见问题
  • ✅ 选择合适的数据共享方案

常见疑问解答

Q1:什么时候必须用 ContentProvider?

A:只有在需要向其他应用暴露数据时才必须使用。如果只是应用内部使用,Room 更好。

Q2:ContentProvider 和 Room 能一起用吗?

A:可以!你可以在 ContentProvider 内部使用 Room 作为数据源:

kotlin 复制代码
class NoteProvider : ContentProvider() {
    private lateinit var database: NoteDatabase
    
    override fun onCreate(): Boolean {
        database = Room.databaseBuilder(
            context!!,
            NoteDatabase::class.java,
            "notes.db"
        ).build()
        return true
    }
    
    override fun query(...): Cursor? {
        // 使用 Room DAO 查询,然后转换为 Cursor
        val notes = database.noteDao().getAllNotes()
        return convertToCursor(notes)
    }
}

Q3:ContentProvider 性能如何?

A:由于涉及跨进程通信(Binder),ContentProvider 的性能开销比直接数据库访问大。但对于大多数场景,这个开销是可以接受的。如果性能关键,考虑:

  • 使用批量操作
  • 添加索引
  • 异步查询
  • 限制返回的数据量

Q4:能用 ContentProvider 传输大文件吗?

A:不推荐通过 Cursor 传输大文件。应该使用 openFile() 方法返回 ParcelFileDescriptor,让调用方直接读取文件流。

Q5:为什么很多现代应用不用 ContentProvider?

A:因为:

  1. 大多数应用不需要跨应用共享数据
  2. Room 提供了更好的类型安全和 API
  3. 微服务化趋势,数据通过 API 共享而非本地
  4. ContentProvider 学习曲线陡峭

但系统应用和需要系统集成的应用(如输入法、启动器、同步服务)仍然需要它。

实战挑战

尝试实现以下功能来巩固学习:

初级挑战:

  • 实现一个联系人备份应用,读取系统联系人并保存
  • 创建一个笔记应用,支持跨应用分享笔记
  • 实现搜索建议功能(SearchView 集成)

中级挑战:

  • 实现数据加密的 ContentProvider
  • 添加数据同步功能(SyncAdapter)
  • 实现分页加载(配合 Paging 3)

高级挑战:

  • 实现一个自定义的文件管理器,使用 ContentProvider 暴露文件
  • 创建一个插件化系统,插件通过 ContentProvider 通信
  • 实现跨应用的数据同步机制

十七、结语

到这里,Android 四大组件系列就正式完结了。

从 Activity 的生命周期,到 Service 的后台任务,再到 BroadcastReceiver 的消息通知,最后是 ContentProvider 的数据共享,我们系统地学习了 Android 应用的核心架构。

这四大组件是 Android 的基石,理解它们的工作原理,你就掌握了 Android 开发的本质。

但要记住:

框架会变,工具会更新,但核心原理是永恒的。

现代 Android 开发正在向 Jetpack Compose、Kotlin Coroutines、Flow 等新技术演进,但这些新技术底层仍然依赖四大组件。

理解了底层,学习新技术就会事半功倍。

下一步?

建议你:

  1. 动手实践:把文章中的代码敲一遍(复制粘贴也行,毕竟我也用了AI😂),运行起来
  2. 深入一个:选择一个感兴趣的组件深入研究源码
  3. 实战项目:用四大组件搭建一个完整应用
  4. 继续学习:开始 Jetpack 架构组件的学习

感谢阅读!

如果这篇文章对你有帮助:

  • 点赞收藏,方便日后查阅
  • 在评论区分享你的学习心得
  • 转发给需要的朋友

有任何疑问,欢迎在评论区讨论。我会持续更新这个系列,敬请期待:

下一篇预告:《Android Jetpack 核心系列·第1篇:ViewModel 原理与最佳实践》


附录:快速参考表

ContentProvider 核心方法

方法 参数 返回值 说明
onCreate() - Boolean 初始化 Provider
query() uri, projection, selection, selectionArgs, sortOrder Cursor? 查询数据
insert() uri, values Uri? 插入数据
update() uri, values, selection, selectionArgs Int 更新数据,返回影响行数
delete() uri, selection, selectionArgs Int 删除数据,返回影响行数
getType() uri String? 返回 MIME 类型

ContentResolver 常用方法

方法 说明
query() 查询数据
insert() 插入单条数据
bulkInsert() 批量插入
update() 更新数据
delete() 删除数据
applyBatch() 批量操作(推荐)
registerContentObserver() 注册观察者
unregisterContentObserver() 取消注册观察者
notifyChange() 通知数据变化
openInputStream() 打开输入流
openOutputStream() 打开输出流
openFileDescriptor() 打开文件描述符

UriMatcher 通配符

通配符 说明 示例
# 匹配数字 notes/# 匹配 notes/1
* 匹配任意字符串 notes/* 匹配 notes/abc

MIME 类型格式

arduino 复制代码
vnd.android.cursor.dir/vnd.<company>.<type>   // 多条记录
vnd.android.cursor.item/vnd.<company>.<type>  // 单条记录

常用系统 ContentProvider

功能 URI 权限
联系人 content://com.android.contacts/contacts READ_CONTACTS
通话记录 content://call_log/calls READ_CALL_LOG
短信 content://sms/ READ_SMS
图片 content://media/external/images/media READ_EXTERNAL_STORAGE
音频 content://media/external/audio/media READ_EXTERNAL_STORAGE
视频 content://media/external/video/media READ_EXTERNAL_STORAGE
日历 content://com.android.calendar/events READ_CALENDAR

系列文章:

  • 第一篇:[《彻底讲懂 Activity》](#《彻底讲懂 Activity》 "#")
  • 第二篇:[《彻底讲懂 Service》](#《彻底讲懂 Service》 "#")
  • 第三篇:[《彻底讲懂 BroadcastReceiver》](#《彻底讲懂 BroadcastReceiver》 "#")
  • 第四篇:[《彻底讲懂 ContentProvider》](#《彻底讲懂 ContentProvider》 "#")(本篇)

版权声明:

本文为原创技术文章,欢迎转载,但请注明出处。

最后更新: 2025年10月

作者: ANTI-Tony


全文完。祝你在 Android 开发的道路上越走越远!

相关推荐
小马爱打代码8 小时前
zookeeper:架构原理和使用场景
分布式·zookeeper·架构
鹏多多8 小时前
flutter-切换状态显示不同组件10种实现方案全解析
android·前端·ios
FengyunSky9 小时前
高通Camx内存问题排查
android·linux·后端
00后程序员张10 小时前
苹果软件混淆的工程逻辑,从符号空间到资源扰动的体系化实现
android·ios·小程序·https·uni-app·iphone·webview
武子康12 小时前
AI-调查研究-96-具身智能 机器人场景测试全攻略:从极端环境到实时仿真
人工智能·深度学习·机器学习·ai·架构·系统架构·具身智能
alexhilton18 小时前
突破速度障碍:非阻塞启动画面如何将Android 应用启动时间缩短90%
android·kotlin·android jetpack
kobe_OKOK_19 小时前
Django `models.Field` 所有常见配置参数的完整清单与说明表
android
canonical_entropy19 小时前
对《DDD本质论》一文的解读
后端·架构·领域驱动设计
haogexiaole20 小时前
Java高并发常见架构、处理方式、api调优
java·开发语言·架构