4、ContentResolver 操作:通过 ContentResolver 查询、插入、更新、删除数据

好,咱们接着聊。上一章我们把 ContentProvider 的底层原理和注册机制讲透了,这一章我带你上手实操——用 ContentResolver 去增删改查。

说白了,ContentResolver 就是客户端和 Provider 之间的桥梁。你作为 App 开发者,大部分时候根本不需要自己写 Provider,但一定会频繁调用 Resolver 去访问系统通讯录、媒体库、日历这些数据。所以这块儿必须吃透。

4.1 获取 ContentResolver 实例

在 Activity 或 Fragment 里,直接调用 getContentResolver() 就行。我习惯在 Application 或 BaseActivity 里提前持有这个实例,避免到处传 Context。

// 在 Activity 中
ContentResolver resolver = getContentResolver();

// 在非 Context 类中,需要传入 Context
ContentResolver resolver = context.getContentResolver();
💡 我的习惯: 在 ViewModel 里不要直接持有 Context,而是通过 Application 类获取 Resolver,避免内存泄漏。

4.2 查询数据(query)

查询是最常用的操作。ContentResolver 的 query() 方法返回一个 Cursor,你可以把它理解成数据库查询结果集的游标。

// 查询通讯录中的联系人
Uri uri = ContactsContract.Contacts.CONTENT_URI;
String[] projection = {
    ContactsContract.Contacts._ID,
    ContactsContract.Contacts.DISPLAY_NAME
};
String selection = ContactsContract.Contacts.DISPLAY_NAME + " LIKE ?";
String[] selectionArgs = new String[]{"%张三%"};
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " ASC";

Cursor cursor = resolver.query(
    uri,          // 内容 URI
    projection,   // 要查询的列(null 表示所有列)
    selection,    // 查询条件(WHERE 子句)
    selectionArgs,// 条件参数
    sortOrder     // 排序
);

if (cursor != null) {
    while (cursor.moveToNext()) {
        long id = cursor.getLong(cursor.getColumnIndexOrThrow(
            ContactsContract.Contacts._ID));
        String name = cursor.getString(cursor.getColumnIndexOrThrow(
            ContactsContract.Contacts.DISPLAY_NAME));
        // 处理数据...
    }
    cursor.close(); // 千万别忘了关闭!
}
⚠️ 我曾经踩过的坑: 忘记关闭 Cursor 导致的内存泄漏。早期项目里有个页面反复查询联系人,结果 OOM 了。后来我强制团队所有 Cursor 操作必须用 try-finally 或 Kotlin 的 use 块。

4.3 插入数据(insert)

插入操作返回新插入行的 URI,里面包含了自动生成的 ID。我个人习惯先构建 ContentValues,再调用 insert。

// 向通讯录插入一条联系人
ContentValues values = new ContentValues();
values.put(ContactsContract.Contacts.DISPLAY_NAME, "李四");
values.put(ContactsContract.Contacts.STARRED, 1); // 标记为星标联系人

Uri newUri = resolver.insert(
    ContactsContract.Contacts.CONTENT_URI,
    values
);

if (newUri != null) {
    long newId = ContentUris.parseId(newUri);
    Log.d("TAG", "插入成功,新ID: " + newId);
}

你想想看,为什么 insert 返回的是 URI 而不是直接返回 ID?因为 Provider 可能对 URI 做了路径映射,返回 URI 更灵活。嗯,这里要注意,有些 Provider 的 insert 实现可能不支持批量插入,需要逐条操作。

4.4 更新数据(update)

更新操作返回受影响的行数。我建议更新前先确认条件是否准确,避免误更新。

// 更新联系人姓名
ContentValues updateValues = new ContentValues();
updateValues.put(ContactsContract.Contacts.DISPLAY_NAME, "李四(已更新)");

String where = ContactsContract.Contacts._ID + " = ?";
String[] whereArgs = new String[]{"123"};

int rowsUpdated = resolver.update(
    ContactsContract.Contacts.CONTENT_URI,
    updateValues,
    where,
    whereArgs
);

if (rowsUpdated > 0) {
    Log.d("TAG", "更新成功,影响行数: " + rowsUpdated);
}
🔑 关键点: update 的 selection 参数和 query 的 selection 语法完全一致。如果你传 null 作为 selection,会更新所有行——这通常不是你想要的。

4.5 删除数据(delete)

删除操作同样返回受影响的行数。我曾经在测试环境里误删了整张表的数据,还好有备份。所以生产环境一定要加条件。

// 删除指定联系人
String deleteWhere = ContactsContract.Contacts._ID + " = ?";
String[] deleteArgs = new String[]{"123"};

int rowsDeleted = resolver.delete(
    ContactsContract.Contacts.CONTENT_URI,
    deleteWhere,
    deleteArgs
);

Log.d("TAG", "删除了 " + rowsDeleted + " 条记录");
⚠️ 安全提醒: 永远不要在 UI 线程执行大批量的 delete 操作。系统 ContentProvider 的删除可能触发级联删除,耗时不可控。建议用 AsyncTask 或协程。

4.6 批量操作(applyBatch)

当需要同时插入、更新多条记录时,用 applyBatch() 可以保证事务性。我一般在同步联系人、批量导入数据时用这个。

// 批量插入两条联系人
ArrayList<ContentProviderOperation> operations = new ArrayList<>();

operations.add(ContentProviderOperation.newInsert(
    ContactsContract.Contacts.CONTENT_URI)
    .withValue(ContactsContract.Contacts.DISPLAY_NAME, "王五")
    .build());

operations.add(ContentProviderOperation.newInsert(
    ContactsContract.Contacts.CONTENT_URI)
    .withValue(ContactsContract.Contacts.DISPLAY_NAME, "赵六")
    .build());

ContentProviderResult[] results = resolver.applyBatch(
    ContactsContract.AUTHORITY,
    operations
);

for (ContentProviderResult result : results) {
    Log.d("TAG", "批量操作结果: " + result.uri);
}

4.7 核心流程一览

下面这张图把 ContentResolver 的完整调用链路画出来了,你一看就明白。

ContentResolver 操作核心流程 App 层(你的代码) ContentResolver.query() / insert() / update() / delete() ContentResolver 内部 通过 URI 匹配 → 查找对应 Provider → 跨进程 IPC 调用 ContentProvider 实现 query() / insert() / update() / delete() 方法被调用 数据源(SQLite / 文件 / 网络) IPC 边界

4.8 常见问题与避坑

问题 原因 解决方案
query 返回 null URI 不匹配或 Provider 未注册 检查 AndroidManifest 中的 authority 是否一致
insert 返回 null Provider 的 insert 实现返回了 null 查看 Provider 源码,确认 URI 匹配逻辑
update 返回 0 条件不匹配或没有权限 检查 selection 参数和权限声明
delete 抛异常 数据库约束或外键冲突 检查 Provider 的异常处理,添加 try-catch
跨进程数据不同步 没有调用 notifyChange Provider 中必须调用 getContext().getContentResolver().notifyChange()
💡 调试技巧: 我经常用 adb shell content query --uri content://xxx 直接在命令行测试 Provider 是否正常工作,比写代码调试快多了。

好了,这一章的内容就这些。ContentResolver 的操作其实不复杂,核心就是记住四个方法加上批量操作。你只要把 URI、投影、条件这几个参数搞明白,剩下的就是熟练度的问题了。

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