4. ContentProvider 基础实现
本章核心目标
理解并掌握 Android 中 ContentProvider 的基本实现模式。你将学会如何继承 ContentProvider 类,在 onCreate() 中完成初始化,实现标准的 CRUD(增、删、改、查)操作骨架,以及配置 URI 匹配器(UriMatcher)来处理不同资源的请求。这是后续与 Room 集成、实现跨进程数据共享的基础。
本章所有代码均基于 Android 10+,IDE 推荐使用 Android Studio 2023+。ContentProvider 必须在 AndroidManifest.xml 中注册才能生效。
4.1 实现流程概览
创建一个自定义 ContentProvider 需要以下步骤:
- 新建类继承
ContentProvider,覆写必要方法。 - 在
onCreate()中完成数据库或提供者初始化。 - 定义 URI 匹配规则,实例化
UriMatcher。 - 实现
query(),insert(),update(),delete(),getType()骨架。 - 在 AndroidManifest.xml 中声明 provider。
4.2 继承 ContentProvider 类
新建一个类,例如 com.example.app.provider.ItemProvider,继承自 android.content.ContentProvider。IDE 会自动要求覆写全部抽象方法。
public class ItemProvider extends ContentProvider {
// 数据列常量
public static final String TABLE_NAME = "items";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_DESCRIPTION = "description";
// 授权 & URI 基础
private static final String AUTHORITY = "com.example.app.provider";
private static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
// URI 匹配器
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int CODE_ITEMS = 1; // 匹配整个表
private static final int CODE_ITEM_ID = 2; // 匹配单行
static {
sUriMatcher.addURI(AUTHORITY, TABLE_NAME, CODE_ITEMS);
sUriMatcher.addURI(AUTHORITY, TABLE_NAME + "/#", CODE_ITEM_ID);
}
// 数据库帮助器(占位)
// private AppDatabase mDatabase;
private SQLiteDatabase mDb; // 仅示例,本章未集成Room
@Override
public boolean onCreate() {
// 初始化在单独方法中完成
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 待实现
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 待实现
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// 待实现
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 待实现
return 0;
}
@Override
public String getType(Uri uri) {
// 待实现
return null;
}
}
4.3 onCreate 方法 —— 初始化数据源
onCreate() 在 ContentProvider 首次被访问时调用(线程安全),一般用于初始化数据库或缓存。本阶段我们使用 SQLiteOpenHelper 创建一张简单的表。
public class ItemProvider extends ContentProvider {
// ... 前面常量代码不变 ...
private static class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "item_provider.db";
private static final int DATABASE_VERSION = 1;
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ COLUMN_NAME + " TEXT NOT NULL, "
+ COLUMN_DESCRIPTION + " TEXT DEFAULT ''"
+ ");";
db.execSQL(sql);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
private DatabaseHelper mDbHelper;
@Override
public boolean onCreate() {
// 获取 Context,初始化数据库帮助器
mDbHelper = new DatabaseHelper(getContext());
// 主动创建/打开数据库(可选,但推荐在onCreate时准备)
SQLiteDatabase db = mDbHelper.getWritableDatabase();
return (db != null);
}
}
在
onCreate() 中不要执行耗时超过 5 秒的操作,否则可能引发 ANR。如果数据库较大,请考虑延迟初始化(例如在第一个实际查询时再打开数据库)。4.4 URI 匹配器配置
UriMatcher 用于将外部传入的 URI 解析为预定义的整型码,从而分发到不同的 CRUD 处理逻辑。推荐将 URI 模式添加到静态块中。
| URI 模式 | 匹配码 | 说明 |
|---|---|---|
content://com.example.app.provider/items | CODE_ITEMS (1) | 操作全部记录 |
content://com.example.app.provider/items/# | CODE_ITEM_ID (2) | 操作指定 ID 的记录 |
在 CRUD 方法中,使用 sUriMatcher.match(uri) 获取匹配码并分支:
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int CODE_ITEMS = 1;
private static final int CODE_ITEM_ID = 2;
static {
sUriMatcher.addURI(AUTHORITY, TABLE_NAME, CODE_ITEMS);
sUriMatcher.addURI(AUTHORITY, TABLE_NAME + "/#", CODE_ITEM_ID);
}
// 在 CRUD 方法内部使用:
int match = sUriMatcher.match(uri);
switch (match) {
case CODE_ITEMS:
// 操作整个表
break;
case CODE_ITEM_ID:
// 提取 URI 末尾的数字 ID
long id = ContentUris.parseId(uri);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
4.5 CRUD 方法骨架实现
以下代码展示完整的 CRUD 操作骨架,使用原生 SQLiteDatabase 实现(后续章节将替换为 Room)。
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = mDbHelper.getReadableDatabase();
Cursor cursor;
switch (sUriMatcher.match(uri)) {
case CODE_ITEMS:
cursor = db.query(TABLE_NAME, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case CODE_ITEM_ID:
long id = ContentUris.parseId(uri);
String where = COLUMN_ID + "=?";
String[] whereArgs = new String[]{String.valueOf(id)};
cursor = db.query(TABLE_NAME, projection, where, whereArgs,
null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
// 设置监听 URI 变化的通知
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (sUriMatcher.match(uri) != CODE_ITEMS) {
throw new IllegalArgumentException("Insert only supported on table URI");
}
SQLiteDatabase db = mDbHelper.getWritableDatabase();
long newId = db.insert(TABLE_NAME, null, values);
if (newId > 0) {
Uri itemUri = ContentUris.withAppendedId(BASE_URI, newId);
// 通知变化
getContext().getContentResolver().notifyChange(itemUri, null);
return itemUri;
}
throw new SQLException("Failed to insert row into " + uri);
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
int count;
switch (sUriMatcher.match(uri)) {
case CODE_ITEMS:
count = db.update(TABLE_NAME, values, selection, selectionArgs);
break;
case CODE_ITEM_ID:
long id = ContentUris.parseId(uri);
String where = COLUMN_ID + "=?";
// 组合外部条件
count = db.update(TABLE_NAME, values,
where + (selection != null ? " AND (" + selection + ")" : ""),
mergeArgs(id, selectionArgs));
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
int count;
switch (sUriMatcher.match(uri)) {
case CODE_ITEMS:
count = db.delete(TABLE_NAME, selection, selectionArgs);
break;
case CODE_ITEM_ID:
long id = ContentUris.parseId(uri);
String where = COLUMN_ID + "=?";
count = db.delete(TABLE_NAME,
where + (selection != null ? " AND (" + selection + ")" : ""),
mergeArgs(id, selectionArgs));
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
@Override
public String getType(Uri uri) {
switch (sUriMatcher.match(uri)) {
case CODE_ITEMS:
return "vnd.android.cursor.dir/vnd.com.example.provider.items";
case CODE_ITEM_ID:
return "vnd.android.cursor.item/vnd.com.example.provider.items";
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
// 辅助方法:合并 ID 与原有参数
private String[] mergeArgs(long id, String[] original) {
String[] args = new String[1 + (original != null ? original.length : 0)];
args[0] = String.valueOf(id);
if (original != null) {
System.arraycopy(original, 0, args, 1, original.length);
}
return args;
}
在实际生产代码中,
mergeArgs 方法需要处理 SQL 注入风险,建议使用参数化查询。本章示例仅用于展示骨架,请勿直接用于敏感数据环境。4.6 AndroidManifest.xml 注册
ContentProvider 必须在清单文件中声明,否则外部进程无法发现。注册时需指定 authorities 和 exported 属性。
<provider
android:name=".provider.ItemProvider"
android:authorities="com.example.app.provider"
android:exported="true" <!-- 允许跨进程访问,正式环境按需调整 -->
android:readPermission="com.example.app.READ_ITEMS"
android:writePermission="com.example.app.WRITE_ITEMS" />
| 属性 | 值 | 说明 |
|---|---|---|
android:name | .provider.ItemProvider | 类名(相对于包名) |
android:authorities | com.example.app.provider | 唯一标识,用于构造 URI 的 authority 部分 |
android:exported | true | 允许其他应用访问(跨进程必备) |
android:readPermission | com.example.app.READ_ITEMS | 读操作权限(可选,可增强安全) |
android:writePermission | com.example.app.WRITE_ITEMS | 写操作权限(可选) |
4.7 调试与验证
安装部署后,可以使用 adb 命令直接调用 ContentProvider 来验证:
# 查询所有记录
adb shell content query --uri content://com.example.app.provider/items
# 插入一条记录
adb shell content insert \
--uri content://com.example.app.provider/items \
--bind name:s:"测试物品" \
--bind description:s:"这是一个测试"
# 查询单条
adb shell content query --uri content://com.example.app.provider/items/1
# 更新
adb shell content update \
--uri content://com.example.app.provider/items/1 \
--bind name:s:"已更新名称"
# 删除
adb shell content delete --uri content://com.example.app.provider/items/1
日志输出示例(Tag 为 ContentProvider 或自定义):
D/ItemProvider: query() called with uri=content://com.example.app.provider/items
D/ItemProvider: insert() called, newId=1
D/ItemProvider: delete() called, deleted rows=1
调试阶段建议在 CRUD 方法入口和出口添加日志,便于追踪调用链。同时可利用 Android Studio 的 Database Inspector 实时查看 SQLite 数据。
4.8 常见错误与规避
- URI 不匹配导致 IllegalArgumentException:检查
addURI()中的 authority 和 path 是否与清单一致。 - ContentProvider 无法实例化:确保清单中
android:name路径正确,且类为 public。 - 跨进程权限拒绝:若
exported="true"仍无法访问,检查是否有自定义权限尚未声明。 - 数据库文件未创建:确认
onCreate()中调用了getWritableDatabase()或getReadableDatabase()。 - 通知机制失效:CRUD 操作后必须调用
getContentResolver().notifyChange(),否则监听该 URI 的 CursorLoader 不会刷新。
4.9 性能与稳定性建议
| 关注点 | 建议做法 |
|---|---|
| 数据库连接池 | 使用单例 SQLiteOpenHelper,避免多次创建 |
| 大批量操作 | 使用 applyBatch() 或事务包装 multiple 操作 |
| URI 解析缓存 | UriMatcher 本身是线程安全且高效的,无需额外缓存 |
| 空结果处理 | 始终返回非 null 的 Cursor(例如使用 EMPTY_CURSOR) |
| 跨进程 Binder 限制 | 1MB 总数据限制,大结果集需分页或使用 FileProvider |