尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SimpleCursorAdapter 查联系人没数据?Codex 走 TaoToken 对 ContentResolver 排查

SimpleCursorAdapter 查联系人没数据?Codex 走 TaoToken 对 ContentResolver 排查 ListView 摆好了getContentResolver().query()也写了SimpleCursorAdapter 一 set 上去界面却是一整片空白logcat 里连个红字都不给——这类问题最难受的地方在于它不崩。想让 Codex 帮忙逐项对照第一步不是把代码丢进去而是先打开 https://taotoken.net/?utm_sourcetaotoken_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_url2.1 创建 API Key 并在模型广场确认模型 IDKey 的创建入口在控制台具体地址是 TaoToken 控制台 API Keys点击新建之后复制出来本文一律用YOUR_API_KEY占位。模型 ID 不要自己拼去模型广场看当时列表里有哪些可选值写配置时直接照抄。原因是各家模型命名规则不一样随手加个日期后缀请求就会以 404 的形式回来排查起来反而绕远。2.2 config.toml 可复制片段Codex 这套工具读的是~/.codex/config.toml注意不要把 Anthropic 的环境变量套过来它认的是model_provider加base_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 chatBase URL 末尾不要加/v1这是最容易被顺手补上去的一段。环境变量里放 Keyexport TAOTOKEN_API_KEYYOUR_API_KEY改完保存重新开一个终端让环境变量生效。如果之前启动过 Codex 会话退出后重开一次避免旧配置还在内存里。2.3 最小验证先让它解释一段 ContentResolver 代码在正式拿联系人代码去问之前先发一条低成本的验证消息比如把上面那段query()粘进去问它每个参数分别是什么含义、返回空 Cursor 可能的原因有哪些。能正常返回文字说明 Key、模型 ID、Base URL 三件套都通了。这一步过不去后面问再细的问题也是白费。验证通过之后再让它按 §1.2 的四样材料逐项对照。这里有个习惯值得养成先让 Codex 输出一份检查清单再让它针对每一项给出看到什么现象说明是这个问题。这样你手上就有了一份可执行的自查表而不是一堆泛泛的建议。3. 对照 query 的四个参数People.CONTENT_URI 为什么返回空 Cursor3.1 READ_CONTACTS 权限清单文件写了不等于运行时给了Android 6.0 之后uses-permission android:nameandroid.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 映射四层各给一条验证命令或代码片段。这种约束式追问能让它输出一份能照着做的清单而不是一段综述。跑通之后你还可以继续追问from和to的长度是否必须严格相等、换用自定义布局时to应该怎么改把这一块彻底吃透。6. 跑通之后去对一下这次调用代码能出数据了顺手回控制台看一眼这次调用有没有记上账也能确认 Key 有没有被别的地方误用。用同一把 Key 打开 TaoToken 模型对话 发一条测试消息模型 ID 和 Base URL 是否填对一试就知道。如果后面打算把这类排查变成日常动作可以看一眼 Coding Plan 是否够用Key 随时可以在 控制台 API Keys 里新建或轮换。邀请同事一起排查时把~/.codex/config.toml那段配置和自己的查询日志一起发过去比只说ListView 没数据能省下好几轮来回。联系人列表这件事本身不复杂难的是它不报错。把 Cursor、权限、投影列名、from/to 映射这四层分开验证再让 Codex 逐项对照空白列表基本就没什么藏身之处了。
返回列表