批量操作与事务:ContentProviderOperation、批量应用、事务回滚
说实话,刚接触 ContentProvider 的时候,我总觉得它就是个「数据中转站」。直到有一次,我在一个通讯录同步项目里,需要一次性插入 500 条联系人数据。你猜怎么着?一条一条 insert,耗时 40 多秒,用户直接卡到想摔手机。
嗯,从那天起,我就彻底明白了——批量操作不是锦上添花,而是刚需。
为什么需要批量操作?
先想一个问题:每次调用 ContentProvider 的 insert/update/delete,都会触发一次跨进程通信(IPC)。如果你有 100 条数据要插入,那就得跑 100 次 IPC。每次 IPC 都要序列化、反序列化、权限检查……这开销,想想都心疼。
我在项目中遇到过最夸张的情况:一个第三方 App 通过 ContentProvider 批量写入日志,每次只写一条,结果 UI 线程直接卡死。后来改成批量操作,性能提升了 20 倍。
说白了,批量操作的核心就两个:
- 减少 IPC 次数:一次调用搞定多条数据
- 保证事务性:要么全成功,要么全回滚
ContentProviderOperation 登场
Android 官方早就想到了这个问题。它提供了 ContentProviderOperation 这个类,专门用来构建批量操作请求。
我个人习惯把它理解成「操作说明书」——你不需要亲自去执行每条 SQL,只需要把「我要做什么」写清楚,然后交给 ContentProvider.applyBatch() 去统一执行。
基本用法
// 构建批量操作列表
ArrayList<ContentProviderOperation> operations = new ArrayList<>();
// 插入一条数据
operations.add(ContentProviderOperation.newInsert(URI)
.withValue("name", "张三")
.withValue("phone", "13800138000")
.build());
// 更新一条数据
operations.add(ContentProviderOperation.newUpdate(URI)
.withSelection("_id = ?", new String[]{"1"})
.withValue("name", "李四")
.build());
// 删除一条数据
operations.add(ContentProviderOperation.newDelete(URI)
.withSelection("_id = ?", new String[]{"2"})
.build());
// 批量执行
ContentProviderResult[] results = getContentResolver()
.applyBatch(AUTHORITY, operations);
你看,代码结构非常清晰。每个操作都是独立的 ContentProviderOperation 对象,最后统一提交。
批量插入的实战技巧
我记得有一次做数据迁移,需要把本地数据库的 2000 条记录同步到 ContentProvider 里。一开始我用了循环 insert,跑了将近 2 分钟。改成批量操作后,只用了 3 秒。
这里有个关键点:批量操作的大小要控制。不是越多越好。
| 批量大小 | 耗时(2000条) | 内存占用 |
|---|---|---|
| 单条插入 | ~120秒 | 低 |
| 每次100条 | ~8秒 | 中 |
| 每次500条 | ~3秒 | 高 |
| 一次性2000条 | ~2.5秒 | 极高(可能OOM) |
我个人建议:每次批量 100~200 条。既能保证速度,又不会撑爆内存。如果你要插入上万条数据,可以分批次提交,每批之间稍微 sleep 一下,给系统喘口气。
事务回滚:出了问题怎么办?
批量操作最怕什么?怕执行到一半挂了。比如你插入了 50 条数据,第 51 条出错了,那前面 50 条怎么办?
嗯,这里就要靠事务了。
ContentProvider 的 applyBatch() 默认是在一个事务里执行的。也就是说,只要有一条操作失败,整个批次都会回滚。
关键点:事务回滚的前提是 ContentProvider 的实现正确支持了事务。如果你自己写 ContentProvider,记得在 applyBatch() 方法里开启事务。
在 ContentProvider 中支持事务
@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
db.beginTransaction();
try {
ContentProviderResult[] results = super.applyBatch(operations);
db.setTransactionSuccessful();
return results;
} finally {
db.endTransaction();
}
}
这段代码看着简单,但坑不少。我曾经犯过一个错误:忘了调用 setTransactionSuccessful(),结果所有操作都回滚了。排查了半天才发现是这个问题。
注意:beginTransaction() 和 endTransaction() 必须成对出现。如果中间抛出异常,endTransaction() 会自动回滚。只有调用了 setTransactionSuccessful(),才会提交。
依赖操作:后一步依赖前一步的结果
有时候,你需要先插入一条数据,拿到它的 _id,然后再用这个 _id 去插入关联表。这种场景在通讯录、订单系统里很常见。
ContentProviderOperation 提供了 withValueBackReference() 方法,专门解决这个问题。
// 第一步:插入联系人
ContentProviderOperation op1 = ContentProviderOperation.newInsert(ContactsURI)
.withValue("name", "王五")
.build();
// 第二步:插入电话号码,依赖第一步生成的 _id
ContentProviderOperation op2 = ContentProviderOperation.newInsert(PhoneURI)
.withValueBackReference("contact_id", 0) // 0 表示引用第一个操作的结果
.withValue("number", "13900139000")
.build();
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
ops.add(op1);
ops.add(op2);
ContentProviderResult[] results = getContentResolver()
.applyBatch(AUTHORITY, ops);
// results[0] 是插入联系人的结果
// results[1] 是插入电话号码的结果
long contactId = results[0].uri != null
? ContentUris.parseId(results[0].uri)
: -1;
这里要注意:withValueBackReference() 的第二个参数是索引,指向 operations 列表中的第几个操作。索引从 0 开始。
小技巧:如果你有多个依赖关系,可以用 withValueBackReference() 多次调用。但别搞得太复杂,否则代码可读性会下降。我一般控制在 3 层依赖以内。
批量操作的性能对比
为了让你更直观地感受批量操作的优势,我放一张对比图。这是我之前在一个项目里实际测试的数据。
从图上可以清楚看到:单条插入耗时 90 秒,而批量 500 条只需要 3 秒。这差距,不是一星半点。
避坑指南
最后,分享几个我踩过的坑:
- 不要在主线程调用 applyBatch():批量操作可能耗时较长,一定要放到子线程。我曾经在主线程执行 500 条批量插入,直接 ANR。
- 注意 ContentProvider 的线程安全:如果你的 ContentProvider 被多个进程访问,批量操作期间要加锁,否则可能出现数据不一致。
- 批量操作不是万能的:如果每条数据都需要复杂的业务逻辑判断,那批量操作反而会降低灵活性。这时候可以考虑用「小批量 + 循环」的方式。
- 记得处理 OperationApplicationException:
applyBatch()会抛出这个异常,通常是操作数量不匹配或者依赖关系出错。捕获后要仔细看异常信息。
我曾经在一个项目里,因为忘了处理 OperationApplicationException,导致批量操作失败后没有任何提示,数据丢失了都不知道。从那以后,我每次都会加上 try-catch,并且在 catch 里记录详细日志。
好了,批量操作和事务的内容就讲到这里。记住一句话:能用批量,就别单条。这是提升 ContentProvider 性能最直接、最有效的手段。
公众号:蓝海资料掘金营,微信deep3321