为什么ContentResolver调用bulkInsert批量插入数据失败

做Android开发的朋友肯定对使用ContentProvider插入数据并不陌生,通常我们使用ContentProvider基本都是经历如下两个步骤:

  • 声明定义ContentProvider及其相关的URI,编写Provider中对应的增删改查方法;
  • 使用ContentResolver及其对应的URI来对ContentProvider进行增删改查操作;

对于使用ContentProvider进行插入操作,分别可以使用insert、bulkInsert两个API接口,前者用于单条数据插入操作,后者则更适合批量数据插入操作,简单的了解了一遍ContentProvider的相关知识后,来看看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void addOrUpdateContacts(Context context, Collection<ContactStruct> contacts) {
if (contacts == null || contacts.isEmpty()) {
return;
}
final int kCount = contacts.size();
ContentValues[] valuesArray = new ContentValues[kCount];
int pos = 0;
for (ContactStruct contact : contacts) {
ContentValues values = new ContentValues();
values.put(ContactTable.COLUMN_UID, contact.uid);
values.put(ContactTable.COLUMN_NAME, contact.name);
values.put(ContactTable.COLUMN_PHONE, contact.phone);
values.put(ContactTable.COLUMN_PINYIN, contact.pinyin);
values.put(ContactTable.COLUMN_REMARK, contact.remark);
// add "REPLACE" flag
values.put(ContactProvider.SQL_INSERT_OR_REPLACE, true);
valuesArray[pos++] = values;
}
try {
int ret = context.getContentResolver().bulkInsert(ContactProvider.Contact.CONTENT_URI, valuesArray);
if (ret != kCount) {
Log.e(Log.TAG_DATABASE, "addOrUpdateContacts partial failed, succ:" + ret + ",total:" + kCount);
}
} catch (Exception e) {
e.printStackTrace();
}
}

其传入contacts是一组联系人信息数据,通过一个循环的转换成ContentValues类型的数组,再使用context.getContentResolver().bulkInsert将ContentValues数组插入到数据库中去。简单的了解这段代码的作用后,大家觉得这种写法是否存在问题呢?

这里不卖关子,这段代码逻辑上并没有问题的,但实现上忘了考虑contacts的大小问题,如果contacts的元素数量足够多(假设有10000个元素,每个元素有200个Byte左右),则转换后的ContentValues数组也是相当大的,这时候再使用bulkInsert去插入数据,返回成功插入的数据为0,意味着我们的插入操作并没有生效。没有生效的原因在于一次性插入的数据过大,由于ContentProvider底层数据通信是采用了Binder,而关于Binder的文档也提到了

The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all transactions in progress for the process.

所以,在批量插入数据的时候我们需要注意处理这个大小限制的问题,如果数据量过大,会导致Binder报TransactionTooLargeException。简单改良后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void addOrUpdateContacts(Context context, Collection<ContactStruct> contacts) {
if (contacts == null || contacts.isEmpty()) {
return;
}
List<ContentValues> buffer = new LinkedList<>();
for (ContactStruct contact : contacts) {
ContentValues values = new ContentValues();
values.put(ContactTable.COLUMN_UID, contact.uid);
values.put(ContactTable.COLUMN_NAME, contact.name);
values.put(ContactTable.COLUMN_PHONE, contact.phone);
values.put(ContactTable.COLUMN_PINYIN, contact.pinyin);
values.put(ContactTable.COLUMN_REMARK, contact.remark);
values.put(ContactProvider.SQL_INSERT_OR_REPLACE, true);// add "REPLACE" flag
buffer.add(values);
if (buffer.size() == 1024) { //Buffer每满1024条数据,就往数据库批量插入一次,不要一次性插入非常大的数组,底层的Binder有数据限制,会报Exception导致插入失败
ContentValues[] bufferDataArray = new ContentValues[buffer.size()];
buffer.toArray(bufferDataArray);
int successRow = context.getContentResolver().bulkInsert(ContactProvider.Contact.CONTENT_URI, bufferDataArray);
if (successRow != bufferDataArray.length) {
Log.e(Log.TAG_DATABASE, "addOrUpdateContacts buffer data failed, success row:" + successRow);
} else {
Log.i(Log.TAG_DATABASE, "addOrUpdateContacts success!");
buffer.clear();
Log.i(Log.TAG_DATABASE, "addOrUpdateContacts clear buffer");
}
}
}
if (buffer.size() > 0){//缓冲区还有数据
ContentValues[] bufferDataArray = new ContentValues[buffer.size()];
buffer.toArray(bufferDataArray);
int successRow = context.getContentResolver().bulkInsert(ContactProvider.Contact.CONTENT_URI, bufferDataArray);
if (successRow != bufferDataArray.length) {
Log.e(Log.TAG_DATABASE, "addOrUpdateContacts flush buffer data failed, success row:" + successRow);
} else {
Log.i(Log.TAG_DATABASE, "addOrUpdateContacts flush success!");
buffer.clear();
}
}
}

思路是将大量的数据进行拆分,分批批量插入,避免超过Binder传递数据缓冲区的限制问题。实际开发中,这个问题比较容易被忽略,主要原因有以下几个:

  • App的用户群体还不足够庞大,不能触发此问题;
  • 只看到ContentProvider插入数据,没有联想到底层Binder缓冲区的限制;
  • 即使超过Binder缓冲区的限制,bulkInsert也不会报TransactionTooLargeException;

之所以不会报TransactionTooLargeException是因为ContentResolver内部做了RemoteException的捕获消化,并直接return 0,且并没有log输出

感觉并不是一个很好的设计,反而把潜在的问题藏得更深了,本文便是最近处理应用某个线上问题的一点记录,如果你的应用也没有考虑以上描述的问题,不妨可以考虑处理一下。

本文为技术视界原创作品,转载请注明原文出处:http://blog.coderclock.com/2017/04/29/android/why_use_contentresolver_bulkinsert_return_0 ,欢迎关注我的微信公众号:技术视界

推荐文章