SimpleCursorAdapter 查联系人没数据?Codex 走 TaoToken 对 ContentResolver 排查
2026/9/18 16:22:57 网站建设 项目流程

ListView 摆好了,getContentResolver().query()也写了,SimpleCursorAdapter 一 set 上去,界面却是一整片空白,logcat 里连个红字都不给——这类问题最难受的地方在于它不崩。想让 Codex 帮忙逐项对照,第一步不是把代码丢进去,而是先打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册账号、创建一把 API Key,再把 Codex 的 Base URL 指向 https://taotoken.net/api。TaoToken 在这里只干一件事:给你一把可用的 Key 和一条稳定的兼容通道,它替不了你查联系人,Cursor 里有没有数据、SimpleCursorAdapter 的 from/to 有没有对齐,还是得靠代码本身说清楚。

1. ListView 一片空白:先判断是 Cursor 空还是 SimpleCursorAdapter 映射错

1.1 复现:getContentResolver().query 到底返回了什么

原文第 2 部分的写法很典型:先拿People.CONTENT_URI去查,把结果 Cursor 交给 SimpleCursorAdapter,再把People.NAME映射到android.R.id.text1。这条链路只有三个环节,任何一个断掉,ListView 都会安静地空着。

Cursor cursor = getContentResolver().query( People.CONTENT_URI, new String[] { People._ID, People.NAME }, null, null, null); SimpleCursorAdapter adapter = new SimpleCursorAdapter( this, android.R.layout.simple_list_item_1, cursor, new String[] { People.NAME }, new int[] { android.R.id.text1 }, 0); ListView listView = findViewById(R.id.contact_list); listView.setAdapter(adapter);

把这段跑起来之后,按下面三个数字分情况看,基本能定位到问题在哪一层:

  • cursor == null:查询本身被拒绝,通常是权限没给,或者 URI 在当前系统版本上不可用。
  • cursor != null && cursor.getCount() == 0:查询通了,但没有匹配的行,问题在 URI、selection 或者设备上确实没有联系人数据。
  • cursor.getCount() > 0但界面还是空:数据已到手,问题在 SimpleCursorAdapter 的 from/to 或布局。

ContentResolver在这里的角色更像一个图书检索台:你递一张索书单(URI + projection + selection),它给你一张检索结果单(Cursor)。结果单是空的,可能是你没办借书证(权限),也可能是书名写错了(列名),不一定是检索台坏了。先分清是哪一种,再决定改哪里。

1.2 整理一份能直接贴给 Codex 的材料

只贴一段query()代码,Codex 只能猜。要让它给出有价值的对照,材料最好包含四样东西:完整的查询与适配代码、AndroidManifest.xml里声明的权限、运行时权限申请的那几行、以及 logcat 中打印出来的getCount()getColumnNames()

在动手整理之前,先把 Key 准备好。打开 TaoToken 注册并创建 API Key,记下模型广场里当时列出的模型 ID——这个 ID 后面要填进 Codex 的配置文件,别凭印象写。TaoToken 只提供 Key 和统一接入通道,不替你执行联系人查询,这一点先说明白,后面问问题的方式也会更顺。

2. 让 Codex 走 TaoToken:~/.codex/config.toml 里改 base_url

2.1 创建 API Key 并在模型广场确认模型 ID

Key 的创建入口在控制台,具体地址是 TaoToken 控制台 API Keys,点击新建之后复制出来,本文一律用YOUR_API_KEY占位。模型 ID 不要自己拼,去模型广场看当时列表里有哪些可选值,写配置时直接照抄。原因是各家模型命名规则不一样,随手加个日期后缀,请求就会以 404 的形式回来,排查起来反而绕远。

2.2 config.toml 可复制片段

Codex 这套工具读的是~/.codex/config.toml,注意不要把 Anthropic 的环境变量套过来,它认的是model_providerbase_url这一组。下面是一份可直接改占位符使用的片段:

model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"

Base URL 末尾不要加/v1,这是最容易被顺手补上去的一段。环境变量里放 Key:

export TAOTOKEN_API_KEY=YOUR_API_KEY

改完保存,重新开一个终端让环境变量生效。如果之前启动过 Codex 会话,退出后重开一次,避免旧配置还在内存里。

2.3 最小验证:先让它解释一段 ContentResolver 代码

在正式拿联系人代码去问之前,先发一条低成本的验证消息,比如把上面那段query()粘进去,问它每个参数分别是什么含义、返回空 Cursor 可能的原因有哪些。能正常返回文字,说明 Key、模型 ID、Base URL 三件套都通了。这一步过不去,后面问再细的问题也是白费。

验证通过之后,再让它按 §1.2 的四样材料逐项对照。这里有个习惯值得养成:先让 Codex 输出一份检查清单,再让它针对每一项给出"看到什么现象说明是这个问题"。这样你手上就有了一份可执行的自查表,而不是一堆泛泛的建议。

3. 对照 query 的四个参数:People.CONTENT_URI 为什么返回空 Cursor

3.1 READ_CONTACTS 权限:清单文件写了不等于运行时给了

Android 6.0 之后,<uses-permission android:name="android.permission.READ_CONTACTS" />只是声明,真正读取联系人还需要运行时申请。很多人照着老教程把清单权限一写就以为完事,结果在真机上查询直接返回空。

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.READ_CONTACTS }, REQ_READ_CONTACTS); }

把这段加进去,并且把onRequestPermissionsResult的结果打印出来。注意不要在权限还没回调的时候就执行查询,否则第一次进页面永远是空列表,用户手动给了权限之后也不会自动刷新——这种"第二次进来就有数据"的现象,八成就是这个顺序问题。

3.2 projection 里的列名和 People.NAME 对不上

query()的第二个参数是投影列,它和 SimpleCursorAdapter 里from数组必须来自同一套列名。原文用的是People.NAME,这是早期android.provider.Contacts体系里的常量。问题在于,不同系统版本上同一个 URI 暴露出来的列集合并不完全一样,而People.CONTENT_URI这套 API 在老版本之后基本已经不作为推荐入口。

一个实际的判断方法:把cursor.getColumnNames()的结果打印出来,和People.NAME的字面值比对。如果getColumnNames()里根本没有这一列,SimpleCursorAdapter 拿不到数据,界面自然空白,而且不会抛异常。

3.3 selection 与 sortOrder 写错,Cursor 有表无数据

第三个参数 selection 相当于 WHERE 条件,第四个是 orderBy。写错条件最典型的后果是getCount()返回 0,但查询本身没有报错。排查时先用null, null试一遍,确认不加条件能查出数据,再把条件一项一项加回去。

第五个参数 sortOrder 一般不会导致空结果,但排序字段名写错在某些实现上会直接让查询失败。稳妥的做法是:排序字段也取自投影里出现过的列名,而不是凭记忆写。

4. SimpleCursorAdapter 的 from/to 与 android.R.id.text1 对齐检查

4.1 from 和 to 数量、顺序必须一致

SimpleCursorAdapter 的两个数组是成对的:from里第 i 个列名,会被塞进to里第 i 个控件。数量不一致会直接抛异常,顺序不一致则更隐蔽——界面有行、内容却是另一列的值,或者干脆是空白。

new String[] { People.NAME }, // from new int[] { android.R.id.text1 }, // to

这里android.R.id.text1来自android.R.layout.simple_list_item_1这个系统布局,两者是配套的。如果你换成了自己的 item 布局,就必须把to改成自己布局里那个 TextView 的 id,系统 id 在新布局里不存在,结果就是有行无字。

4.2 Cursor 没有 moveToFirst 时适配器拿不到行

这一点在 CursorAdapter 体系里其实已经被处理了,但自己写 Cursor 遍历逻辑时经常忘。凡是手写循环读 Cursor 的地方,先moveToFirst()再读;不判断返回值就直接getString(),拿到的是空字符串而不是报错。这也是"看起来有数据、读出来却是空"的一个来源。

4.3 People.CONTENT_URI 已过时:ContactsContract 下的等价写法

如果目标设备版本比较高,把查询入口换到ContactsContract.CommonDataKinds.Phone,列名也换成对应的常量,整个链路会稳定很多:

Cursor cursor = getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[] { ContactsContract.CommonDataKinds.Phone._ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME }, null, null, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC");

注意这里投影里的_ID必须保留,SimpleCursorAdapter 需要它作为行标识。把DISPLAY_NAME对应到android.R.id.text1,其余逻辑不变。这样改完之后,再回头看原文那套People.NAME的写法,就能理解为什么同一份代码在不同设备上表现不一致。

5. 排障清单:ContactsAdapter 仍然空白时按这个顺序查

5.1 打印 getCount() 与 getColumnNames()

第一步永远是打日志,别急着改代码。把cursor.getCount()Arrays.toString(cursor.getColumnNames())打印出来,再配合adapter.getCount()一起看。前者告诉你数据有没有到手,后者告诉你适配器认不认这份数据,两个数字不一样,问题就落在中间那层映射上。

第二步,把权限申请结果也打印出来。真机调试时,用户点了"拒绝"却不自知的情况很常见。

5.2 让 Codex 逐项对照,而不是让它替你执行

这里要划一条线:Codex 能做的,是读你贴的代码和日志、指出哪一项可能不对、给出改法;它不能替你在模拟器上运行程序,也不能替你去读设备上的联系人。诊断 SQL、编译运行、在真机上复现,这些都得你自己在本地做完,再把结果贴回对话。把这条分工搞清楚,问出来的答案会实在很多。

提问时给的信息越具体越好,比如:"这是 query 的四个参数、这是 getColumnNames() 的输出、这是 getCount() 的值,请判断问题出在哪一层,并给出三种可能的原因和对应的验证方法。"这种问法比"为什么没数据"有用得多。

5.3 换一种问法拿到可执行的修复清单

如果第一轮回答太宽泛,可以追问:"请按 ContentResolver 查询、权限、投影列名、SimpleCursorAdapter 映射四层,各给一条验证命令或代码片段。"这种约束式追问,能让它输出一份能照着做的清单,而不是一段综述。跑通之后,你还可以继续追问fromto的长度是否必须严格相等、换用自定义布局时to应该怎么改,把这一块彻底吃透。

6. 跑通之后去对一下这次调用

代码能出数据了,顺手回控制台看一眼这次调用有没有记上账,也能确认 Key 有没有被别的地方误用。用同一把 Key 打开 TaoToken 模型对话 发一条测试消息,模型 ID 和 Base URL 是否填对,一试就知道。

如果后面打算把这类排查变成日常动作,可以看一眼 Coding Plan 是否够用;Key 随时可以在 控制台 API Keys 里新建或轮换。邀请同事一起排查时,把~/.codex/config.toml那段配置和自己的查询日志一起发过去,比只说"ListView 没数据"能省下好几轮来回。

联系人列表这件事本身不复杂,难的是它不报错。把 Cursor、权限、投影列名、from/to 映射这四层分开验证,再让 Codex 逐项对照,空白列表基本就没什么藏身之处了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询