联系人

首先需要说明的是,Android系统中的联系人的存储并不是仅仅是一张表。信息存储分为了不同的表,可以按表访问,同时其设计人员为应用开发人员提供了视图模式。

下图是通讯录的表结构:

查看联系人SQLITE表结构流程方法博客链接

在做通讯录相关开发之前,首先要添加联系人相关权限
< uses-permission android:name=”android.permission.READ_CONTACTS” />
< uses-permission android:name=”android.permission.WRITE_CONTACTS” />

数据库的存储结构:

  • 在联系人数据库中,保存的都是一些小的数据表,即与把所有数据保存成一个表不同,它会对联系人的资料模块化,然后分成多个表保存。
  • android已经替我们准备好了,它在数据库里面建了一些视图,视图就是虚拟表。
  • 联系人的数据库比较复杂,在联系人相关应用开发中,一般也不直接通过数据库字段来操作,主要用视图(指定的Uri)来操作。
  • android也提供了很多接口,通过ContentResolver().query方法,传入不同的URI即可访问相应的数据集。

一个联系人信息的存储

  • 在联系人数据库里面联系人和电话号码是分别存在两个表里面的,因为存在一个联系人拥有几个号码的情况,所以android为联系人和手机号码分别单独创建了相应的视图。
  • 联系人信息的视图里面只保存与联系人相关的资料,例如姓名,是否有手机号码等。
  • 手机号码资料则是每一个电话号码为一条记录,如果有一个联系人有3个号码,则里面会出现3个该联系人的记录,号码分别为他的三个号码。

不同视图URL

如果是需要读取联系人信息,使用的URI为:ContactsContract.Contacts.CONTENT_URI
如果是需要读取手机号码信息, 使用的URI为:ContactsContract.CommonDataKinds.Phone.CONTENT_URI

ContentResolver.query Method解析

ContentResolver.query函数的原型,query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

  • projection:是需要读取的字段
  • selection:是数据检索的条件
  • selectionArgs:是数据检索条件的参数
  • sortOrder:是排序的字段

解释一下:
假如一条sql语句如下:select * from anyTable where var=’const’
那么anyTable就是uri,*就是projection,selection是“var=?”,selectionArgs写成这样:new String[]{‘const‘}至于最后一个就简单了,就是排序方式。

只读取自己需要的信息,减少读取的信息,可以减少读取时间


public static List<FriendItem> getContactsMultiPhoneNumber(Context context) {
    //定义常量,节省重复引用的时间
    Uri CONTENT_URI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
    String CONTACT_ID = ContactsContract.CommonDataKinds.Phone.CONTACT_ID;
    String NUMBER = ContactsContract.CommonDataKinds.Phone.NUMBER;
    String ID = ContactsContract.Contacts._ID;
    String DISPLAY_NAME = ContactsContract.Contacts.DISPLAY_NAME;
    String HAS_PHONE_NUMBER = ContactsContract.Contacts.HAS_PHONE_NUMBER;
    //临时变量
    String contactId;
    String displayName;
    Cursor phoneCursor;
    //生成ContentResolver对象
    ContentResolver contentResolver = context.getContentResolver();
    // 获取手机联系人
    Cursor cursor = contentResolver.query(Uri.parse("content://com.android.contacts/contacts"), null, null, null, null);
    List<FriendItem> friendList = new ArrayList<>();
    // 无联系人直接返回
    if (!cursor.moveToFirst()) {//moveToFirst定位到第一行
        return null;
    }
    do {
        // 获得联系人的ID:String类型  列名--》列数--》列内容
        contactId = cursor.getString(cursor.getColumnIndex(ID));
        // 获得联系人姓名:String类型
        displayName = cursor.getString(cursor.getColumnIndex(DISPLAY_NAME));
        // 查看联系人有多少个号码,如果没有号码,返回0
        int phoneCount = cursor.getInt(cursor.getColumnIndex(HAS_PHONE_NUMBER));

        FriendItem item;
        List<String> phoneList = new ArrayList<>();
        if (phoneCount <= 0) {
            continue;
        } else if (phoneCount == 1) {
            //仅有一个联系号码
            phoneCursor = contentResolver.query(CONTENT_URI, null, CONTACT_ID + "=" + contactId, null, null);
            if (!phoneCursor.moveToFirst()) {
                continue;
            }               phoneList.add(StringUtil.removeBlank(phoneCursor.getString(phoneCursor.getColumnIndex(NUMBER))));
            item = new FriendItem(displayName, phoneList, false);
        } else {
            // 有多个联系号码
            phoneCursor = contentResolver.query(CONTENT_URI, null, CONTACT_ID + "=" + contactId, null, null);
            if (!phoneCursor.moveToFirst()) {
                continue;
            }
            do {
                phoneList.add(StringUtil.removeBlank(
                        phoneCursor.getString(phoneCursor.getColumnIndex(NUMBER))
                ));
            } while (phoneCursor.moveToNext());
            item = new FriendItem(displayName, phoneList, false);
        }
        item.setMainPhoneNumber(phoneList.get(0));
        friendList.add(item);
    } while (cursor.moveToNext());
    return friendList;
}

上述代码所实现的效果是读取联系人姓名,手机号(如果有多个手机号,将多个手机号都读出来)。因为联系人信息和手机号信息是在不同的数据库表中,所以先通过Contact视图读出用户信息,然后再在phoneNumber视图中读出手机号。思路很自然。该代码经过实际测试,1000个联系人耗时约为20秒左右,实际使用中效率无法满足需求。
对上述代码的优化过程:

流程拆分和优化

将上面的读取信息的两个过程拆分开,与用户界面结合。初始显示所有用户时不显示手机号,只显示用户名,当用户列表中某个用户名被选中时,再去寻找该用户对应的手机号。这样就节省了大量无用用户手机号的读取时间。经过实际测试,经过这样处理之后,1000个联系人的预读取时间在300ms左右,单独获取手机号的时间可以不在用户能够感知到的范围。

获取简略用户列表

public static List<FriendItem> getBriefContactInfor(Context context) {
    //定义常量,节省重复引用的时间
    Uri CONTENT_URI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
    String CONTACT_ID = ContactsContract.CommonDataKinds.Phone.CONTACT_ID;
    String NUMBER = ContactsContract.CommonDataKinds.Phone.NUMBER;
    String ID = ContactsContract.Contacts._ID;
    String DISPLAY_NAME = ContactsContract.Contacts.DISPLAY_NAME;
    String HAS_PHONE_NUMBER = ContactsContract.Contacts.HAS_PHONE_NUMBER;
    //临时变量
    String contactId;
    String displayName;
    //生成ContentResolver对象
    ContentResolver contentResolver = context.getContentResolver();
    // 获取手机联系人
    Cursor cursor = contentResolver.query(Uri.parse("content://com.android.contacts/contacts"), null, null, null, null);
    List<FriendItem> friendList = new ArrayList<>();
    // 无联系人直接返回
    if (!cursor.moveToFirst()) {//moveToFirst定位到第一行
        return null;
    }
    do {
        // 获得联系人的ID:String类型  列名--》列数--》列内容
        contactId = cursor.getString(cursor.getColumnIndex(ID));
        // 获得联系人姓名:String类型
        displayName = cursor.getString(cursor.getColumnIndex(DISPLAY_NAME));
        // 查看联系人有多少个号码,如果没有号码,返回0
        int phoneCount = cursor.getInt(cursor.getColumnIndex(HAS_PHONE_NUMBER));
        FriendItem item;
        item = new FriendItem(displayName, null, false);
        item.setContactId(contactId);
        item.setPhoneCount(phoneCount);
        friendList.add(item);
    } while (cursor.moveToNext());

    for (int i = 0; i < friendList.size(); i++) {
        friendList.get(i).setPinyin(PinYin.getPinYin(friendList.get(i).getName()).toLowerCase());
    }
    Collections.sort(friendList);
    return friendList;
}

获取当个用户的手机号

//根据cotact_id来获取该联系人的手机号
public static FriendItem getDetailFromContactID(Context context, FriendItem item) {
    Uri CONTENT_URI = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
    String CONTACT_ID = ContactsContract.CommonDataKinds.Phone.CONTACT_ID;
    String NUMBER = ContactsContract.CommonDataKinds.Phone.NUMBER;

    Cursor phoneCursor;
    ContentResolver contentResolver = context.getContentResolver();
    List<String> phoneList = new ArrayList<>();
    if (item.getContactId() == null) {
        return item;
    }
    phoneCursor = contentResolver.query(CONTENT_URI, null, CONTACT_ID + "=" + item.getContactId(), null, null);
    if (!phoneCursor.moveToFirst()) {
        return item;
    }
    do {
        String temp = StringUtil.normalizePhone(phoneCursor.getString(phoneCursor.getColumnIndex(NUMBER)));
        if (temp != null) {
            phoneList.add(temp);
        }
    } while (phoneCursor.moveToNext());

    item.setPhoneNumber(phoneList);
    return item;
}
预读取和修改监控

考虑到通讯录的实际变动比较少,可以先对通讯录的信息进行预先读取之后,自行保存下来,或者在app登录时预先读取(有一定用户隐私的争议,这里是个思路)。在每次实际的使用前,先检查,当前的通讯录与保存下来的通讯录之间有没有信息改动。检测的过程是,在raw_contacts表中有version字段。在预读取时将通讯录的所有用户以及version保存下来,然后使用通论录信息时,获取当前用户以及version字段,相互对比,即可获取新增,删除和修改信息。然后更新修改信息。
相关资料:

  • 监控通讯录更改信息

附录:

raw_contacts表结构:
data表结构: