4. ContentProvider 基础实现

本章核心目标

理解并掌握 Android 中 ContentProvider 的基本实现模式。你将学会如何继承 ContentProvider 类,在 onCreate() 中完成初始化,实现标准的 CRUD(增、删、改、查)操作骨架,以及配置 URI 匹配器(UriMatcher)来处理不同资源的请求。这是后续与 Room 集成、实现跨进程数据共享的基础。

本章所有代码均基于 Android 10+,IDE 推荐使用 Android Studio 2023+。ContentProvider 必须在 AndroidManifest.xml 中注册才能生效。

4.1 实现流程概览

创建一个自定义 ContentProvider 需要以下步骤:

  1. 新建类继承 ContentProvider,覆写必要方法。
  2. 在 onCreate() 中完成数据库或提供者初始化。
  3. 定义 URI 匹配规则,实例化 UriMatcher。
  4. 实现 query(), insert(), update(), delete(), getType() 骨架。
  5. 在 AndroidManifest.xml 中声明 provider。
ContentProvider 实现流程 继承 ContentProvider onCreate() 初始化 配置 UriMatcher CRUD 骨架实现 Manifest 注册 说明: • 必须搭配 URI 权限声明 • getType() 需返回MIME类型 • 多线程安全需自行保证

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/itemsCODE_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:authoritiescom.example.app.provider唯一标识,用于构造 URI 的 authority 部分
android:exportedtrue允许其他应用访问(跨进程必备)
android:readPermissioncom.example.app.READ_ITEMS读操作权限(可选,可增强安全)
android:writePermissioncom.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