ContentProvider 与数据共享
问题
ContentProvider 的作用是什么?它是如何实现跨进程数据共享的?
答案
1. ContentProvider 概述
ContentProvider 是 Android 四大组件中唯一专门用于跨进程数据共享的组件。它提供统一的 CRUD 接口,底层通过 Binder IPC 实现。
2. URI 结构
content://com.example.app.provider/users/123
|______| |________________________| |____| |_|
scheme authority path id
// 定义 URI 常量
object UserContract {
const val AUTHORITY = "com.example.app.provider"
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/users")
object Columns {
const val ID = "_id"
const val NAME = "name"
const val EMAIL = "email"
}
}
3. 实现 ContentProvider
class UserProvider : ContentProvider() {
private lateinit var dbHelper: DatabaseHelper
companion object {
private const val USERS = 1
private const val USER_ID = 2
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(UserContract.AUTHORITY, "users", USERS)
addURI(UserContract.AUTHORITY, "users/#", USER_ID)
}
}
override fun onCreate(): Boolean {
dbHelper = DatabaseHelper(context!!)
return true
}
override fun query(
uri: Uri, projection: Array<String>?,
selection: String?, selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
val db = dbHelper.readableDatabase
val cursor = when (uriMatcher.match(uri)) {
USERS -> db.query("users", projection, selection, selectionArgs, null, null, sortOrder)
USER_ID -> {
val id = uri.lastPathSegment
db.query("users", projection, "_id=?", arrayOf(id), null, null, sortOrder)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
cursor?.setNotificationUri(context!!.contentResolver, uri)
return cursor
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = dbHelper.writableDatabase
val id = db.insert("users", null, values)
context!!.contentResolver.notifyChange(uri, null)
return ContentUris.withAppendedId(UserContract.CONTENT_URI, id)
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
val db = dbHelper.writableDatabase
val count = db.update("users", values, selection, selectionArgs)
context!!.contentResolver.notifyChange(uri, null)
return count
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val db = dbHelper.writableDatabase
val count = db.delete("users", selection, selectionArgs)
context!!.contentResolver.notifyChange(uri, null)
return count
}
override fun getType(uri: Uri): String = when (uriMatcher.match(uri)) {
USERS -> "vnd.android.cursor.dir/vnd.${UserContract.AUTHORITY}.users"
USER_ID -> "vnd.android.cursor.item/vnd.${UserContract.AUTHORITY}.users"
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
4. 使用 ContentResolver 访问
// 查询
val cursor = contentResolver.query(
UserContract.CONTENT_URI,
arrayOf(UserContract.Columns.NAME, UserContract.Columns.EMAIL),
null, null, null
)
cursor?.use {
while (it.moveToNext()) {
val name = it.getString(it.getColumnIndexOrThrow(UserContract.Columns.NAME))
}
}
// 插入
val values = ContentValues().apply {
put(UserContract.Columns.NAME, "Alice")
put(UserContract.Columns.EMAIL, "alice@example.com")
}
val uri = contentResolver.insert(UserContract.CONTENT_URI, values)
5. FileProvider(文件共享)
Android 7.0+ 要求通过 FileProvider 共享文件 URI:
<!-- AndroidManifest.xml -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<!-- res/xml/file_paths.xml -->
<paths>
<external-files-path name="images" path="Pictures/" />
<cache-path name="cache" path="/" />
</paths>
// 分享文件
val file = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "photo.jpg")
val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", file)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "image/jpeg"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(shareIntent, "分享图片"))
常见面试问题
Q1: ContentProvider 的 onCreate 在什么时候调用?
答案:
ContentProvider 的 onCreate 在 Application.onCreate 之前调用。这是 Android 组件中最早初始化的。
启动顺序:Application.attachBaseContext → ContentProvider.onCreate → Application.onCreate
许多第三方库(如 WorkManager、Firebase)利用这个特性通过 ContentProvider 实现自动初始化(无需手动在 Application 中初始化)。App Startup 库就是为了优化这种场景设计的。
Q2: ContentProvider 是线程安全的吗?
答案:
ContentProvider 的 CRUD 方法可能在多个线程上被调用(来自不同进程的 Binder 线程池)。因此需要自行保证线程安全:
- SQLiteDatabase 的
insert/update/delete内部有锁,基本线程安全 - 但复合操作(先查后改)仍需额外同步
- Room 数据库可以更好地处理并发
Q3: ContentProvider 和直接 AIDL 的区别?
答案:
| 特性 | ContentProvider | AIDL |
|---|---|---|
| 接口 | 固定 CRUD | 自定义接口 |
| 数据格式 | Cursor / Uri | 任意 Parcelable |
| 使用复杂度 | 简单(标准 API) | 复杂(需定义 .aidl) |
| 适用场景 | 结构化数据共享 | 任意跨进程通信 |
| 权限控制 | 内置支持 | 需自行实现 |
ContentProvider 本身底层也是通过 Binder(类似 AIDL)实现的。