资讯动态

Android 读取手机联系人实战:用 READ_CONTACTS + ContentResolver 打通 ContactsContract

发布时间:2026/9/29 13:27:18 来源:尧图企业网站定制
1. 从一次真机崩溃说起Android 读取手机联系人到底卡在哪很多人第一次写 Android 读取手机联系人代码照着示例敲完在模拟器上跑得好好的一装到真机就出问题要么列表空空如也要么直接抛SecurityException闪退要么查询返回的 Cursor 是 null。我试过最典型的一次是同事把READ_CONTACTS只写进了AndroidManifest.xml忘了 Android 6.0 之后必须运行时动态申请结果用户点“读取联系人”按钮App 当场挂掉。这个场景的核心链路其实就四步清单里声明READ_CONTACTS权限、运行时动态申请、用ContentResolver查询ContactsContract、解析返回的Uri和 Cursor 拿到姓名与号码。听起来简单但每一步都有坑。比如ContactsContract.CommonDataKinds.Phone.CONTENT_URI和ContactsContract.Contacts.CONTENT_URI查出来的字段不一样前者直接带号码后者还得再查一次再比如同一个联系人存了三个号码用 Phone 表查就会返回三行姓名重复出现。这篇文章面向的是刚接触 Android 数据读取的开发者或者被权限和 Cursor 折磨过的同学。我会把可复制的权限配置、查询代码骨架、真机验证步骤都写清楚最后再对照几个真实报错讲排查思路。你跟着做基本能一次跑通。如果你在本地调试时想顺手对比一下模型对这类代码的解释可以用 TaoToken 的模型对话功能把报错贴进去问地址是 https://taotoken.net/api 配合接入文档看会更顺。先说清楚一个概念ContentResolver是 Android 里跨应用访问数据的“中介”联系人数据存在系统的 Contacts Provider 里你的 App 不能直接读数据库文件只能通过ContentResolver.query()这个统一入口去查。ContactsContract则是一组契约类定义了联系人相关的表名、列名和 Uri。理解这两者的分工后面写代码就不会迷路。2. 前置准备TaoToken 接入与 Android 工程环境确认在动手写联系人读取之前先把两件事准备好一个是 Android 工程本身的环境另一个是调试过程中用来辅助排查的 TaoToken 接入。这里不是让你把 TaoToken 塞进 App 里而是当你在写代码、看报错、查字段含义时有个能快速问答和验证的通道。Android 侧你需要确认compileSdk建议 34 及以上minSdk至少 23因为运行时权限是 23 引入的Android Studio 用较新的稳定版即可。真机调试比模拟器靠谱因为模拟器默认联系人库是空的你得手动加几个联系人才能验证。真机上一般自带通讯录验证更真实。TaoToken 这边如果你要在本地用命令行工具或者编辑器插件辅助写代码需要拿到 API Key。进入控制台创建密钥https://taotoken.net/api-keys 然后按接入文档配置 Base URL 和 Model IDhttps://taotoken.net/doc 。Base URL 填https://taotoken.net/api注意不要带多余的路径。Model ID 按你选的模型填比如做代码解释和报错分析选一个擅长代码的就行。如果你打算长期用 AI 辅助 Android 开发比如让它帮你生成 Cursor 解析样板、解释ContactsContract字段可以考虑 Coding Planhttps://taotoken.net/coding-plan 适合持续编码场景。Claude Code 用户走 Anthropic 兼容入口https://taotoken.net/claude-code-anthropic 。这些配置和联系人读取本身是解耦的只是让你在踩坑时有个能问的地方。工程侧还要注意一点AndroidManifest.xml里的权限声明只是“声明”不代表“授予”。从 Android 6.0 开始危险权限必须运行时申请用户点了允许才算真正拿到。READ_CONTACTS就属于危险权限所以清单声明和动态申请缺一不可。很多人只做了一半这就是闪退的根源。3. 可复制配置AndroidManifest 权限声明与运行时申请代码这一节给你可以直接抄的配置。先看清单文件在manifest标签下、application之前加权限声明manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.contactsdemo uses-permission android:nameandroid.permission.READ_CONTACTS / application android:allowBackuptrue android:labelContactsDemo android:themestyle/Theme.AppCompat.Light activity android:name.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifest注意READ_CONTACTS是危险权限光写这一行不够。接下来是运行时申请。推荐用registerForActivityResult这套新 API比老的onRequestPermissionsResult更清晰public class MainActivity extends AppCompatActivity { private static final int REQ_CONTACTS 1001; private final ActivityResultLauncherString requestPermissionLauncher registerForActivityResult(new ActivityResultContracts.RequestPermission(), granted - { if (granted) { readContacts(); } else { Toast.makeText(this, 未授予联系人权限, Toast.LENGTH_SHORT).show(); } }); Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.btn_read).setOnClickListener(v - checkAndRequest()); } private void checkAndRequest() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) PackageManager.PERMISSION_GRANTED) { readContacts(); } else { requestPermissionLauncher.launch(Manifest.permission.READ_CONTACTS); } } }如果你用的是 Kotlin逻辑一样只是语法更短。这里的关键点是先checkSelfPermission判断已授权就直接读没授权才launch申请。不要一上来就申请用户体验差而且部分定制系统会拦截。再给一个settings.gradle和build.gradle里需要确认的片段避免依赖缺失// app/build.gradle android { compileSdk 34 defaultConfig { minSdk 23 targetSdk 34 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.core:core:1.12.0 }androidx.core提供了ContextCompat和ActivityResultContracts别漏了。配置到这一步权限链路就完整了清单声明 运行时申请 结果回调。4. 查询 ContactsContractContentResolver 代码骨架与真机验证权限拿到后核心就是查询。先定义 Uri再调ContentResolver.query()然后遍历 Cursor。下面这段是可以直接跑的骨架private void readContacts() { ListString result new ArrayList(); Uri uri ContactsContract.CommonDataKinds.Phone.CONTENT_URI; String[] projection { ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER }; ContentResolver resolver getContentResolver(); Cursor cursor null; try { cursor resolver.query(uri, projection, null, null, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME ASC); if (cursor ! null) { int nameIdx cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME); int numberIdx cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.NUMBER); while (cursor.moveToNext()) { String name cursor.getString(nameIdx); String number cursor.getString(numberIdx); result.add(姓名: name ;电话: number); } } } catch (SecurityException e) { Log.e(Contacts, 权限异常, e); } finally { if (cursor ! null) { cursor.close(); } } Toast.makeText(this, 联系人条数: result.size(), Toast.LENGTH_LONG).show(); for (String s : result) { Log.d(Contacts, s); } }几个要点。第一projection明确指定要查的列比传 null 更高效也避免拿到一堆用不上的字段。第二getColumnIndex要在循环外取一次循环内反复取会拖慢速度。第三Cursor 用完必须close()放在finally里最稳否则容易内存泄漏。第四排序用DISPLAY_NAME ASC让结果按姓名排方便核对。真机验证步骤先在手机通讯录里手动加三个联系人其中一个存两个号码。然后安装 App点按钮第一次会弹权限框选“允许”。看 Logcat 里Contacts标签的输出应该能看到姓名和号码。注意那个存了两个号码的联系人会输出两行姓名相同、号码不同这是 Phone 表的正常行为因为它是“每个号码一行”。如果你想只拿每个联系人一行可以改用ContactsContract.Contacts.CONTENT_URI查_ID和DISPLAY_NAME再根据_ID去 Phone 表查号码。但那样代码更长初学阶段先用 Phone 表跑通更直观。验证时如果 Logcat 里一条都没有先确认手机通讯录里真的有联系人再看权限是否真的授予了。5. 常见报错排查SecurityException、Cursor 为 null 与字段读不到这一节对照几个真实报错讲。第一个java.lang.SecurityException: Permission Denial: reading com.android.providers.contacts。这个几乎都是权限没到位。检查三处清单里有没有READ_CONTACTS、运行时有没有申请、用户是不是点了拒绝。如果用户勾了“不再询问”下次launch会直接回调 false你得引导用户去系统设置里手动开。可以用shouldShowRequestPermissionRationale判断但别过度设计先保证基础流程对。第二个cursor为 null。query()返回 null 通常意味着 Uri 写错了或者 Provider 不可用。确认你用的是ContactsContract.CommonDataKinds.Phone.CONTENT_URI而不是自己拼的字符串。另外如果projection里的列名写错某些设备会抛IllegalArgumentException而不是返回 null所以列名要从ContactsContract常量里取别手写字符串。第三个getColumnIndex返回 -1然后getString(-1)抛CursorIndexOutOfBoundsException。这说明你查的列不在 projection 里。比如你 projection 只放了DISPLAY_NAME和NUMBER却去取_ID就会 -1。解决办法是 projection 和取值列一一对应或者用getColumnIndexOrThrow让错误更早暴露。第四个OAuth 或本地代理相关报错比如local proxy failed、401 Unauthorized。这类一般出现在你用 AI 工具辅助调试、配置 Base URL 或 Key 时。检查https://taotoken.net/api是否写对Key 是否过期Model ID 是否匹配。如果是 Claude Code 场景确认走的是 Anthropic 兼容入口。这些和联系人读取本身无关但调试时容易混在一起分开看就行。还有一个隐蔽的坑部分国产 ROM 对联系人权限做了额外限制即使你申请了READ_CONTACTS也可能需要用户在系统“权限管理”里单独开“读取联系人”。遇到查不到数据但权限显示已授予去系统设置里翻一下。6. 继续深入把联系人读取接进你的实际项目跑通基础链路后你可以按需扩展。比如把结果做成 RecyclerView 列表加搜索框过滤或者把号码做格式化。再进一步如果要做批量导入、去重、和云端同步逻辑会复杂不少这时候用 AI 辅助梳理字段和边界条件会省时间。需要验证模型对某段 Cursor 代码的解释可以用模型对话https://taotoken.net/api 把代码和报错一起贴进去。实际项目里我建议把联系人读取封装成一个 Repository 类权限申请留在 Activity 或 Fragment查询逻辑独立出来方便测试。Cursor 的关闭、异常捕获、空判断都收在 Repository 里上层只拿ListContact。这样后面换数据源或者加缓存改动面小。最后提醒一句读取联系人属于敏感数据Google Play 上架时需要在隐私政策里说明用途并且最好做“最小必要”读取别一次性把全部联系人上传。技术上跑通是一回事合规使用是另一回事。你先把这篇的代码在真机上跑一遍遇到报错按第 5 节对照排查基本就稳了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑