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

2,621 阅读3分钟

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

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

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

public static void addOrUpdateContacts(Context context, Collection 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。简单改良后的代码如下:

public static void addOrUpdateContacts(Context context, Collection contacts) {
    if (contacts == null || contacts.isEmpty()) {
        return;
    }
    List 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输出

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

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