3、创建 Content Provider:继承 ContentProvider 类、实现 CRUD 方法、在 Manifest 中注册

好,咱们今天聊聊怎么亲手搭建一个 Content Provider。说实话,很多新手看到 ContentProvider 这个抽象类就头大——四个方法要重写,还得配 UriMatcher,感觉门槛不低。但你别怕,这东西说白了就是个「数据中间人」,你把它拆开看,每一步其实都很清晰。

3.1 继承 ContentProvider 类

第一步,创建一个类,继承 ContentProvider。我习惯先写个空壳子,把 IDEA 提示要重写的方法全部生成出来。你会看到这么几个方法:

public class MyContentProvider extends ContentProvider {

    @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) {
        // 返回 MIME 类型
        return null;
    }
}

嗯,这里要注意:onCreate() 返回 true 表示 Provider 初始化成功。我曾经见过有人在这里返回 false,结果整个 App 启动后 Content Provider 根本不可用,查了半天才发现是这个小坑。

3.2 实现 CRUD 方法

CRUD 是 Content Provider 的核心。说白了,就是让外部应用能通过 URI 来操作你的数据。我个人习惯先定义好 URI 和 UriMatcher,这样后面写起来思路清晰。

3.2.1 定义 URI 和 UriMatcher

public static final String AUTHORITY = "com.example.myapp.provider";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/items");

private static final int ITEMS = 1;
private static final int ITEM_ID = 2;

private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

static {
    uriMatcher.addURI(AUTHORITY, "items", ITEMS);
    uriMatcher.addURI(AUTHORITY, "items/#", ITEM_ID);
}

你想想看,UriMatcher 的作用就是帮你区分「我要操作全部数据」还是「我要操作某一条数据」。比如 content://com.example.myapp.provider/items 代表所有条目,而 content://com.example.myapp.provider/items/3 代表 id 为 3 的那一条。

3.2.2 query 方法实现

query 方法是最常用的。我在项目中遇到过一个问题:外部应用传进来的 projection 可能为 null,这时候你得自己处理,不能直接扔给数据库。

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor cursor;

    switch (uriMatcher.match(uri)) {
        case ITEMS:
            cursor = db.query(TABLE_NAME, projection, selection, selectionArgs,
                    null, null, sortOrder);
            break;
        case ITEM_ID:
            String id = uri.getLastPathSegment();
            cursor = db.query(TABLE_NAME, projection, "_id = ?",
                    new String[]{id}, null, null, null);
            break;
        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    // 设置监听,当数据变化时通知观察者
    cursor.setNotificationUri(getContext().getContentResolver(), uri);
    return cursor;
}
小技巧: 别忘了调用 cursor.setNotificationUri()。如果不设置,外部应用通过 ContentResolver 注册的观察者就收不到数据变更通知。我早期就吃过这个亏,数据改了 UI 不刷新,用户还以为 App 卡死了。

3.2.3 insert 方法实现

@Override
public Uri insert(Uri uri, ContentValues values) {
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    long rowId;

    switch (uriMatcher.match(uri)) {
        case ITEMS:
            rowId = db.insert(TABLE_NAME, null, values);
            break;
        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    if (rowId > 0) {
        Uri resultUri = ContentUris.withAppendedId(CONTENT_URI, rowId);
        // 通知数据变化
        getContext().getContentResolver().notifyChange(resultUri, null);
        return resultUri;
    }

    throw new SQLException("Failed to insert row into " + uri);
}

这里有个细节:insert 方法返回的 URI 要带上新插入行的 ID。这样外部应用拿到返回值后,可以直接用这个 URI 去查询刚插入的数据。说白了,这就是一种「回执」机制。

3.2.4 update 和 delete 方法

@Override
public int update(Uri uri, ContentValues values, String selection,
                  String[] selectionArgs) {
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    int count;

    switch (uriMatcher.match(uri)) {
        case ITEMS:
            count = db.update(TABLE_NAME, values, selection, selectionArgs);
            break;
        case ITEM_ID:
            String id = uri.getLastPathSegment();
            count = db.update(TABLE_NAME, values, "_id = ?",
                    new String[]{id});
            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 = dbHelper.getWritableDatabase();
    int count;

    switch (uriMatcher.match(uri)) {
        case ITEMS:
            count = db.delete(TABLE_NAME, selection, selectionArgs);
            break;
        case ITEM_ID:
            String id = uri.getLastPathSegment();
            count = db.delete(TABLE_NAME, "_id = ?",
                    new String[]{id});
            break;
        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }

    if (count > 0) {
        getContext().getContentResolver().notifyChange(uri, null);
    }
    return count;
}
注意: update 和 delete 方法返回的是受影响的行数。如果返回 0,说明没有数据被修改。外部应用可以根据这个返回值判断操作是否成功。我曾经见过有人把返回值写死成 1,结果外部应用以为数据改了,实际上根本没动——这种 bug 特别隐蔽。

3.3 在 Manifest 中注册

Content Provider 必须在 AndroidManifest.xml 中注册,否则系统根本找不到它。这一步很多人会忘,尤其是从别的组件复制代码时容易漏掉。

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">

    <provider
        android:name=".MyContentProvider"
        android:authorities="com.example.myapp.provider"
        android:exported="true"
        android:multiprocess="false" />

    <!-- 其他组件 -->
</application>

几个关键属性:

  • android:name:Provider 类的完整路径。如果类在子包中,要写全路径,比如 .data.MyContentProvider
  • android:authorities:唯一标识符,通常用包名加后缀。这个值要和代码中定义的 AUTHORITY 一致。
  • android:exported:是否允许其他应用访问。如果设为 false,只有本应用能调用这个 Provider。
  • android:multiprocess:是否在每个进程中创建 Provider 实例。默认 false,所有进程共享一个实例。
核心要点: 注册 Content Provider 时,authorities 必须唯一。如果两个 App 用了相同的 authorities,系统会报错。我建议用「包名 + .provider」这种命名方式,基本不会冲突。

3.4 知识体系总览

下面这张图把创建 Content Provider 的完整流程串起来了。你可以对照着看,每一步都对应着具体的代码实现。

创建 Content Provider 完整流程 1. 继承 ContentProvider 重写 6 个抽象方法 2. 实现 CRUD 方法 query / insert / update / delete 3. Manifest 注册 配置 authorities 等属性 定义 UriMatcher 区分 URI 模式 数据库 CRUD SQLiteDatabase 操作 通知数据变化 notifyChange() 关键注意事项 • authorities 必须唯一,建议用「包名 + .provider」 • 每次数据变更后记得调用 notifyChange()

3.5 避坑指南

最后,我把自己踩过的坑总结一下,你写代码时多留个心眼:

  • UriMatcher 匹配失败: 检查 URI 中的路径和 authorities 是否完全一致。我曾经因为大小写写错,查了整整一个下午。
  • 多线程安全问题: Content Provider 的方法可能被多个线程同时调用。如果你的数据库操作不是线程安全的,记得加同步锁。
  • getType 方法不能返回 null: 这个方法用于返回数据的 MIME 类型。如果返回 null,某些系统组件(比如 SearchView)会崩溃。
  • exported 属性要谨慎: 如果 Provider 只给本应用用,设成 false。暴露给外部应用时,一定要做权限校验。

嗯,创建 Content Provider 其实没那么复杂。你只要把 URI 定义清楚,CRUD 方法写完整,Manifest 注册好,剩下的就是数据操作的具体逻辑了。多写几次,你会发现它就是个「带 URI 路由的数据库封装」而已。


公众号:蓝海资料掘金营,微信deep3321