☰
Android 中的 Cursor 类:从查询到遍历,一次讲透
2026/10/12 19:03:50 网站建设 项目流程

1. Android Cursor 类到底解决什么问题:从 SQLite 查询到遍历取值的完整链路

如果你写过 Android 本地数据存储,大概率绕不开android.database.Cursor。它是什么?简单说,Cursor 就是 SQLite 查询结果集的游标,你可以把它理解成一张临时表格加一个指向当前行的指针。SQLiteDatabase.query()或rawQuery()返回的不是 List,也不是数组,而是一个 Cursor 对象。它适合谁?适合所有需要在 Android 里读写 SQLite、封装 ContentProvider、或者维护老项目的开发者。哪怕你现在用 Room,底层依然会碰到 Cursor,比如RoomDatabase.query()或SupportSQLiteDatabase的原始查询。

很多新手第一次拿到 Cursor 会懵:为什么不能直接cursor[0]?为什么取个字段还要先getColumnIndex?原因在于 Cursor 是随机数据源,所有数据通过列下标取得,而且它内部维护一个位置指针,初始位置在-1,也就是第一行之前。你必须先moveToFirst()或moveToNext()把指针移到有效行,才能读取数据。这个设计跟 ADO.NET 的DataReader很像,但 Cursor 支持随机访问,可以moveToPosition(int)跳到任意行。

核心方法我按使用频率列一下:moveToFirst()定位第一行,返回 false 表示结果集为空;moveToNext()移到下一行,常配合 while 循环;getCount()返回总行数;getColumnIndex(String)返回列名对应的下标,不存在返回 -1;getColumnIndexOrThrow(String)不存在直接抛IllegalArgumentException;getString(int)、getInt(int)、getLong(int)按下标取值;isAfterLast()判断是否在最后一行之后;close()释放资源。这些方法组合起来,就构成了从查询到遍历的完整链路。

实际场景里,最常见的坑有三个。第一是空指针:查询结果为空时直接getString会抛CursorIndexOutOfBoundsException,因为指针还在 -1。第二是列名写错:getColumnIndex返回 -1,再用 -1 去取值同样越界。第三是忘记close():Cursor 持有底层 SQLite 的窗口资源,长期不关会导致内存泄漏,尤其在 Activity 或 Fragment 里查询后没释放,反复进出页面就会累积。我试过在列表页每次onResume查询却不关闭,跑几十次后StrictMode直接报CursorWindow泄漏警告。

所以正确的使用姿势是:查询后先判断moveToFirst(),遍历时用getColumnIndexOrThrow提前拿好下标,最后在finally块里close()。下面我会先讲清楚 TaoToken 在调试这类数据库逻辑时能帮上什么忙,再给一套可直接复制的 Cursor 封装工具类和查询示例,然后带你验证请求结果、排查真实报错,最后给出语义一致的接入入口。

2. TaoToken 前置准备:用模型对话快速定位 Cursor 报错与封装思路

在写 Cursor 封装之前,我先说一个提效手段。Android 数据库报错往往堆栈很长,CursorIndexOutOfBoundsException、StrictMode CursorWindow leak、IllegalArgumentException: column 'xxx' does not exist这些信息夹在几十行日志里,肉眼找根因很费时间。你可以把报错日志贴给 TaoToken 的模型对话,让它帮你定位是哪一步指针没移动、哪个列名拼错、哪个分支漏了 close。TaoToken 的模型对话入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,打开后直接粘贴日志和你的查询代码,让它给出修改建议。

如果你打算长期做 Android 编码和 Agent 辅助开发,可以了解 Coding Plan,入口是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。它适合把模型能力接进日常编码流程,比如让模型帮你生成 Cursor 封装、审查 close 是否遗漏、解释getColumnIndexOrThrow和getColumnIndex的取舍。对于本篇这种数据库工具类编写,模型对话足够用;如果你要批量处理多个模块的数据库访问层重构,Coding Plan 更顺手。

接入前你需要准备三件套:Base URL、API Key、Model ID。Base URL 用 https://taotoken.net/api ,API Key 在控制台创建,入口是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。Model ID 根据你控制台里可用的模型填写,比如常见的对话模型标识。这三件套在后面的配置片段里会完整出现,你照着填就行。文档入口在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,遇到参数不确定可以查。

这里要强调一点:TaoToken 是模型调用入口,不是数据库工具,也不替代 Android Studio 或 SQLite。它的作用是帮你理解报错、生成封装代码、审查资源释放逻辑。你最终运行的还是本地 SQLite 和 Cursor。把定位放在“辅助排查和代码生成”上,用起来才顺。下面进入可复制配置环节,我会给一套完整的 Cursor 封装工具类,以及一个查询示例,包含 Base URL、Key、Model ID 的配置片段,方便你把模型对话接进调试流程。

3. 可复制配置:Cursor 封装工具类 + 查询示例 + 模型接入片段

先给 Cursor 封装工具类。目标是把“移动指针、取列下标、判空、关闭”这几件事收拢,避免每次查询都写重复代码。下面这个类我实测下来比较稳,你可以直接放到项目里。

import android.database.Cursor; import android.database.CursorIndexOutOfBoundsException; import java.io.Closeable; import java.util.ArrayList; import java.util.List; public class CursorHelper implements Closeable { private final Cursor cursor; public CursorHelper(Cursor cursor) { this.cursor = cursor; } public boolean isEmpty() { return cursor == null || cursor.getCount() == 0 || !cursor.moveToFirst(); } public int getColumnIndexOrThrow(String columnName) { int index = cursor.getColumnIndex(columnName); if (index == -1) { throw new IllegalArgumentException("column '" + columnName + "' does not exist"); } return index; } public String getString(String columnName) { return cursor.getString(getColumnIndexOrThrow(columnName)); } public int getInt(String columnName) { return cursor.getInt(getColumnIndexOrThrow(columnName)); } public long getLong(String columnName) { return cursor.getLong(getColumnIndexOrThrow(columnName)); } public boolean moveToNext() { return cursor.moveToNext(); } public boolean moveToFirst() { return cursor.moveToFirst(); } public int getCount() { return cursor.getCount(); } @Override public void close() { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } }

这个封装的关键点:isEmpty()里先判断getCount() == 0,再moveToFirst(),避免空结果集时指针停在 -1;getColumnIndexOrThrow把 -1 转成明确异常,比直接越界好排查;close()判断isClosed()防止重复关闭。接下来是查询示例,用SQLiteOpenHelper建一张 people 表,然后查询并遍历。

public List<Person> queryAllPeople(SQLiteDatabase db) { List<Person> result = new ArrayList<>(); Cursor cursor = null; try { cursor = db.query( "people", new String[]{"_id", "name", "phone"}, null, null, null, null, "name ASC" ); CursorHelper helper = new CursorHelper(cursor); if (helper.isEmpty()) { return result; } do { Person p = new Person(); p.id = helper.getLong("_id"); p.name = helper.getString("name"); p.phone = helper.getString("phone"); result.add(p); } while (helper.moveToNext()); } finally { if (cursor != null) { cursor.close(); } } return result; }

注意这里用do-while而不是while,因为isEmpty()里已经moveToFirst()把指针移到第一行,如果再用while(moveToNext())会跳过第一行。这是很多人踩过的坑:先moveToFirst()判断非空,然后while(moveToNext())遍历,结果第一条数据永远读不到。正确做法要么用do-while,要么在moveToFirst()后先处理当前行再moveToNext()。

如果你要把模型对话接进调试流程,可以用下面这个 JSON 配置片段,路径按你项目实际存放位置调整。Base URL、Key、Model ID 三件套都在里面。

{ "baseUrl": "https://taotoken.net/api", "apiKey": "你的_API_Key", "modelId": "你的_Model_ID", "timeoutMs": 60000, "debug": { "logCursorLeak": true, "strictMode": true } }

如果你用 TOML 管理配置,等价写法如下:

[taotoken] base_url = "https://taotoken.net/api" api_key = "你的_API_Key" model_id = "你的_Model_ID" timeout_ms = 60000 [taotoken.debug] log_cursor_leak = true strict_mode = true

如果你在 Android 项目里用settings.gradle或自定义 properties 管理,也可以把这三件套写进local.properties,但注意不要提交到版本库。API Key 创建入口在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite ,创建后复制到配置里。Model ID 以控制台显示为准,不要照抄别人的。配置完成后,你就可以在调试 Cursor 报错时,把日志和代码片段发给模型对话,让它帮你确认是不是漏了 close、列名是否拼错、指针移动顺序是否正确。

4. 验证请求与成功结果:确认 Cursor 遍历、取值、关闭都正确

配置和代码都就位后,怎么验证 Cursor 逻辑真的对了?我分三步走:先验证查询返回行数,再验证遍历取值,最后验证资源关闭。第一步,插入三条测试数据,然后调用queryAllPeople,打印result.size()。如果返回 3,说明moveToFirst和do-while配合正确;如果返回 2,大概率是遍历时跳过了第一行;如果返回 0,检查isEmpty()是不是把非空结果误判了。

SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("INSERT INTO people (name, phone) VALUES ('Alice', '1001')"); db.execSQL("INSERT INTO people (name, phone) VALUES ('Bob', '1002')"); db.execSQL("INSERT INTO people (name, phone) VALUES ('Cindy', '1003')"); List<Person> list = queryAllPeople(db); Log.d("CursorTest", "size=" + list.size()); for (Person p : list) { Log.d("CursorTest", "id=" + p.id + ", name=" + p.name + ", phone=" + p.phone); }

预期日志是size=3,然后依次打印 Alice、Bob、Cindy 的 id、name、phone。如果name打印出 null,检查getColumnIndexOrThrow是否抛了异常被吞掉,或者列名大小写是否匹配。SQLite 列名默认不区分大小写,但getColumnIndex在某些版本对大小写敏感,建议统一用小写。

第二步,验证空结果集。把查询条件改成name = 'NotExist',再跑一次。预期isEmpty()返回 true,result.size()为 0,且不抛异常。如果你看到CursorIndexOutOfBoundsException,说明在空结果集上直接调了getString,也就是isEmpty()判断没生效。检查isEmpty()里是否先getCount() == 0再moveToFirst(),顺序反了在某些实现上会出问题。

第三步,验证关闭。开启 StrictMode 的detectLeakedSqlLiteObjects和detectLeakedClosableObjects,反复进出查询页面 20 次,观察 Logcat 是否有CursorWindow泄漏警告。如果没有警告,说明finally里的close()生效了。你也可以在CursorHelper.close()里加一行日志,确认每次查询后都走到关闭分支。

StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .build());

成功结果的标准是:三条数据完整打印、空查询不崩、反复查询无泄漏警告。这三步都过了,说明你的 Cursor 封装和查询示例是可靠的。接下来讲常见报错排查,我会对照真实错误信息给出定位路径。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 对照定位

虽然本篇主题是 Cursor,但你在接入模型对话辅助调试时,可能会遇到几类报错。我把它们和 Cursor 本身的报错分开讲,方便你对照定位。

第一类,Cursor 本身的CursorIndexOutOfBoundsException: Index -1 requested, with a size of 3。这个错误说明指针还在 -1 就去取值了。根因通常是查询后没调moveToFirst()或moveToNext(),直接getString。修复方式:在取值前先if (!cursor.moveToFirst()) return;,或者用本文的CursorHelper.isEmpty()做前置判断。注意getCount()返回 3 不代表指针有效,指针初始位置始终是 -1。

第二类,IllegalArgumentException: column 'phoneNumber' does not exist。这是列名拼写错误,getColumnIndexOrThrow抛出来的。修复方式:用cursor.getColumnNames()打印所有列名,核对你的查询 projection 和实际表结构。常见坑是建表时列名是phone,查询时写phoneNumber,或者 projection 里没包含该列却去取值。

第三类,StrictMode CursorWindow leak或A resource was acquired at attached stack trace but never released。这是忘记close()的典型表现。修复方式:所有 Cursor 操作放在 try-finally 里,finally 中 close;或者用 try-with-resources(Cursor 实现了 Closeable,API 16+ 可用)。注意CursorHelper实现了Closeable,你也可以直接 try-with-resources。

第四类,模型接入侧的401 Unauthorized。这通常表示 API Key 无效或没带上。检查你的配置里apiKey是否填了控制台创建的值,Base URL 是否是 https://taotoken.net/api ,请求头是否带了Authorization: Bearer 你的_API_Key。如果 Key 刚创建,确认没有多余空格。创建入口在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

第五类,local proxy failed或连接超时。这类报错通常和本地网络环境、代理配置有关。检查你的 HTTP 客户端是否误设了代理,或者超时时间太短。把timeoutMs调到 60000 再试。如果你在公司网络下,确认出口是否允许访问对应域名。

第六类,reading choices相关报错,通常出现在解析模型返回结构时。模型返回的 JSON 里choices数组为空或字段名不匹配,就会在读取时抛错。检查你的解析代码是否按实际返回结构取值,不要硬编码假设。可以先把原始返回打印出来,确认choices[0].message.content是否存在。

第七类,OAuth相关报错。如果你用的是需要 OAuth 的客户端或插件,报错通常表示 token 过期或回调地址不匹配。检查你的 OAuth 配置里的回调 URL 是否和平台登记一致,token 是否需要刷新。这类问题优先看客户端文档,确认认证流程。

排查顺序建议:先确认 Cursor 指针和列名,再确认 close,最后才看模型接入侧的网络和认证。因为数据库报错和网络报错堆栈特征完全不同,混在一起看容易绕晕。把 Cursor 逻辑用本文的封装固定下来,能消掉一大半低级错误。

6. 语义一致 CTA:按场景选择模型对话、Coding Plan 或 API Keys

走到这里,你已经有一套可复制的 Cursor 封装工具类、查询示例、验证步骤和排错路径。接下来按你的实际场景选入口。如果你只是偶尔遇到 Cursor 报错,想把日志贴给模型快速定位,用模型对话,入口是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。如果你要长期做 Android 编码、批量重构数据库访问层、让模型参与日常开发流程,用 Coding Plan,入口是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。如果你还没创建 Key,先去 API Keys 页面,入口是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。参数和接入细节查文档,入口是 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。API 地址统一用 https://taotoken.net/api 。

最后补一个实用技巧:把本文的CursorHelper和queryAllPeople放进项目后,先跑一遍 StrictMode 验证,确认没有 CursorWindow 泄漏,再接入模型对话辅助排查。这样你的数据库层是干净的,模型给出的建议也更容易聚焦在真正的逻辑问题上。Cursor 本身不难,难的是指针移动顺序、列名核对和资源释放这三件事,把这三件固定成工具类,后面就省心了。

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

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

立即咨询