Appearance
Android ContentProvider 内容提供者 - 全面详解
ContentProvider 是 Android 四大组件之一,用于在应用间安全地共享数据。
目录
- ContentProvider 基础概念
- CRUD 操作详解
- UriMatcher 使用
- ContentObserver 监听变化
- 跨进程数据共享
- 权限控制
- 常用 ContentProvider
- 自定义 ContentProvider
- 面试考点
1. ContentProvider 基础概念
1.1 什么是 ContentProvider
ContentProvider(内容提供者)是 Android 四大组件之一,为应用提供了一种标准化的数据共享接口。
核心作用:
- ✅ 应用间数据共享
- ✅ 统一的数据访问接口
- ✅ 权限控制和数据安全
- ✅ 数据抽象和封装
- ✅ 支持远程查询(跨进程)
1.2 为什么需要 ContentProvider
问题场景:没有 ContentProvider 时
┌─────────────┐ ┌─────────────┐
│ App A │ │ App B │
│ │ │ │
│ 直接访问数据库 │ ❌ │ 数据库私有 │
│ │ │ │
└─────────────┘ └─────────────┘
问题:
- 无法直接访问其他应用的数据库
- 数据库结构变化会导致兼容性问题
- 没有统一的权限控制机制使用 ContentProvider 后:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ App A │────▶│ ContentProvider│────▶│ App B │
│ │ │ (App C) │ │ │
│ query │ │ │ │ query │
│ insert │ │ 数据库 │ │ update │
│ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
优势:
- 统一的数据访问接口
- 权限控制
- 数据抽象(App B 不需要知道 App C 的数据库结构)1.3 ContentProvider 工作原理
┌────────────────────────────────────────────────────────┐
│ ContentProvider 工作流程 │
│ │
│ ┌──────────┐ URI ┌─────────────┐ │
│ │ Consumer │ ───────> │ Provider │ │
│ │ (Client) │ 查询请求 │ (Content) │ │
│ └──────────┘ └──────┬──────┘ │
│ │ │
│ 解析 URI │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Database │ │
│ │ /File │ │
│ │ /Memory │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Cursor │ │
│ │ (结果) │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ 返回数据 │
│ │ Consumer │ ◀────────────────────────── │
│ └──────────┘ │
│ │
└────────────────────────────────────────────────────────┘1.4 ContentProvider 的 URI 结构
ContentProvider 通过 URI(统一资源标识符)来定位数据。
URI 格式:
content://authority/path/id
例如:
content://com.example.provider/books/123
│ │ │ │
│ │ │ └─ ID(可选)
│ │ └───── 路径
│ └────────────── 权威标识符
└────────────────────────── 方案(固定为 content)解析示例:
kotlin
val uri = Uri.parse("content://com.example.provider/books/123")
uri.scheme // "content"
uri.authority // "com.example.provider"
uri.path // "/books/123"
uri.pathSegments // ["books", "123"]
uri.lastPathSegment // "123"2. CRUD 操作详解
2.1 ContentProvider 核心方法
kotlin
class MyContentProvider : ContentProvider() {
// 1. 查询数据
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
// 实现查询逻辑
return database.query(...)
}
// 2. 插入数据
override fun insert(
uri: Uri,
values: ContentValues?
): Uri? {
// 实现插入逻辑
val id = database.insert(...)
return Uri.withAppendedPath(uri, id.toString())
}
// 3. 更新数据
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int {
// 实现更新逻辑
return database.update(...)
}
// 4. 删除数据
override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<String>?
): Int {
// 实现删除逻辑
return database.delete(...)
}
// 5. 返回 MIME 类型
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
BOOKS -> "vnd.android.cursor.dir/vnd.com.example.book"
BOOK -> "vnd.android.cursor.item/vnd.com.example.book"
else -> null
}
}
// 6. 初始化
override fun onCreate(): Boolean {
// 初始化数据库
return true
}
}2.2 查询操作(Query)
ContentResolver 查询:
kotlin
// 方式 1:查询所有数据
fun queryAllBooks(context: Context) {
val cursor = context.contentResolver.query(
BooksContract.CONTENT_URI,
null, // 所有列
null, // 无筛选条件
null, // 无筛选参数
null // 无排序
)
cursor?.use {
while (it.moveToNext()) {
val id = it.getInt(it.getColumnIndexOrThrow(BooksContract._ID))
val title = it.getString(it.getColumnIndexOrThrow(BooksContract.TITLE))
val author = it.getString(it.getColumnIndexOrThrow(BooksContract.AUTHOR))
Log.d("Book", "$id: $title by $author")
}
}
}
// 方式 2:查询指定列
fun queryBookTitles(context: Context) {
val projection = arrayOf(
BooksContract.TITLE,
BooksContract.AUTHOR
)
val cursor = context.contentResolver.query(
BooksContract.CONTENT_URI,
projection,
null,
null,
null
)
}
// 方式 3:带筛选条件查询
fun queryBooksByAuthor(context: Context, author: String) {
val selection = "${BooksContract.AUTHOR} = ?"
val selectionArgs = arrayOf(author)
val cursor = context.contentResolver.query(
BooksContract.CONTENT_URI,
null,
selection,
selectionArgs,
null
)
}
// 方式 4:带排序查询
fun queryBooksSorted(context: Context) {
val sortOrder = "${BooksContract.TITLE} ASC, ${BooksContract.CREATED_DATE} DESC"
val cursor = context.contentResolver.query(
BooksContract.CONTENT_URI,
null,
null,
null,
sortOrder
)
}
// 方式 5:查询指定 ID 的书籍
fun queryBookById(context: Context, id: Long) {
val bookUri = ContentUris.withAppendedId(
BooksContract.CONTENT_URI,
id
)
val cursor = context.contentResolver.query(
bookUri,
null,
null,
null,
null
)
}2.3 插入操作(Insert)
kotlin
// 插入单条数据
fun insertBook(context: Context, title: String, author: String): Uri? {
val values = ContentValues().apply {
put(BooksContract.TITLE, title)
put(BooksContract.AUTHOR, author)
put(BooksContract.PAGE_COUNT, 300)
put(BooksContract.CREATED_DATE, System.currentTimeMillis())
}
return context.contentResolver.insert(
BooksContract.CONTENT_URI,
values
)
}
// 插入多条数据(使用 Transaction)
fun insertMultipleBooks(context: Context, books: List<Book>) {
context.contentResolver.apply {
acquireContentProviderTransaction()
try {
for (book in books) {
val values = ContentValues().apply {
put(BooksContract.TITLE, book.title)
put(BooksContract.AUTHOR, book.author)
}
insert(BooksContract.CONTENT_URI, values)
}
} finally {
finishContentProviderTransaction()
}
}
}
// 使用 BulkInsertCallback 批量插入
fun bulkInsertBooks(context: Context, books: List<Book>) {
val values = books.map { book ->
ContentValues().apply {
put(BooksContract.TITLE, book.title)
put(BooksContract.AUTHOR, book.author)
}
}.toTypedArray()
context.contentResolver.bulkInsert(
BooksContract.CONTENT_URI,
values
)
}2.4 更新操作(Update)
kotlin
// 更新指定 ID 的数据
fun updateBook(context: Context, id: Long, newTitle: String): Int {
val values = ContentValues().apply {
put(BooksContract.TITLE, newTitle)
put(BooksContract.UPDATED_DATE, System.currentTimeMillis())
}
val selection = "${BooksContract._ID} = ?"
val selectionArgs = arrayOf(id.toString())
return context.contentResolver.update(
ContentUris.withAppendedId(BooksContract.CONTENT_URI, id),
values,
null,
null
)
}
// 更新多条数据
fun updateBooksByAuthor(context: Context, author: String, newAuthor: String): Int {
val values = ContentValues().apply {
put(BooksContract.AUTHOR, newAuthor)
}
val selection = "${BooksContract.AUTHOR} = ?"
val selectionArgs = arrayOf(author)
return context.contentResolver.update(
BooksContract.CONTENT_URI,
values,
selection,
selectionArgs
)
}2.5 删除操作(Delete)
kotlin
// 删除指定 ID 的数据
fun deleteBook(context: Context, id: Long): Int {
val bookUri = ContentUris.withAppendedId(BooksContract.CONTENT_URI, id)
return context.contentResolver.delete(bookUri, null, null)
}
// 删除多条数据
fun deleteBooksByAuthor(context: Context, author: String): Int {
val selection = "${BooksContract.AUTHOR} = ?"
val selectionArgs = arrayOf(author)
return context.contentResolver.delete(
BooksContract.CONTENT_URI,
selection,
selectionArgs
)
}
// 删除所有数据
fun deleteAllBooks(context: Context): Int {
return context.contentResolver.delete(
BooksContract.CONTENT_URI,
null,
null
)
}2.6 使用 Kotlin Flow 进行协程查询
kotlin
// 将 Cursor 转换为 Flow
fun Context.booksFlow(): Flow<List<Book>> = flow {
val cursor = contentResolver.query(
BooksContract.CONTENT_URI,
null,
null,
null,
null
)
val books = mutableListOf<Book>()
cursor?.use {
while (it.moveToNext()) {
val book = Book(
id = it.getLong(it.getColumnIndexOrThrow(BooksContract._ID)),
title = it.getString(it.getColumnIndexOrThrow(BooksContract.TITLE)),
author = it.getString(it.getColumnIndexOrThrow(BooksContract.AUTHOR))
)
books.add(book)
}
}
emit(books)
}
// 在 ViewModel 中使用
class BooksViewModel(application: Application) : ViewModel() {
private val context = application.applicationContext
private val _books = MutableStateFlow<List<Book>>(emptyList())
val books: StateFlow<List<Book>> = _books.asStateFlow()
init {
loadBooks()
}
private fun loadBooks() {
viewModelScope.launch {
context.booksFlow().collect { books ->
_books.value = books
}
}
}
}3. UriMatcher 使用
3.1 什么是 UriMatcher
UriMatcher 用于解析和匹配 ContentProvider 的 URI,根据匹配结果返回不同的代码,便于在 ContentProvider 中进行相应的处理。
3.2 添加 URI 匹配规则
kotlin
class BooksContentProvider : ContentProvider() {
private lateinit var database: SQLiteDatabase
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
companion object {
// 定义匹配代码
private const val BOOKS = 1 // 匹配 /books
private const val BOOK = 2 // 匹配 /books/123
private const val AUTHORS = 3 // 匹配 /authors
private const val AUTHOR = 4 // 匹配 /authors/456
}
override fun onCreate(): Boolean {
// 初始化数据库
database = dbHelper.writableDatabase
// 添加匹配规则
uriMatcher.addURI(
BooksContract.AUTHORITY, // 权威标识符
BooksContract.PATH_BOOKS, // 路径
BOOKS // 匹配代码
)
uriMatcher.addURI(
BooksContract.AUTHORITY,
BooksContract.PATH_BOOKS + "/#", // # 表示数字 ID
BOOK
)
uriMatcher.addURI(
BooksContract.AUTHORITY,
BooksContract.PATH_AUTHORS,
AUTHORS
)
uriMatcher.addURI(
BooksContract.AUTHORITY,
BooksContract.PATH_AUTHORS + "/#",
AUTHOR
)
return true
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
return when (uriMatcher.match(uri)) {
BOOKS -> {
// 查询所有书籍
database.query(
BooksContract.TABLE_BOOKS,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
)
}
BOOK -> {
// 查询指定 ID 的书籍
val id = uri.lastPathSegment?.toLongOrNull() ?: return null
val newSelection = "(${selection ?: ""}) AND ${BooksContract._ID} = ?"
val newSelectionArgs = if (selectionArgs != null) {
selectionArgs + id.toString()
} else {
arrayOf(id.toString())
}
database.query(
BooksContract.TABLE_BOOKS,
projection,
newSelection,
newSelectionArgs,
null,
null,
sortOrder
)
}
AUTHORS -> {
// 查询所有作者
database.query(
AuthorsContract.TABLE_AUTHORS,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
)
}
AUTHOR -> {
// 查询指定 ID 的作者
val id = uri.lastPathSegment?.toLongOrNull() ?: return null
// ... 类似 BOOK 的处理
null
}
else -> {
throw IllegalArgumentException("Unknown URI: $uri")
}
}
}
// insert, update, delete 方法同样使用 uriMatcher 判断
}3.3 UriMatcher 匹配规则
kotlin
// 精确匹配
uriMatcher.addURI("com.example", "books", BOOKS)
// 匹配:content://com.example/books
// 通配符匹配(# 表示数字)
uriMatcher.addURI("com.example", "books/#", BOOK)
// 匹配:content://com.example/books/123
// 通配符匹配(* 表示任意字符)
uriMatcher.addURI("com.example", "books/*/authors", BOOK_AUTHORS)
// 匹配:content://com.example/books/123/authors
// 多级路径匹配
uriMatcher.addURI("com.example", "books/*/chapters/*", BOOK_CHAPTER)
// 匹配:content://com.example/books/123/chapters/4563.4 自定义 UriMatcher
kotlin
class CustomUriMatcher : UriMatcher(NO_MATCH) {
companion object {
const val USERS = 100
const val USER = 101
const val USER_POSTS = 102
const val USER_POST = 103
}
fun addRules(authority: String) {
addURI(authority, "users", USERS)
addURI(authority, "users/#", USER)
addURI(authority, "users/*/posts", USER_POSTS)
addURI(authority, "users/*/posts/#", USER_POST)
}
fun extractUserId(uri: Uri): Long? {
return when (match(uri)) {
USER, USER_POSTS, USER_POST -> {
uri.pathSegments[1].toLongOrNull()
}
else -> null
}
}
fun extractPostId(uri: Uri): Long? {
return if (match(uri) == USER_POST) {
uri.pathSegments[3].toLongOrNull()
} else {
null
}
}
}4. ContentObserver 监听变化
4.1 什么是 ContentObserver
ContentObserver 用于监听 ContentProvider 的数据变化,当数据发生增删改时自动通知观察者。
4.2 注册 ContentObserver
kotlin
class BookActivity : AppCompatActivity() {
private var contentObserver: ContentObserver? = null
override fun onStart() {
super.onStart()
// 创建 ContentObserver
contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
// 数据变化时调用
refreshBooks()
}
override fun onChange(selfChange: Boolean, uri: Uri?) {
// 带 URI 的变化通知
if (uri == BooksContract.CONTENT_URI) {
refreshBooks()
}
}
}
// 注册观察者
contentResolver.registerContentObserver(
BooksContract.CONTENT_URI,
true, // 是否立即通知
contentObserver
)
}
override fun onStop() {
super.onStop()
// 注销观察者
contentObserver?.let {
contentResolver.unregisterContentObserver(it)
}
}
private fun refreshBooks() {
// 重新加载数据
loadBooks()
}
}4.3 通知观察者
在 ContentProvider 中通知数据变化:
kotlin
class BooksContentProvider : ContentProvider() {
override fun insert(
uri: Uri,
values: ContentValues?
): Uri? {
val id = database.insert(
BooksContract.TABLE_BOOKS,
null,
values
)
// 插入成功后通知观察者
val insertUri = ContentUris.withAppendedId(uri, id)
context?.contentResolver?.notifyChange(insertUri, null)
return insertUri
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int {
val count = database.update(
BooksContract.TABLE_BOOKS,
values,
selection,
selectionArgs
)
// 更新成功后通知观察者
if (count > 0) {
context?.contentResolver?.notifyChange(uri, null)
}
return count
}
override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<String>?
): Int {
val count = database.delete(
BooksContract.TABLE_BOOKS,
selection,
selectionArgs
)
// 删除成功后通知观察者
if (count > 0) {
context?.contentResolver?.notifyChange(uri, null)
}
return count
}
}4.4 使用 LiveData 监听
kotlin
// 使用 LiveData 包装 ContentObserver
fun Context.observeBooks(): LiveData<List<Book>> {
return object : LiveData<List<Book>>() {
private val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
postValue(loadBooks())
}
}
override fun onActive() {
super.onActive()
contentResolver.registerContentObserver(
BooksContract.CONTENT_URI,
true,
contentObserver
)
}
override fun onInactive() {
super.onInactive()
contentResolver.unregisterContentObserver(contentObserver)
}
private fun loadBooks(): List<Book> {
// 查询书籍
return emptyList()
}
}
}
// 在 Activity 中使用
bookLiveData.observe(this) { books ->
adapter.submitList(books)
}4.5 使用 Flow 监听
kotlin
fun Context.booksFlow(): StateFlow<List<Book>> {
return MutableStateFlow(emptyList<Book>()).apply {
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
val books = contentResolver.query(
BooksContract.CONTENT_URI,
null,
null,
null,
null
)?.use { cursor ->
cursor.toBooksList()
} ?: emptyList()
value = books
}
}
// 注册观察者
contentResolver.registerContentObserver(
BooksContract.CONTENT_URI,
true,
observer
)
}
}5. 跨进程数据共享
5.1 跨进程通信原理
┌─────────────────┐ ┌─────────────────┐
│ App A │ │ App B │
│ │ │ │
│ ContentResolver│ │ ContentProvider│
│ │ │ │
│ query() │────────────▶ │ query() │
│ │ IPC (Binder) │
│ Cursor │◀──────────── │ Cursor │
│ │ │ │
└─────────────────┘ └─────────────────┘5.2 设置跨进程访问
在 Manifest 中声明:
xml
<provider
android:name=".BooksContentProvider"
android:authorities="com.example.provider.books"
android:exported="true" <!-- 允许跨进程访问 -->
android:grantUriPermissions="true"
android:permission="com.example.READ_BOOKS_PERMISSION">
<path-permission
android:path="/books"
android:readPermission="com.example.READ_BOOKS_PERMISSION"
android:writePermission="com.example.WRITE_BOOKS_PERMISSION" />
</provider>
<!-- 定义权限 -->
<permission
android:name="com.example.READ_BOOKS_PERMISSION"
android:protectionLevel="normal" />
<permission
android:name="com.example.WRITE_BOOKS_PERMISSION"
android:protectionLevel="normal" />其他应用访问:
kotlin
// App B 访问 App A 的 ContentProvider
// 1. 声明使用权限
// App B 的 AndroidManifest.xml
<uses-permission android:name="com.example.READ_BOOKS_PERMISSION" />
<uses-permission android:name="com.example.WRITE_BOOKS_PERMISSION" />
// 2. 查询数据
fun queryBooksFromOtherApp(context: Context) {
val uri = Uri.parse("content://com.example.provider.books/books")
val cursor = context.contentResolver.query(
uri,
arrayOf("title", "author"),
null,
null,
null
)
cursor?.use {
while (it.moveToNext()) {
val title = it.getString(0)
val author = it.getString(1)
Log.d("Books", "$title by $author")
}
}
}
// 3. 插入数据
fun insertBookToOtherApp(context: Context, title: String, author: String) {
val uri = Uri.parse("content://com.example.provider.books/books")
val values = ContentValues().apply {
put("title", title)
put("author", author)
}
context.contentResolver.insert(uri, values)
}5.3 跨进程 Cursor
跨进程 Cursor 的使用注意事项:
kotlin
// ⚠️ 注意:跨进程返回的 Cursor 不能直接使用,需要转换
// 方式 1:使用 copy()
val cursor = contentResolver.query(uri, projection, null, null, null)
val localCursor = cursor?.copy()
localCursor?.use {
// 安全使用
}
// 方式 2:转换为 List
val cursor = contentResolver.query(uri, projection, null, null, null)
val data = cursor?.toList()
cursor?.close()
// 方式 3:使用 ContentResolver 的便捷方法
val books = queryBooksAsList(context, uri)6. 权限控制
6.1 权限保护级别
xml
<!-- normal:安装时自动授予 -->
<permission
android:name="com.example.NORMAL_PERMISSION"
android:protectionLevel="normal" />
<!-- dangerous:用户手动授予 -->
<permission
android:name="com.example.DANGEROUS_PERMISSION"
android:protectionLevel="dangerous" />
<!-- signature:签名一致才能获取 -->
<permission
android:name="com.example.SIGNATURE_PERMISSION"
android:protectionLevel="signature" />
<!-- signatureOrSystem:签名一致或系统应用 -->
<permission
android:name="com.example.SIGNATURE_OR_SYSTEM_PERMISSION"
android:protectionLevel="signatureOrSystem" />6.2 ContentProvider 权限设置
xml
<provider
android:name=".SecureContentProvider"
android:authorities="com.example.secure.provider"
android:exported="true"
android:permission="com.example.ACCESS_SECURE_DATA">
<!-- 路径级别的权限 -->
<path-permission
android:path="/public"
android:readPermission="com.example.READ_PUBLIC"
android:writePermission="com.example.WRITE_PUBLIC" />
<path-permission
android:path="/private"
android:readPermission="com.example.READ_PRIVATE"
android:writePermission="com.example.WRITE_PRIVATE" />
</provider>6.3 运行时权限检查
kotlin
class SecureContentProvider : ContentProvider() {
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
// 检查权限
if (!hasReadPermission()) {
throw SecurityException("Permission denied for reading")
}
return database.query(...)
}
override fun insert(
uri: Uri,
values: ContentValues?
): Uri? {
// 检查写权限
if (!hasWritePermission()) {
throw SecurityException("Permission denied for writing")
}
return super.insert(uri, values)
}
private fun hasReadPermission(): Boolean {
return context?.checkCallingOrSelfPermission(
"com.example.READ_PRIVATE"
) == PackageManager.PERMISSION_GRANTED
}
private fun hasWritePermission(): Boolean {
return context?.checkCallingOrSelfPermission(
"com.example.WRITE_PRIVATE"
) == PackageManager.PERMISSION_GRANTED
}
}7. 常用 ContentProvider
7.1 ContactsProvider(联系人)
kotlin
// 查询联系人
fun queryContacts(context: Context): List<Contact> {
val projection = arrayOf(
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER
)
val contacts = mutableListOf<Contact>()
val cursor = context.contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
null,
null,
"${ContactsContract.Contacts.DISPLAY_NAME} ASC"
)
cursor?.use {
while (it.moveToNext()) {
val id = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
val name = it.getString(it.getColumnIndexOrThrow(ContactsContract.DISPLAY_NAME))
val hasPhone = it.getInt(it.getColumnIndexOrThrow(ContactsContract.Contacts.HAS_PHONE_NUMBER)) == 1
if (hasPhone) {
val phones = queryPhoneNumbers(context, id)
contacts.add(Contact(name, phones))
}
}
}
return contacts
}
// 查询电话号码
fun queryPhoneNumbers(context: Context, contactId: String): List<String> {
val phones = mutableListOf<String>()
val cursor = context.contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER),
"${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
arrayOf(contactId),
null
)
cursor?.use {
while (it.moveToNext()) {
val number = it.getString(it.getColumnIndexOrThrow(
ContactsContract.CommonDataKinds.Phone.NUMBER
))
phones.add(number)
}
}
return phones
}
// 添加联系人
fun addContact(
context: Context,
name: String,
phone: String,
email: String?
) {
val rawContactId = context.contentResolver.insert(
ContactsContract.RawContacts.CONTENT_URI,
ContentValues().apply {
put(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
put(ContactsContract.RawContacts.ACCOUNT_NAME, null)
}
)?.lastPathSegment?.toLongOrNull()
if (rawContactId != null) {
// 设置姓名
context.contentResolver.insert(
ContactsContract.Data.CONTENT_URI,
ContentValues().apply {
put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.MIMETYPE)
put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, name)
}
)
// 设置电话
context.contentResolver.insert(
ContactsContract.Data.CONTENT_URI,
ContentValues().apply {
put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.MIMETYPE)
put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
}
)
// 设置邮箱
if (email != null) {
context.contentResolver.insert(
ContactsContract.Data.CONTENT_URI,
ContentValues().apply {
put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.MIMETYPE)
put(ContactsContract.CommonDataKinds.Email.DATA, email)
}
)
}
}
}7.2 MediaStore(媒体存储)
kotlin
// 查询图片
fun queryImages(context: Context): List<Uri> {
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA
)
val selection = MediaStore.Images.Media.IS_DELETED + " = 0"
val imageUris = mutableListOf<Uri>()
val cursor = context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
null,
"${MediaStore.Images.Media.DATE_ADDED} DESC"
)
cursor?.use {
while (it.moveToNext()) {
val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
val uri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
imageUris.add(uri)
}
}
return imageUris
}
// 保存图片
fun saveImage(context: Context, bitmap: Bitmap, title: String): Uri? {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, title)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
)
uri?.let {
context.contentResolver.openOutputStream(it)?.use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}
}
return uri
}
// 查询视频
fun queryVideos(context: Context): List<Video> {
val projection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE
)
val videos = mutableListOf<Video>()
val cursor = context.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
null
)
cursor?.use {
while (it.moveToNext()) {
val id = it.getLong(0)
val name = it.getString(1)
val duration = it.getLong(2)
val size = it.getLong(3)
videos.add(Video(id, name, duration, size))
}
}
return videos
}7.3 CallLogProvider(通话记录)
kotlin
// 查询通话记录
fun queryCallLog(context: Context): List<CallRecord> {
val projection = arrayOf(
CallLog.Calls._ID,
CallLog.Calls.NUMBER,
CallLog.Calls.CACHED_NAME,
CallLog.Calls.TYPE,
CallLog.Calls.DATE
)
val records = mutableListOf<CallRecord>()
val cursor = context.contentResolver.query(
CallLog.Calls.CONTENT_URI,
projection,
null,
null,
"${CallLog.Calls.DATE} DESC"
)
cursor?.use {
while (it.moveToNext()) {
val number = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
val name = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.CACHED_NAME))
val type = it.getInt(it.getColumnIndexOrThrow(CallLog.Calls.TYPE))
val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
val callType = when (type) {
CallLog.Calls.INCOMING_TYPE -> "来电"
CallLog.Calls.OUTGOING_TYPE -> "去电"
CallLog.Calls.MISSED_TYPE -> "未接"
else -> "未知"
}
records.add(CallRecord(number, name, callType, date))
}
}
return records
}8. 自定义 ContentProvider
8.1 完整的 ContentProvider 实现
定义 Contract 类:
kotlin
class BooksContract {
companion object {
// 权威标识符
const val AUTHORITY = "com.example.provider.books"
// Content URI
val CONTENT_URI: Uri = Uri.parse(
"content://$AUTHORITY/books"
)
// 路径
const val PATH_BOOKS = "books"
const val PATH_AUTHORS = "authors"
// 表名
const val TABLE_BOOKS = "books"
const val TABLE_AUTHORS = "authors"
// 列名
const val _ID = "_id"
const val TITLE = "title"
const val AUTHOR = "author"
const val PAGE_COUNT = "page_count"
const val CREATED_DATE = "created_date"
const val UPDATED_DATE = "updated_date"
// MIME 类型
const val MIME_BOOKS = "vnd.android.cursor.dir/vnd.com.example.book"
const val MIME_BOOK = "vnd.android.cursor.item/vnd.com.example.book"
}
}创建数据库帮助类:
kotlin
class DatabaseHelper(context: Context) :
SQLiteOpenHelper(context, "books.db", null, DATABASE_VERSION) {
companion object {
const val DATABASE_VERSION = 1
private const val CREATE_BOOKS = """
CREATE TABLE ${BooksContract.TABLE_BOOKS} (
${BooksContract._ID} INTEGER PRIMARY KEY AUTOINCREMENT,
${BooksContract.TITLE} TEXT NOT NULL,
${BooksContract.AUTHOR} TEXT NOT NULL,
${BooksContract.PAGE_COUNT} INTEGER,
${BooksContract.CREATED_DATE} INTEGER,
${BooksContract.UPDATED_DATE} INTEGER
)
"""
}
override fun onCreate(db: SQLiteDatabase?) {
db?.execSQL(CREATE_BOOKS)
}
override fun onUpgrade(
db: SQLiteDatabase?,
oldVersion: Int,
newVersion: Int
) {
// 处理数据库升级
}
}实现 ContentProvider:
kotlin
class BooksContentProvider : ContentProvider() {
private var database: SQLiteDatabase? = null
private var context: Context? = null
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(BooksContract.AUTHORITY, BooksContract.PATH_BOOKS, BOOKS)
addURI(BooksContract.AUTHORITY, BooksContract.PATH_BOOKS + "/#", BOOK)
}
companion object {
private const val BOOKS = 1
private const val BOOK = 2
}
override fun onCreate(): Boolean {
context = context
database = DatabaseHelper(context).writableDatabase
return true
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
val db = database ?: return null
return when (uriMatcher.match(uri)) {
BOOKS -> db.query(
BooksContract.TABLE_BOOKS,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
)
BOOK -> {
val id = uri.lastPathSegment?.toLongOrNull() ?: return null
db.query(
BooksContract.TABLE_BOOKS,
projection,
"(${selection ?: ""}) AND ${BooksContract._ID} = ?",
if (selectionArgs != null) selectionArgs + id.toString()
else arrayOf(id.toString()),
null,
null,
sortOrder
)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun insert(
uri: Uri,
values: ContentValues?
): Uri? {
if (uriMatcher.match(uri) != BOOKS || values == null) {
return null
}
val id = database?.insertOrThrow(
BooksContract.TABLE_BOOKS,
null,
values
)
context?.contentResolver?.notifyChange(
ContentUris.withAppendedId(uri, id),
null
)
return ContentUris.withAppendedId(uri, id)
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int {
return when (uriMatcher.match(uri)) {
BOOKS -> database?.update(
BooksContract.TABLE_BOOKS,
values,
selection,
selectionArgs
)
BOOK -> {
val id = uri.lastPathSegment?.toLongOrNull() ?: return 0
database?.update(
BooksContract.TABLE_BOOKS,
values,
"${BooksContract._ID} = ?",
arrayOf(id.toString())
)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}?.let {
context?.contentResolver?.notifyChange(uri, null)
it
} ?: 0
}
override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<String>?
): Int {
return when (uriMatcher.match(uri)) {
BOOKS -> database?.delete(
BooksContract.TABLE_BOOKS,
selection,
selectionArgs
)
BOOK -> {
val id = uri.lastPathSegment?.toLongOrNull() ?: return 0
database?.delete(
BooksContract.TABLE_BOOKS,
"${BooksContract._ID} = ?",
arrayOf(id.toString())
)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}?.let {
context?.contentResolver?.notifyChange(uri, null)
it
} ?: 0
}
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
BOOKS -> BooksContract.MIME_BOOKS
BOOK -> BooksContract.MIME_BOOK
else -> null
}
}
}在 Manifest 中注册:
xml
<provider
android:name=".BooksContentProvider"
android:authorities="com.example.provider.books"
android:exported="false"
android:grantUriPermissions="true" />9. 面试考点
9.1 基础题
Q1: ContentProvider 的作用是什么?
A: ContentProvider 用于在应用间安全地共享数据,提供统一的数据访问接口,支持权限控制和跨进程通信。
Q2: ContentProvider 的四大核心方法?
A:
- query(): 查询数据,返回 Cursor
- insert(): 插入数据,返回 URI
- update(): 更新数据,返回更新行数
- delete(): 删除数据,返回删除行数
Q3: UriMatcher 的作用?
A: UriMatcher 用于解析和匹配 ContentProvider 的 URI,根据匹配结果返回不同的代码,便于进行相应的数据处理。
9.2 进阶题
Q4: ContentObserver 如何使用?
A: ContentObserver 用于监听 ContentProvider 的数据变化,在数据增删改时自动通知观察者,实现数据同步。
Q5: 如何保护 ContentProvider 的访问权限?
A:
- 设置 android:exported 控制是否跨进程访问
- 使用 android:permission 设置访问权限
- 使用 path-permission 设置路径级别权限
- 在代码中手动检查权限
Q6: ContentProvider 跨进程返回的 Cursor 有什么特殊性?
A: 跨进程返回的是 CursorWindow,不能直接修改,需要使用 copy() 方法转换为本地 Cursor。
9.3 高级题
Q7: ContentProvider 的实现原理?
A: ContentProvider 通过 Binder 实现跨进程通信,客户端通过 ContentResolver 发送请求,服务端 ContentProvider 接收请求并操作数据源。
Q8: 如何高效地批量插入数据?
A:
- 使用 bulkInsert() 方法
- 使用 Transaction 事务
- 实现 BulkInsertCallback 接口
Q9: ContentProvider 和直接数据库访问的区别?
A:
- ContentProvider 提供统一接口,直接数据库访问暴露实现细节
- ContentProvider 支持跨进程,数据库访问仅限本进程
- ContentProvider 有权限控制,数据库访问无权限控制
总结
ContentProvider 是 Android 数据共享的核心机制,掌握要点:
- 理解 URI 的作用和结构
- 掌握 CRUD 操作的实现
- 熟练使用 UriMatcher 进行 URI 匹配
- 使用 ContentObserver 监听数据变化
- 注意跨进程访问的安全性和性能
- 了解常用系统 ContentProvider 的使用