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
- 运行应用
- 打开 View → Tool Windows → App Inspection
- 选择 Database Inspector
- 可以实时查看数据库内容、执行 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 四大组件已完结:
- Activity - 用户界面
- Service - 后台任务
- BroadcastReceiver - 消息通知
- ContentProvider - 数据共享
现代 Android 开发趋势:
虽然四大组件是 Android 的基础,但现代应用开发正在向更高层次的架构演进:
markdown
传统方式:
Activity → ContentProvider → SQLite
现代方式:
Jetpack Compose → ViewModel → Repository → Room
↓
可选:ContentProvider(仅用于跨应用)
什么时候还需要 ContentProvider?
- 系统集成:需要被系统或其他应用访问(如输入法、壁纸、同步适配器)
- 数据共享:明确需要向第三方应用暴露数据
- 权限控制:需要细粒度的读写权限分离
- 标准接口:需要符合 Android 标准的 CRUD 接口
什么时候不需要?
- 应用内部:单应用的数据访问,直接用 Room
- 简单配置:键值对存储,用 SharedPreferences/DataStore
- 网络数据:API 调用,用 Retrofit + Repository
- 实时通信:应用内事件,用 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:因为:
- 大多数应用不需要跨应用共享数据
- Room 提供了更好的类型安全和 API
- 微服务化趋势,数据通过 API 共享而非本地
- ContentProvider 学习曲线陡峭
但系统应用和需要系统集成的应用(如输入法、启动器、同步服务)仍然需要它。
实战挑战
尝试实现以下功能来巩固学习:
初级挑战:
- 实现一个联系人备份应用,读取系统联系人并保存
- 创建一个笔记应用,支持跨应用分享笔记
- 实现搜索建议功能(SearchView 集成)
中级挑战:
- 实现数据加密的 ContentProvider
- 添加数据同步功能(SyncAdapter)
- 实现分页加载(配合 Paging 3)
高级挑战:
- 实现一个自定义的文件管理器,使用 ContentProvider 暴露文件
- 创建一个插件化系统,插件通过 ContentProvider 通信
- 实现跨应用的数据同步机制
十七、结语
到这里,Android 四大组件系列就正式完结了。
从 Activity 的生命周期,到 Service 的后台任务,再到 BroadcastReceiver 的消息通知,最后是 ContentProvider 的数据共享,我们系统地学习了 Android 应用的核心架构。
这四大组件是 Android 的基石,理解它们的工作原理,你就掌握了 Android 开发的本质。
但要记住:
框架会变,工具会更新,但核心原理是永恒的。
现代 Android 开发正在向 Jetpack Compose、Kotlin Coroutines、Flow 等新技术演进,但这些新技术底层仍然依赖四大组件。
理解了底层,学习新技术就会事半功倍。
下一步?
建议你:
- 动手实践:把文章中的代码敲一遍(复制粘贴也行,毕竟我也用了AI😂),运行起来
- 深入一个:选择一个感兴趣的组件深入研究源码
- 实战项目:用四大组件搭建一个完整应用
- 继续学习:开始 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 开发的道路上越走越远!