应用场景
项目中存在这样的需求,通过设备之间通过蓝牙传输联系人,并且需要将获取过来的联系人插入到ContactsProvider中
批量插入联系人的标准代码
在Android的源码中,ContactsContract.java中为我们展示了批量插入联系人的方法。
The batch method is by far preferred. It inserts the raw contact and its
constituent data rows in a single database transaction
and causes at most one aggregation pass.
<pre>
ArrayList<ContentProviderOperation> ops =
new ArrayList<ContentProviderOperation>();
...
int rawContactInsertIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
.withValue(RawContacts.ACCOUNT_TYPE, accountType)
.withValue(RawContacts.ACCOUNT_NAME, accountName)
.build());
ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
.withValue(StructuredName.DISPLAY_NAME, "Mike Sullivan")
.build());
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
应用实例
在项目联系人列表一开始是存在如下的一个本地联系人列表中
public class TsContact implements Parcelable {
public String mPhoneNumber;
public int mPhoneType;
public String mFirstName;
public String mLastName;
public String mMiddlename;
...
}
假如List < TsContact> contactsList中存放了联系人列表,那么如何将这个TsContacts列表同步到ContactsProvider中呢?
我一开始采用的代码如下:
private void syncTSContactsToContactsProvider(List<TsContact> contactsList) {
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (int index = 0; index < contactsList.size(); index++) {
TsContact contact = contactsList.get(index);
int rawContactInsertIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.withYieldAllowed(true).build());
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.mLastName)
.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.mFirstName)
.withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.mMiddlename)
.withYieldAllowed(true).build());
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, contact.mPhoneNumber)
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, contact.mPhoneType)
.withValue(ContactsContract.CommonDataKinds.Phone.LABEL, "").withYieldAllowed(true).build());
}
try {
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
ops.clear();
} catch (final TransactionTooLargeException e) {
e.printStackTrace();
} catch (final RemoteException e) {
LogUtil.e(TAG, "RemoteException in commit");
e.printStackTrace();
} catch (final OperationApplicationException e) {
LogUtil.e(TAG, "OperationApplicationException in commit");
e.printStackTrace();
}
}
大功告成,运行成功,很开心。效率也比单个插入高很多。
遇到了TransactionTooLargeException
代码运行了一段时间以后,测试提了一个bug,如果联系人很多的时候,无法更新联系人。
查了一下,当联系人列表过大的时候,比如说超过一千,会抛出异常:TransactionTooLargeException。这是因为我们使用applyBatch接口来插入数据,最终还是需要通过binder将这些数据传递给ContactsProvider。而binder是轻量级跨进程通信机制,其传递数据上限。在Android 5.0中其定义为:
BINDER_VM_SIZE ((1*1024*1024) - (4096 *2))
那么当联系人列表过大,则传递的数据超过了binder所能传递数据的上限,抛出了异常。
怎么解决这个问题呢?那就是把联系人列表截断,分多次传输。
错误的分批批处理
一开始的我想到的是仍然将联系人列表一次性转化为插入操作的列表ArrayList< ContentProviderOperation > ops,再执行applyBatch,如果遇到TransactionTooLargeException,则将插入列表截成两段,重新插入,如果还有异常,继续截成两段,就这样二分下去,肯定可以插入 。恩,是的perfect code。
那么代码就变成了
private void syncTSContactsToContactsProvider(List<TsContact> contactsList) {
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (int index = 0; index < contactsList.size(); index++) {
TsContact contact = contactsList.get(index);
int rawContactInsertIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.withYieldAllowed(true).build());
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.mLastName)
.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.mFirstName)
.withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.mMiddlename)
.withYieldAllowed(true).build());
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, contact.mPhoneNumber)
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, contact.mPhoneType)
.withValue(ContactsContract.CommonDataKinds.Phone.LABEL, "").withYieldAllowed(true).build());
}
int transactionSize = ops.size();
ArrayList<ContentProviderOperation> subOps = new ArrayList<>();
while (!ops.isEmpty()) {
subOps.clear();
if (transactionSize > ops.size()) {
transactionSize = ops.size();
}
subOps.addAll(ops.subList(0, transactionSize));
try {
getContentResolver().applyBatch(ContactsContract.AUTHORITY, subOps);
ops.removeAll(subOps);
} catch (final TransactionTooLargeException e) {
// If the transaction is too large, try splitting it.
if (transactionSize == 1) {
Log.e(TAG, "Single operation transaction too large");
}
Log.d(TAG, "Transaction operation count %d too large, halving..." + transactionSize);
transactionSize = transactionSize / 2;
if (transactionSize < 1) {
transactionSize = 1;
}
} catch (final RemoteException e) {
Log.e(TAG, "RemoteException in commit");
e.printStackTrace();
} catch (final OperationApplicationException e) {
Log.e(TAG, "OperationApplicationException in commit");
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ops.clear();
}
恩,写完很满意,但是当ops被分成几段,多次处理以后,只有第一次的插入是有效,后面的插入都是无效的了。
正确的批处理的插入姿势
之所以ArrayList< ContentProviderOperation > ops被截断 以后的插入,后面的插入都失败了,是因为插入到id已经不对了,应该每次重新构建一个新的ContentProviderOperation列表。
所以正确的处理方式是:每次截取联系人列表的一段,构建一个ContentProviderOperation列表,插入完成以后,取联系人列表的下一段,再重新构建一个新的ContentProviderOperation列表,再次插入
所以正确的代码是这样的
private void syncTSContactsToContactsProvider(List<TsContact> contactsList) {
final int contactsListSize = contactsList.size();
int unitLength = 400; //large insert will cause binder data overflow.
int syncedCount = 0;
while (syncedCount < contactsListSize) {
int syncLength = (contactsListSize - syncedCount) < unitLength ? (contactsListSize - syncedCount) : unitLength;
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (int index = 0; index < contactsList.size(); index++) {
TsContact contact = contactsList.get(index);
int rawContactInsertIndex = ops.size();
ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.withYieldAllowed(true).build());
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, contact.mLastName)
.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, contact.mFirstName)
.withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, contact.mMiddlename)
.withYieldAllowed(true).build());
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, contact.mPhoneNumber)
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, contact.mPhoneType)
.withValue(ContactsContract.CommonDataKinds.Phone.LABEL, "").withYieldAllowed(true).build());
}
try {
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
ops.clear();
} catch (final TransactionTooLargeException e) {
e.printStackTrace();
} catch (final RemoteException e) {
LogUtil.e(TAG, "RemoteException in commit");
e.printStackTrace();
} catch (final OperationApplicationException e) {
LogUtil.e(TAG, "OperationApplicationException in commit");
e.printStackTrace();
}
Log.d(TAG, "" + syncedCount + "contacts has been synced to contacts provider" );
}
}