📚 概述
这个教程将帮助你理解 Android 中的ContentProvider和ContentResolver,它们是 Android 四大组件之一,用于实现应用间的数据共享。
🎯 学习目标
- 理解 ContentProvider 和 ContentResolver 的作用
- 学会创建和注册 ContentProvider
- 学会使用 ContentResolver 访问数据
- 理解 Uri 的作用和格式
🔍 核心概念
1. ContentProvider(内容提供者)
作用:暴露应用数据给其他应用访问
类比:就像一个餐厅的服务员,负责提供食物(数据)给客人(其他应用)
位置:在 chapter06 模块中(数据提供方)
2. ContentResolver(内容解析器)
作用:访问 ContentProvider 提供的数据
类比:就像一个客人,通过服务员(ContentProvider)点餐(获取数据)
位置:在 chapter07 模块中(数据访问方)
3. Uri(统一资源标识符)
作用:定位要访问的数据,类似网址
格式:content://授权标识/数据路径/ID
示例:
content://com.example.chapter06.provider.ShoppingCarProvider/shoppingcar ↓ ↓ 授权标识(类似域名) 数据路径
🏗️ 实现步骤
第一步:创建 ContentProvider(在 chapter06 中)
我已经为你创建了ShoppingCarProvider.java,它包含以下核心方法:
1.onCreate()- 初始化
@Override public boolean onCreate() { // 初始化数据库访问对象 shoppingCarDao = MyApplication.shoppingDatabase.shoppingCarDao(); return true; }2.query()- 查询数据
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // 根据 Uri 返回对应的数据 // 返回值是 Cursor(游标),包含查询结果 }3.insert()- 插入数据
@Override public Uri insert(Uri uri, ContentValues values) { // 插入数据到数据库 // 返回新插入数据的 Uri }4.update()- 更新数据
@Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // 更新数据 // 返回受影响的行数 }5.delete()- 删除数据
@Override public int delete(Uri uri, String selection, String[] selectionArgs) { // 删除数据 // 返回删除的行数 }第二步:注册 ContentProvider(在 AndroidManifest.xml 中)
在chapter06/src/main/AndroidManifest.xml中添加:
<provider android:name=".provider.ShoppingCarProvider" android:authorities="com.example.chapter06.provider.ShoppingCarProvider" android:exported="true" />关键属性说明:
android:name:Provider 的类名android:authorities:授权标识(必须全局唯一)android:exported="true":允许其他应用访问
第三步:声明包可见性(Android 11+ 必需)
重要提示:从 Android 11 (API 30) 开始,应用需要显式声明要访问的其他应用!
在chapter07/src/main/AndroidManifest.xml中添加:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Android 11+ 包可见性声明(必需!) 如果不添加,会报错:Failed to find provider info --> <queries> <!-- 方式1:通过包名声明要访问的应用 --> <package android:name="com.example.chapter06" /> <!-- 方式2:通过 provider 的 authorities 声明(更精确)--> <provider android:authorities="com.example.chapter06.provider.ShoppingCarProvider" /> </queries> <application> ... </application> </manifest>关键点:
<queries>标签必须放在<application>标签之前- 两种声明方式可以同时使用,也可以只用一种
- 这是 Android 11 引入的隐私和安全特性
第四步:使用 ContentResolver 访问数据(在 chapter07 中)
在ProviderActivity.java中,我已经实现了三个示例方法:
1. 查询所有数据 -readAllShoppingCar()
// 1. 获取 ContentResolver ContentResolver contentResolver = getContentResolver(); // 2. 定义 Uri Uri uri = Uri.parse("content://com.example.chapter06.provider.ShoppingCarProvider/shoppingcar"); // 3. 查询数据 Cursor cursor = contentResolver.query(uri, null, null, null, null); // 4. 遍历结果 while (cursor.moveToNext()) { int id = cursor.getInt(cursor.getColumnIndex("_id")); String name = cursor.getString(cursor.getColumnIndex("name")); // ... 读取其他字段 } // 5. 关闭游标 cursor.close();2. 插入数据 -insertTestData()
// 1. 准备数据 ContentValues values = new ContentValues(); values.put("name", "测试商品"); values.put("price", 99.99f); values.put("count", 1); // 2. 插入数据 Uri newUri = contentResolver.insert(uri, values);3. 查询单条数据 -queryShoppingCarById()
// 1. 构建包含 ID 的 Uri Uri queryUri = Uri.withAppendedPath(uri, "1"); // 2. 查询数据 Cursor cursor = contentResolver.query(queryUri, null, null, null, null); // 3. 读取第一条数据 if (cursor.moveToFirst()) { String name = cursor.getString(cursor.getColumnIndex("name")); }📊 数据流程图
┌─────────────────────────────────────────────────────────────┐ │ 数据共享流程 │ └─────────────────────────────────────────────────────────────┘ chapter06 模块(数据提供方) chapter07 模块(数据访问方) ┌──────────────────────┐ ┌──────────────────────┐ │ ShoppingDatabase │ │ ProviderActivity │ │ (购物车数据库) │ │ │ └──────────┬───────────┘ └──────────┬───────────┘ │ │ ↓ ↓ ┌──────────────────────┐ ┌──────────────────────┐ │ ShoppingCarProvider │ │ ContentResolver │ │ (ContentProvider) │ │ (内容解析器) │ │ │ │ │ │ - onCreate() │ │ - query() │ │ - query() │ ←───── Uri ──────────│ - insert() │ │ - insert() │ │ - update() │ │ - update() │ │ - delete() │ │ - delete() │ │ │ └──────────────────────┘ └──────────────────────┘ ↑ ↑ │ │ 在 AndroidManifest.xml 中注册 添加 <queries> 标签声明 授权标识:xxx.ShoppingCarProvider (Android 11+ 必需)
🔑 关键知识点
1. Uri 的作用
Uri 就像数据的"地址",告诉系统要访问哪里的数据。
格式:content://授权标识/数据路径/ID(可选)
示例:
content://com.example.chapter06.provider.ShoppingCarProvider/shoppingcar → 访问所有购物车数据 content://com.example.chapter06.provider.ShoppingCarProvider/shoppingcar/1 → 访问 ID=1 的购物车数据
2. Cursor(游标)
Cursor 类似于一个指针,用于遍历查询结果。
常用方法:
moveToNext():移动到下一行,返回 true 表示还有数据moveToFirst():移动到第一行getColumnIndex("列名"):获取列的索引getInt(索引):获取整数值getString(索引):获取字符串值close():关闭游标,释放资源
使用示例:
Cursor cursor = contentResolver.query(uri, null, null, null, null); while (cursor.moveToNext()) { int id = cursor.getInt(cursor.getColumnIndex("_id")); String name = cursor.getString(cursor.getColumnIndex("name")); } cursor.close();3. ContentValues
ContentValues 用于存储要插入或更新的数据,类似于 HashMap。
使用示例:
ContentValues values = new ContentValues(); values.put("name", "商品名称"); // 键值对 values.put("price", 99.99f); values.put("count", 1);4. UriMatcher(Uri 匹配器)
UriMatcher 用于匹配不同的 Uri,判断客户端请求的是什么数据。
定义匹配规则:
private static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { // 匹配:content://.../shoppingcar uriMatcher.addURI(AUTHORITY, "shoppingcar", SHOPPING_CAR_ALL); // 匹配:content://.../shoppingcar/1(# 表示数字) uriMatcher.addURI(AUTHORITY, "shoppingcar/#", SHOPPING_CAR_ITEM); }使用匹配器:
int match = uriMatcher.match(uri); switch (match) { case SHOPPING_CAR_ALL: // 查询所有数据 break; case SHOPPING_CAR_ITEM: // 查询单条数据 break; }🎮 如何测试
第一步:准备数据(在 chapter06 中)
- 运行 chapter06 模块
- 在
GoodsList页面中添加一些商品到购物车 - 这些数据会保存在
ShoppingDatabase中
第二步:访问数据(在 chapter07 中)
- 运行 chapter07 模块
- 点击"读取 chapter06 的购物车数据"按钮
- 你会看到从 chapter06 读取的购物车数据
第三步:测试其他功能
- 点击"向 chapter06 插入测试数据":插入一条测试数据
- 点击"查询 ID=1 的数据":查询单条数据
- 再次点击"读取购物车数据":查看插入的数据
💡 常见问题
1. 为什么会报 "Failed to find provider info" 错误?(重要!)
错误信息:
Failed to find provider info for com.example.chapter06.provider.ShoppingCarProvider
原因:Android 11 (API 30) 及以上版本的包可见性限制
解决方法: 在chapter07/src/main/AndroidManifest.xml中添加<queries>标签:
<queries> <package android:name="com.example.chapter06" /> <provider android:authorities="com.example.chapter06.provider.ShoppingCarProvider" /> </queries>详细说明:
- Android 11 之前:应用可以自由查询其他应用
- Android 11 及以后:必须显式声明要访问的应用
- 目的:增强用户隐私和应用安全性
2. 为什么查询不到数据?
可能原因:
- ❌ chapter06 应用未安装(必须先安装)
- ❌ chapter06 从未运行过(至少运行一次,初始化数据库)
- ❌ ContentProvider 未在 AndroidManifest.xml 中注册
- ❌ Uri 写错了(授权标识或路径错误)
- ❌ chapter06 的购物车数据库中没有数据
- ❌缺少
<queries>标签(Android 11+)
解决方法:
- 先安装并运行 chapter06,添加购物车数据
- 检查 chapter06 的 AndroidManifest.xml 中的
<provider>配置 - 确认 Uri 的授权标识和 Provider 中定义的一致
- 确保 chapter07 的 AndroidManifest.xml 中有
<queries>标签
3. 为什么会报 SecurityException?
可能原因:
- ContentProvider 的
android:exported设置为false - 需要权限但没有声明
解决方法:
- 在 chapter06 的 AndroidManifest.xml 中设置
android:exported="true"
4. ContentProvider 闪退:NullPointerException
错误信息:
NullPointerException: Attempt to invoke virtual method '...shoppingCarDao()' on a null object reference
原因:ContentProvider 的onCreate()在 Application 的onCreate()之前调用
解决方法:使用延迟初始化
private ShoppingCarDao getShoppingCarDao() { if (shoppingCarDao == null) { MyApplication app = (MyApplication) getContext().getApplicationContext(); shoppingCarDao = app.getShoppingDB().shoppingCarDao(); } return shoppingCarDao; }5. Room 和 ContentProvider 有什么区别?
Room:
- 用于应用内部的数据库操作
- 更简单、更高效
- 不能跨应用访问
ContentProvider:
- 用于跨应用的数据共享
- 更复杂,需要转换数据格式(Entity → Cursor)
- 可以让其他应用访问你的数据
🎓 学习总结
数据提供方(chapter06)需要做的:
- ✅ 创建 ContentProvider 类
- ✅ 实现 query、insert、update、delete 方法
- ✅ 在 AndroidManifest.xml 中注册 Provider
- ✅ 定义 Uri(授权标识 + 数据路径)
数据访问方(chapter07)需要做的:
- ✅添加
<queries>标签(Android 11+ 必需) - ✅ 获取 ContentResolver 实例
- ✅ 定义要访问的 Uri(与提供方一致)
- ✅ 调用 ContentResolver 的方法访问数据
- ✅ 处理返回的 Cursor 数据
📝 代码位置
chapter06(数据提供方)
- ContentProvider:
chapter06/src/main/java/com/example/chapter06/provider/ShoppingCarProvider.java - AndroidManifest:
chapter06/src/main/AndroidManifest.xml(注册 Provider) - 数据库:
ShoppingDatabase和ShoppingCarDao
chapter07(数据访问方)
- Activity:
chapter07/src/main/java/com/example/chapter07/ProviderActivity.java - 布局:
chapter07/src/main/res/layout/activity_provider_activety.xml - AndroidManifest:
chapter07/src/main/AndroidManifest.xml(包含<queries>标签)
🚀 下一步
现在你可以:
- 运行 chapter07 模块,测试数据访问功能
- 查看日志(Logcat),观察数据交互过程
- 尝试修改代码,添加更新和删除功能
- 理解 ContentProvider 的工作原理
⚠️ 重要提醒
Android 版本差异
| Android 版本 | API Level | 是否需要<queries> | 说明 |
|---|---|---|---|
| Android 10 及以下 | ≤ 29 | ❌ 不需要 | 可以自由访问其他应用 |
| Android 11 及以上 | ≥ 30 | ✅ 必需 | 必须添加<queries>标签 |
完整配置清单
chapter06(数据提供方)
<application> <!-- 注册 ContentProvider --> <provider android:name=".provider.ShoppingCarProvider" android:authorities="com.example.chapter06.provider.ShoppingCarProvider" android:exported="true" /> </application>chapter07(数据访问方)
<!-- Android 11+ 必需 --> <queries> <package android:name="com.example.chapter06" /> <provider android:authorities="com.example.chapter06.provider.ShoppingCarProvider" /> </queries> <application> <!-- Activities --> </application>祝学习顺利!