6、添加联系人:构建ContentValues、插入ContactsContract.RawContacts、插入Data表数据、事务处理与批量插入
好,我们直接进入正题。添加联系人,说白了就是把一个“人”的信息拆成两大部分,塞进系统联系人数据库里。你想想看,一个联系人可能有姓名、电话、邮箱、地址……这些数据在 Android 系统里不是一股脑儿存一张表的,而是分成了 RawContacts 和 Data 两张核心表。
我个人习惯把 RawContacts 理解成“联系人外壳”,它只记录这个联系人是属于哪个账户的(比如 Google 账号、手机本地账号)。而真正的数据,比如“张三”、“13800138000”,全部存在 Data 表里。嗯,这里要注意:必须先插入 RawContacts,拿到它的 _id,才能往 Data 表里写数据。
1. 构建一个空的 ContentValues,插入 RawContacts → 获取 rawContactId
2. 用同一个 rawContactId,构建多条 ContentValues,分别插入 Data 表
3. 用事务包裹整个过程,保证要么全部成功,要么全部回滚
6.1 构建 ContentValues:把数据装进“键值对”
ContentValues 是什么?说白了就是一个 Map,key 是列名,value 是你要存的值。我刚开始做联系人开发时,总觉得这玩意儿跟 Bundle 差不多,但有一点不同:ContentValues 的 key 必须是 String,value 只能是基本类型或 String。
举个例子,你要插入一条手机号数据:
ContentValues values = new ContentValues();
values.put(Data.RAW_CONTACT_ID, rawContactId);
values.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
values.put(Phone.NUMBER, "13800138000");
values.put(Phone.TYPE, Phone.TYPE_MOBILE);
这里 Data.MIMETYPE 是关键中的关键。它告诉系统:这条 Data 记录是什么类型。是电话?是邮箱?还是姓名?每种类型对应不同的 MIMETYPE 常量。我整理了一张常用表:
| 数据类型 | MIMETYPE 常量 | 常用字段 |
|---|---|---|
| 姓名 | StructuredName.CONTENT_ITEM_TYPE | DISPLAY_NAME, GIVEN_NAME, FAMILY_NAME |
| 手机号 | Phone.CONTENT_ITEM_TYPE | NUMBER, TYPE |
| 邮箱 | Email.CONTENT_ITEM_TYPE | ADDRESS, TYPE |
| 地址 | StructuredPostal.CONTENT_ITEM_TYPE | STREET, CITY, POSTCODE |
6.2 插入 RawContacts:拿到“联系人身份证”
插入 RawContacts 其实很简单,只需要一个空的 ContentValues,但必须指定 ACCOUNT_NAME 和 ACCOUNT_TYPE。如果你不指定,系统会默认用本地账户。我个人习惯显式指定:
ContentValues rawValues = new ContentValues();
rawValues.put(RawContacts.ACCOUNT_NAME, "com.android.contacts");
rawValues.put(RawContacts.ACCOUNT_TYPE, "local");
Uri rawUri = getContentResolver().insert(RawContacts.CONTENT_URI, rawValues);
long rawContactId = ContentUris.parseId(rawUri);
这里返回的 rawContactId 就是后续所有 Data 记录的“外键”。你想想看,没有这个 ID,Data 表里的数据就不知道属于谁。
6.3 插入 Data 表数据:一条一条往里塞
拿到 rawContactId 之后,就可以往 Data 表里插数据了。每条数据都是一个独立的 ContentValues,但必须共享同一个 rawContactId。比如你要插入姓名和电话:
// 插入姓名
ContentValues nameValues = new ContentValues();
nameValues.put(Data.RAW_CONTACT_ID, rawContactId);
nameValues.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
nameValues.put(StructuredName.DISPLAY_NAME, "张三");
getContentResolver().insert(Data.CONTENT_URI, nameValues);
// 插入电话
ContentValues phoneValues = new ContentValues();
phoneValues.put(Data.RAW_CONTACT_ID, rawContactId);
phoneValues.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
phoneValues.put(Phone.NUMBER, "13800138000");
phoneValues.put(Phone.TYPE, Phone.TYPE_MOBILE);
getContentResolver().insert(Data.CONTENT_URI, phoneValues);
嗯,这里有个坑:Data.CONTENT_URI 是同一个,但每条记录的 MIMETYPE 不同,系统会根据 MIMETYPE 解析对应的字段。如果你把电话号码填到了姓名字段里,系统不会报错,但显示出来就是乱的。
6.4 事务处理:要么全有,要么全无
你想想看,如果插入姓名成功了,但插入电话时失败了,这个联系人就成了“只有名字没有电话”的残次品。为了避免这种情况,必须用事务把多次插入包起来。
Android 的 ContentProvider 操作支持 applyBatch 方法,它允许你把多个 ContentProviderOperation 打包成一个事务。我个人强烈推荐这种方式,比手动管理 SQLiteTransaction 要安全得多:
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
// 1. 插入 RawContacts
ops.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
.withValue(RawContacts.ACCOUNT_NAME, "com.android.contacts")
.withValue(RawContacts.ACCOUNT_TYPE, "local")
.build());
// 2. 插入姓名(依赖上一步的 rawContactId)
ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0) // 0 表示引用第1个操作的结果
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
.withValue(StructuredName.DISPLAY_NAME, "张三")
.build());
// 3. 插入电话
ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, 0)
.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
.withValue(Phone.NUMBER, "13800138000")
.withValue(Phone.TYPE, Phone.TYPE_MOBILE)
.build());
// 执行事务
ContentProviderResult[] results = getContentResolver()
.applyBatch(ContactsContract.AUTHORITY, ops);
withValueBackReference 里的第二个参数是操作索引。0 表示引用第0个操作(即插入 RawContacts)返回的 ID。如果你有多个 RawContacts 插入,索引要对应好。我曾经在这里搞错过索引,结果电话数据关联到了错误的联系人上,排查了半天。
6.5 批量插入:一次处理多个联系人
如果你需要一次性导入 100 个联系人,千万别用 for 循环一个个调 insert。那样会触发 100 次 ContentProvider 跨进程调用,性能极差。正确的做法是把所有操作打包到一个 applyBatch 里:
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
int rawContactIndex = 0;
for (ContactData contact : contactList) {
// 插入 RawContacts
ops.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
.withValue(RawContacts.ACCOUNT_NAME, "com.android.contacts")
.withValue(RawContacts.ACCOUNT_TYPE, "local")
.build());
// 插入姓名
ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, rawContactIndex)
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
.withValue(StructuredName.DISPLAY_NAME, contact.name)
.build());
// 插入电话
ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, rawContactIndex)
.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
.withValue(Phone.NUMBER, contact.phone)
.withValue(Phone.TYPE, Phone.TYPE_MOBILE)
.build());
rawContactIndex += 3; // 每个联系人占3个操作(1个RawContacts + 2个Data)
}
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
你想想看,这样一次跨进程调用就搞定了所有联系人,效率提升不是一点半点。我在项目里曾经用这种方式导入 500 个联系人,耗时不到 2 秒,而用循环插入的方式要 30 多秒。
6.6 整体流程示意图
下面这张图帮你理清整个添加联系人的数据流向:
好了,添加联系人的核心内容就这些。说白了就是三步走:构建 ContentValues → 插入 RawContacts 拿 ID → 插入 Data 表。再加上事务和批量操作,你就能高效、安全地往系统联系人数据库里写数据了。