ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android 数据库查询中 Cursor 类的使用与 TaoToken 配置实践

Android 数据库查询中 Cursor 类的使用与 TaoToken 配置实践 1. Android 本地 SQLite 查询里 Cursor 到底扮演什么角色如果你写过 Android 本地存储大概率绕不开 SQLite也绕不开 Cursor。很多刚接触的朋友会把它当成一个「结果集对象」但更准确的理解是Cursor 是一个指向查询结果行的游标它本身不存数据而是持有一个可以前后移动的位置指针你通过这个指针去读取当前行每一列的值。这个「游标」的概念其实和数据库里的 cursor 是一脉相承的只是 Android 把它封装成了 Java 对象。它适合谁适合所有需要在 Android 端做本地数据读取的场景比如做一个天气 Demo 时把城市列表存在 SQLite 里启动时查出来填充 Spinner比如做一个离线记账 App把账单按月份查出来做统计再比如做一个本地缓存层把接口返回的 JSON 落到 SQLite下次冷启动直接读库。这些场景的共同点是数据量不大、查询频繁、对启动速度敏感而 Cursor 正好是这套链路的最后一环。我见过不少项目在 Cursor 上翻车问题往往不在「不会用」而在「用完不关」。Cursor 持有底层 SQLite 的窗口资源如果不 close轻则内存上涨重则报CursorWindowAllocationException尤其在列表滚动频繁查询时特别明显。所以这篇文章不只讲怎么取数据还会把「获取—判空—遍历—取值—关闭」这条完整链路讲透并给出一份可以直接复制到项目里的封装代码。另外现在很多 Android 项目会把网络请求的 endpoint 统一收口到自建网关或统一 Key 通道方便做额度管理和日志追踪。本文后半部分会顺带说明如何把项目里的 endpoint 改到 TaoToken 的统一 API 通道让本地数据库查询和远端模型调用各司其职互不干扰。这部分不是必须的但如果你正在做「本地缓存 远端补全」这类混合架构会省不少事。先明确一个边界Cursor 只负责本地 SQLite 的读取它和网络请求没有直接关系。把两者放在一篇文章里是因为很多 Demo 的完整链路是「先查本地库查不到再走远端」理解清楚各自的职责排障时才不会互相甩锅。2. TaoToken 前置准备统一 Key 与 API 通道怎么接在进入 Cursor 代码之前先把「远端通道」这块讲清楚因为后面验证环节会用到。TaoToken 提供的是统一的 API 入口官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。它的定位是把模型调用、Key 管理、额度查看收敛到一个控制台里你不需要在多个平台之间来回切换。前置准备分三步。第一步是拿到 Key进入控制台后创建 API Key建议按项目维度建比如「android-weather-demo」单独一个 Key方便后续按项目看用量。第二步是确认 Base URL所有请求都走https://taotoken.net/api这个前缀具体路径按你调用的能力拼接。第三步是选模型 ID控制台里会列出可用模型复制对应的 Model ID 填到配置里。这里要强调一个常见误区Base URL 和完整请求地址不是一回事。Base URL 是https://taotoken.net/api你在代码里拼接时要注意不要重复加/v1之类的后缀具体以接入文档为准。文档入口在 https://taotoken.net/doc 里面有各语言的示例建议先扫一遍再动手。如果你用的是 Claude Code 这类命令行工具配置方式又不一样需要走 OAuth 或 settings 文件。Claude Code 的接入文档在 https://taotoken.net/ClaudeCodeAnthropic 里面有完整的 settings 片段。Coding Plan 适合长期编码和 Agent 场景入口在 https://taotoken.net/coding-plan 。API Keys 管理页在 https://taotoken.net/api-keys 模型对话调试页在 https://taotoken.net/chat 。把这三样东西记牢Base URL、API Key、Model ID。后面无论你是在 Android 里用 OkHttp 发请求还是在 Cline、Codex 里配 MCP都是围绕这三件套展开的。很多 401 报错追根溯源就是这三者有一个填错了或者 Key 复制时带了空格。3. 可复制配置Cursor 查询封装与 endpoint 收口这一节给两份可直接用的代码。第一份是 Cursor 的查询封装第二份是网络层的 endpoint 配置片段。先看 Cursor 封装。核心思路是把「打开库、查询、遍历、关闭」收进一个方法用 try-finally 保证 close 一定执行。下面这段可以直接放进你的 DbManager 类public ListCity queryAllCities() { ListCity result new ArrayList(); SQLiteDatabase db null; Cursor cursor null; try { db helper.getReadableDatabase(); cursor db.rawQuery(SELECT cityid, cityname FROM citys, null); if (cursor null || !cursor.moveToFirst()) { return result; } int idIndex cursor.getColumnIndex(cityid); int nameIndex cursor.getColumnIndex(cityname); if (idIndex -1 || nameIndex -1) { return result; } do { City city new City(); city.id cursor.getString(idIndex); city.name cursor.getString(nameIndex); result.add(city); } while (cursor.moveToNext()); } finally { if (cursor ! null) { cursor.close(); } if (db ! null) { db.close(); } } return result; }这段代码有几个细节值得说。第一getColumnIndex只调用一次并缓存不要放在循环里反复调虽然开销不大但循环里调用是典型的坏习惯。第二用do-while而不是while因为moveToFirst()已经把指针移到第一行了如果再用while(moveToNext())会漏掉第一行这是新手最容易踩的坑。第三getColumnIndex返回 -1 表示列名不存在必须判断否则getString(-1)会抛异常。再看网络层的 endpoint 配置。如果你用 Gradle 的 buildConfigField 来管理可以在build.gradle里这样写android { defaultConfig { buildConfigField String, API_BASE_URL, \https://taotoken.net/api\ buildConfigField String, API_KEY, \sk-你的Key\ buildConfigField String, MODEL_ID, \你的ModelID\ } }如果你更习惯用 properties 文件可以在项目根目录建local.properties然后读取taotoken.base.urlhttps://taotoken.net/api taotoken.api.keysk-你的Key taotoken.model.id你的ModelID对应的 TOML 形式如果你用 Cline 或类似工具大致是这样[taotoken] base_url https://taotoken.net/api api_key sk-你的Key model_id 你的ModelID注意Key 不要硬编码进提交到 Git 的文件里local.properties默认在.gitignore中是相对安全的做法。生产环境建议走服务端下发或密钥管理服务。4. 验证请求一次完整的查询与远端调用动作配置写完了得验证。验证分两段先验证 Cursor 查询本身再验证远端通道。第一段写一个单元测试或直接在 Activity 里调用queryAllCities()插入三条测试数据后查询打印结果。预期是控制台输出三行城市信息且没有异常。如果输出为空先检查moveToFirst()的返回值再检查列名是否拼写正确。这一步能过说明本地链路没问题。第二段验证远端通道。用 OkHttp 发一个最小请求OkHttpClient client new OkHttpClient(); MediaType JSON MediaType.parse(application/json; charsetutf-8); String body {\model\:\ BuildConfig.MODEL_ID \,\messages\:[{\role\:\user\,\content\:\ping\}]}; Request request new Request.Builder() .url(BuildConfig.API_BASE_URL /v1/chat/completions) .addHeader(Authorization, Bearer BuildConfig.API_KEY) .post(RequestBody.create(body, JSON)) .build(); client.newCall(request).enqueue(new Callback() { Override public void onFailure(Call call, IOException e) { Log.e(TaoToken, request failed, e); } Override public void onResponse(Call call, Response response) throws IOException { Log.d(TaoToken, code response.code() body response.body().string()); } });预期结果是code200body 里能看到模型返回的内容。如果返回 401说明 Key 有问题如果返回 404说明路径拼错了检查 Base URL 后面是否多加了或漏了/v1。这一步过了说明远端通道打通。把两段验证串起来就是一个完整的「本地查库 远端补全」动作先从 SQLite 查出城市列表如果列表为空再走远端接口拉取并写回本地库。这个模式在离线优先的 App 里非常常见。5. 本篇常见错排查401、local proxy failed 与 reading choices排障这块我按真实报错来列都是实际项目里遇到过的。第一个401 Unauthorized。九成是 Key 的问题。检查三处Key 是否复制完整、是否带了首尾空格、请求头是否是Authorization: Bearer sk-xxx格式。还有一种情况是 Key 被禁用或额度耗尽去 https://taotoken.net/api-keys 看一眼状态。第二个local proxy failed。这个报错通常出现在你本地配了代理工具但代理没启动或端口不对。注意这里说的是本地开发环境的网络配置问题不是让你去搞什么特殊通道。解决办法是检查系统代理设置或者临时关闭代理直连。如果你在 Android 模拟器里跑模拟器的网络走的是宿主机宿主机代理没配好就会报这个。第三个reading choices相关报错完整形态类似Cannot read property choices of undefined或reading choices。这通常发生在你解析响应时响应体不是预期的 JSON 结构。原因可能是请求路径错了返回了 HTML 错误页、Key 无效返回了错误 JSON、或者模型 ID 不存在。排查方法是先把原始响应 body 打印出来看看到底返回了什么再对症下药。第四个Cursor 相关的CursorWindowAllocationException。这是典型的没 close 导致的尤其在循环里反复查询时。解决办法就是本文第 3 节的 try-finally 封装确保每次查询后都 close。第五个getColumnIndex返回 -1 导致IllegalStateException。检查 SQL 里的列名和getColumnIndex里的字符串是否完全一致SQLite 列名大小写不敏感但拼写必须对。第六个OAuth 相关报错。如果你用 Claude Code 走 OAuth 流程报错通常是 token 过期或回调地址不匹配。参考 https://taotoken.net/ClaudeCodeAnthropic 里的配置说明重新走一遍授权流程。把这几类报错记住基本能覆盖 90% 的接入问题。剩下的靠看日志和打印原始响应解决。6. 语义一致 CTA按场景选对入口最后说下入口选择别一股脑全点首页。如果你正在排障或做接入直接去 API Keys 管理页 https://taotoken.net/api-keys 和接入文档 https://taotoken.net/doc 这两个页面能解决大部分配置问题。如果你想先验证模型效果比如试试某个模型返回质量如何去模型对话页 https://taotoken.net/chat 在线调试最直观。如果你是长期编码、跑 Agent 任务或者需要稳定的额度支持看 Coding Plan https://taotoken.net/coding-plan 这个更适合持续性的开发场景。控制台总入口在 https://taotoken.net/console 需要看用量、管理项目时从这里进。回到 Cursor 本身我的经验是把查询封装成方法、把 close 放进 finally、把列索引缓存起来这三件事做到基本不会出问题。本地库和远端通道各管各的边界清晰排障时才能快速定位。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进