4、读取联系人列表:查询ContactsContract.Contacts表、获取联系人ID与姓名、使用CursorLoader异步加载、适配器展示数据

好,咱们今天来啃一块硬骨头——读取系统联系人列表。

说实话,这个功能我最早做的时候,踩了不少坑。那时候刚接触Android开发,直接在主线程查数据库,结果界面卡得跟幻灯片似的。后来才明白,联系人数据查询必须异步处理。嗯,这节课咱们就把这件事彻底讲透。

4.1 认识ContactsContract.Contacts表

Android系统把联系人数据存在一个叫ContactsContract的内容提供器里。这里面有很多表,但最核心的就是ContactsContract.Contacts

这张表存的是什么?说白了,就是每个联系人的“概要信息”。每条记录代表一个联系人,包含联系人ID、显示名称、是否有电话号码等基础字段。

字段名 类型 说明
_ID long 联系人唯一标识,后面关联其他表全靠它
DISPLAY_NAME String 联系人显示名称,就是你在通讯录里看到的名字
HAS_PHONE_NUMBER int 0表示无号码,1表示有号码。我经常用它做过滤
PHOTO_URI String 头像URI,可以为空

我个人习惯,每次查询联系人时都会带上HAS_PHONE_NUMBER=1这个条件。为什么?因为很多应用只关心有电话号码的联系人,比如拨号器、短信应用。你想想看,查出一堆没有号码的联系人,展示出来也没意义。

4.2 查询联系人ID与姓名

查询Contacts表其实很简单,就是一次ContentResolver查询。但要注意,我们得用ContentResolver.query()方法,传入正确的URI和投影列。

先看一个基础版本:

// 定义投影列,只查我们需要的字段
String[] projection = {
    ContactsContract.Contacts._ID,
    ContactsContract.Contacts.DISPLAY_NAME,
    ContactsContract.Contacts.HAS_PHONE_NUMBER
};

// 查询条件:只查有电话号码的联系人
String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = ?";
String[] selectionArgs = new String[]{"1"};

// 执行查询
Cursor cursor = getContentResolver().query(
    ContactsContract.Contacts.CONTENT_URI,
    projection,
    selection,
    selectionArgs,
    ContactsContract.Contacts.DISPLAY_NAME + " ASC"  // 按姓名排序
);

if (cursor != null) {
    while (cursor.moveToNext()) {
        long contactId = cursor.getLong(cursor.getColumnIndex(ContactsContract.Contacts._ID));
        String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
        // 这里就可以把数据加到列表里了
    }
    cursor.close();
}

这段代码能跑,但有个大问题——它在主线程执行。我曾经在一个老项目里看到过类似的写法,用户通讯录里有2000多个联系人,结果界面卡了足足3秒。用户直接给了一星差评。

⚠️ 注意:ContentResolver.query()是耗时操作,绝对不能在主线程执行。Android系统从3.0开始,如果检测到主线程执行网络或数据库操作,会直接抛出NetworkOnMainThreadException。虽然联系人查询不会抛这个异常,但ANR的风险是实打实的。

4.3 使用CursorLoader异步加载

那怎么解决异步问题?官方推荐的方式是用CursorLoader。它配合LoaderManager,可以在后台线程查询数据,查询完成后自动把结果送到UI线程。

我个人觉得,CursorLoader的设计思路很优雅。它会在Activity或Fragment的生命周期内自动管理Cursor,不会泄漏,也不会重复查询。你想想看,如果每次屏幕旋转都重新查一遍数据库,那体验得多糟糕。

来看具体实现:

public class ContactListFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {

    private static final int CONTACT_LOADER_ID = 1;
    private SimpleCursorAdapter adapter;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // 初始化Loader
        getLoaderManager().initLoader(CONTACT_LOADER_ID, null, this);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        // 定义查询的URI和投影
        Uri uri = ContactsContract.Contacts.CONTENT_URI;
        String[] projection = {
            ContactsContract.Contacts._ID,
            ContactsContract.Contacts.DISPLAY_NAME,
            ContactsContract.Contacts.HAS_PHONE_NUMBER
        };
        String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER + " = ?";
        String[] selectionArgs = new String[]{"1"};
        String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " ASC";

        // 创建CursorLoader,自动在后台线程执行
        return new CursorLoader(
            getActivity(),
            uri,
            projection,
            selection,
            selectionArgs,
            sortOrder
        );
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        // 数据加载完成,把Cursor交给适配器
        adapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        // Loader被重置时,释放旧的Cursor
        adapter.swapCursor(null);
    }
}

这里有个细节要注意:onLoadFinished是在UI线程回调的,所以你可以放心地更新界面。swapCursor方法会关闭旧的Cursor并换上新数据,非常安全。

💡 小技巧:如果你用的是AndroidX,推荐用CursorLoader的替代方案——LiveData配合Room。但如果是维护老项目,CursorLoader依然是最稳妥的选择。我在一个2015年的项目里,到现在还在用CursorLoader,稳定运行了8年没出过问题。

4.4 适配器展示数据

数据查出来了,怎么展示?最直接的方式是用SimpleCursorAdapter。它能把Cursor里的字段自动映射到ListView或RecyclerView的Item布局上。

先定义Item布局文件contact_list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="12dp">

    <TextView
        android:id="@+id/contact_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/contact_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="12sp"
        android:textColor="#999999" />

</LinearLayout>

然后创建适配器:

// 定义从Cursor到视图的映射
String[] fromColumns = {
    ContactsContract.Contacts.DISPLAY_NAME,
    ContactsContract.Contacts._ID
};
int[] toViews = {
    R.id.contact_name,
    R.id.contact_id
};

// 创建适配器
adapter = new SimpleCursorAdapter(
    getActivity(),
    R.layout.contact_list_item,
    null,  // 初始时没有Cursor,后面通过swapCursor设置
    fromColumns,
    toViews,
    0  // flags,传0即可
);

// 设置到ListView
ListView listView = findViewById(R.id.contact_list);
listView.setAdapter(adapter);

你看,代码量其实不多。但这里有个坑——SimpleCursorAdapter默认情况下,如果Cursor里某个字段为null,它会显示"null"字符串。我曾经因为这个被测试提过bug,说联系人列表里出现了"null"字样。解决方案是自定义一个ViewBinder

adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
    @Override
    public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
        if (view.getId() == R.id.contact_name) {
            String name = cursor.getString(columnIndex);
            if (name == null || name.isEmpty()) {
                ((TextView) view).setText("未知联系人");
            } else {
                ((TextView) view).setText(name);
            }
            return true;  // 表示我们已经处理了这个视图
        }
        return false;  // 其他视图让适配器自己处理
    }
});
📌 核心要点总结:
  • ContactsContract.Contacts表存的是联系人概要信息,核心字段是_ID和DISPLAY_NAME
  • 查询时必须异步,CursorLoader是官方推荐的解决方案
  • SimpleCursorAdapter可以快速实现Cursor到ListView的映射
  • 记得处理null值,避免显示"null"字符串

最后,我画了一张流程图,帮你理清整个数据流向:

联系人列表加载流程图 1. 用户进入页面 2. initLoader() 启动Loader 3. 后台线程查询Contacts表 4. onLoadFinished回调 5. swapCursor更新列表

整个流程其实就五步:用户进入页面 → 初始化Loader → 后台查询 → 回调返回数据 → 更新列表。每一步都有明确的职责,环环相扣。

好了,这节课的内容就到这里。记住,读取联系人列表的核心就是异步加载,千万别在主线程里直接查数据库。CursorLoader虽然看起来代码多一些,但它帮你处理了生命周期、线程切换、Cursor管理等一堆麻烦事,值得你花时间掌握。


公众号:蓝海资料掘金营,微信deep3321