1. Android Cursor 到底是什么:从 query 结果集到逐行遍历的完整链路
如果你写过 Android 本地数据存储,Cursor这个名字一定绕不开。它本质上是一个查询结果集的游标,你可以把它想象成一张 Excel 表格上的一根手指:表格是SQLiteDatabase.query()返回的全部数据,手指指向哪一行,你就能读哪一行的列值。搞 .NET 的同学可以类比DataReader,搞前端的同学可以类比ResultSet的迭代器,只不过 Android 把它抽象成了android.database.Cursor这个接口。
它能做什么?一句话概括:承载查询结果、支持随机定位、按列名或下标取值、最后必须关闭释放资源。适合谁?所有用SQLiteOpenHelper、Room底层、或者直接调rawQuery的 Android 开发者。哪怕你现在用 Room,遇到复杂 SQL 或CursorWindow相关报错时,底层依然是 Cursor 在干活。
我见过太多项目里 Cursor 用得一塌糊涂:要么忘了close()导致CursorWindow泄漏,要么moveToNext()和moveToFirst()混用导致第一行数据被吞掉,要么getColumnIndex返回 -1 直接抛IllegalArgumentException。这篇就围绕「查询 → 移动 → 读取 → 关闭」这条完整链路,把可复制的代码、验证方法和常见报错一次讲透。核心检索词先摆出来:Android Cursor 遍历与资源释放,这是本文的主线。
先明确 Cursor 的几个关键事实,这些是后面所有代码的前提:
Cursor 是每行的集合,初始位置在第一行之前(before first)。你必须先调用moveToFirst()或moveToNext()才能读到数据。所有数据通过下标取得,列名只是帮你找到下标的辅助。Cursor 是随机数据源,支持moveToPosition(int)跳转。最重要的:它持有底层 CursorWindow 内存,不关闭就会泄漏。
常用方法我整理成一张表,方便你对照记忆:
| 方法 | 作用 | 返回值说明 |
|---|---|---|
moveToFirst() | 定位到第一行 | 有数据返回 true,空集返回 false |
moveToNext() | 移动到下一行 | 移动成功 true,越界 false |
moveToLast() | 定位到最后一行 | 有数据 true |
moveToPosition(int) | 跳到绝对位置 | 位置有效 true |
getCount() | 返回总行数 | int |
getColumnIndex(String) | 按列名找下标 | 不存在返回 -1 |
getColumnIndexOrThrow(String) | 同上但抛异常 | 不存在抛 IllegalArgumentException |
getString(int)/getInt(int) | 按下标取值 | 对应类型 |
isAfterLast() | 是否在最后一行之后 | boolean |
isClosed() | 是否已关闭 | boolean |
close() | 关闭释放资源 | void |
理解这张表,你就理解了 Cursor 的 80%。剩下的 20% 在于:什么时候用 while,什么时候用 for,以及怎么保证异常路径下也能关闭。下一节先讲前置准备,把查询入口和 Cursor 的关系理清楚。
2. TaoToken 前置准备:用 API Key 打通模型辅助排查 Cursor 报错
写 Cursor 相关代码时,最烦的不是写不出来,而是遇到CursorWindowAllocationException、IllegalStateException: Couldn't read row 0, col -1这类报错时,不知道从哪查。我的做法是:把报错栈和代码片段丢给模型,让它帮我定位是列名拼错、游标越界还是没关闭。这里就需要一个稳定的模型调用入口。
TaoToken 提供统一的 API 接入,官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api 。它的作用是让你用一个 Key 就能调用多种模型,不用在多个平台之间来回切换。对于 Android 开发者来说,最实用的场景就是:把Cursor报错日志贴进去,让模型帮你分析是getColumnIndex返回 -1 还是moveToNext逻辑写反了。
前置准备分三步。第一步,注册并拿到 API Key,入口在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。第二步,如果你只是临时问几个问题,直接用模型对话页面 https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 就行,不用写代码。第三步,如果你想把模型能力集成到自己的调试工具或 CI 里,那就用 API Key 走接口。
这里要强调一个原则:TaoToken 是模型调用入口,不是数据库工具,更不是让你把生产库直连上去。它只负责帮你分析代码和日志,Cursor 的关闭、遍历逻辑还是得你自己写对。我试过把一段while(cur.moveToNext())漏掉close()的代码贴进去,模型很快就指出「异常路径下没有 finally 关闭」,这个提醒很实用。
对于长期做 Android 开发、经常需要模型辅助排查的同学,可以考虑 Coding Plan,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它的定位是给持续编码场景用的,比单次对话更适合日常开发节奏。控制台入口在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
如果你用的是 Claude Code 这类命令行编码工具,接入配置可以参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite 。配置时记住三件套:Base URL、API Key、Model ID,缺一不可。Base URL 填https://taotoken.net/api,Key 用你申请的那串,Model ID 按文档里支持的模型名填。这三样对齐了,请求才能通。
前置准备做完,下一节进入正题:把 Cursor 的查询、遍历、关闭写成可复制的配置和代码片段。
3. 可复制配置:Cursor 遍历与资源释放的标准写法
这一节是全文的核心,直接给可复制的代码。先说查询入口。Android 里拿到 Cursor 主要有两种方式:SQLiteDatabase.query()和rawQuery()。前者参数化、防注入,后者灵活但需要自己拼 SQL。无论哪种,返回的都是 Cursor。
先看一个标准的查询 + 遍历 + 关闭模板,这是你应该背下来的结构:
public List<People> queryAllPeople(SQLiteDatabase db) { List<People> result = new ArrayList<>(); Cursor cursor = null; try { cursor = db.query( "people", // 表名 new String[]{"_id", "name", "number"}, // 列名数组 null, // where null, // whereArgs null, // groupBy null, // having "name ASC" // orderBy ); if (cursor == null) { return result; } // 先判断是否为空集 if (!cursor.moveToFirst()) { return result; } // 提前取列下标,避免循环内重复查找 int idIndex = cursor.getColumnIndexOrThrow("_id"); int nameIndex = cursor.getColumnIndexOrThrow("name"); int numberIndex = cursor.getColumnIndexOrThrow("number"); do { People p = new People(); p.id = cursor.getLong(idIndex); p.name = cursor.getString(nameIndex); p.number = cursor.getString(numberIndex); result.add(p); } while (cursor.moveToNext()); } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } return result; }这段代码有几个关键点。第一,cursor声明在 try 外面,finally 里才能访问。第二,moveToFirst()返回 false 说明空集,直接返回,不要进循环。第三,用do-while而不是while,因为moveToFirst()已经把游标定位到第一行了,如果再用while(moveToNext())会跳过第一行。第四,列下标在循环外取一次,循环内复用,性能更好。第五,finally 里判断!cursor.isClosed()再关闭,避免重复关闭。
如果你更喜欢 for 循环,Google 官方也给了写法:
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { int nameIndex = cursor.getColumnIndexOrThrow("name"); String name = cursor.getString(nameIndex); // 处理数据 }这种写法用isAfterLast()作为终止条件,逻辑上等价于 do-while。但注意:getColumnIndexOrThrow放在循环内会重复查找,数据量大时建议提到循环外。
再说rawQuery的写法,参数用?占位:
Cursor cursor = db.rawQuery( "SELECT _id, name, number FROM people WHERE age > ? ORDER BY name", new String[]{"18"} );无论哪种查询,关闭逻辑都一样。这里给一个 Kotlin 版本,用use扩展自动关闭:
fun queryAllPeople(db: SQLiteDatabase): List<People> { val result = mutableListOf<People>() db.query("people", arrayOf("_id", "name", "number"), null, null, null, null, "name ASC") .use { cursor -> if (!cursor.moveToFirst()) return result val idIndex = cursor.getColumnIndexOrThrow("_id") val nameIndex = cursor.getColumnIndexOrThrow("name") val numberIndex = cursor.getColumnIndexOrThrow("number") do { result.add( People( id = cursor.getLong(idIndex), name = cursor.getString(nameIndex), number = cursor.getString(numberIndex) ) ) } while (cursor.moveToNext()) } return result }Kotlin 的use会在 lambda 结束后自动调用close(),即使抛异常也能保证关闭,这是最推荐的写法。
关于配置,如果你要把模型接入到调试流程,这里给一个 JSON 配置片段,路径按你的工具要求放:
{ "base_url": "https://taotoken.net/api", "api_key": "你的_API_KEY", "model": "按文档填写的模型ID" }注意 Base URL 不要带 UTM 参数,API 端点就是https://taotoken.net/api。Key 和 Model ID 必须和文档一致,否则会报 401 或模型不存在。
配置和代码都齐了,下一节讲怎么验证:游标越界怎么测、内存泄漏怎么查、请求是否成功怎么确认。
4. 验证请求与成功结果:游标越界、内存泄漏、接口连通性实测
写完代码不验证,等于没写。这一节分三块:Cursor 逻辑验证、内存泄漏验证、模型接口连通性验证。
先说 Cursor 越界验证。最常见的越界是getColumnIndex返回 -1 后直接取值。你可以写一个测试用例,故意传一个不存在的列名:
Cursor cursor = db.query("people", null, null, null, null, null, null); int badIndex = cursor.getColumnIndex("not_exist_column"); // badIndex == -1 // 如果直接 cursor.getString(badIndex) 会抛异常实测下来,getString(-1)会抛IllegalStateException: Couldn't read row 0, col -1 from CursorWindow。所以生产代码里要么用getColumnIndexOrThrow,要么对 -1 做判断。另一个越界场景是moveToPosition超出范围:
int count = cursor.getCount(); boolean ok = cursor.moveToPosition(count); // 越界,返回 false boolean ok2 = cursor.moveToPosition(count - 1); // 最后一行,返回 truemoveToPosition越界不会抛异常,只返回 false,但如果你不判断返回值就取值,读到的可能是上一行的数据,这是隐蔽的 bug。
再说内存泄漏验证。Cursor 不关闭会导致CursorWindow泄漏,表现为CursorWindowAllocationException或内存持续增长。验证方法:在循环里反复查询不关闭,观察 Android Studio Profiler 的内存曲线。正确写法下,每次查询后 Cursor 都被关闭,内存应该平稳。你可以在close()后打印cursor.isClosed()确认:
cursor.close(); Log.d("CursorTest", "isClosed=" + cursor.isClosed()); // 应为 true还有一个实用技巧:用StrictMode检测未关闭的 Cursor。在 Application 里开启:
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .build());如果 Cursor 没关闭,Logcat 会打印A resource was acquired at attached stack trace but never released,直接定位到泄漏点。
最后说模型接口连通性验证。配置好 Base URL、Key、Model ID 后,用 curl 测一下:
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer 你的_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "按文档填写的模型ID", "messages": [{"role": "user", "content": "解释一下 Android Cursor 的 moveToNext 返回值含义"}] }'成功的话会返回 JSON,里面choices[0].message.content就是模型回答。如果返回 401,说明 Key 不对;如果返回模型不存在,说明 Model ID 填错;如果连接超时,检查 Base URL 是否是https://taotoken.net/api。验证模型对话也可以直接在 https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 页面里试,不用写 curl。
三块验证都过了,说明你的 Cursor 逻辑和模型接入都是通的。下一节集中讲报错排查。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 对照
这一节把真实遇到的报错列出来,对照解决。先声明:以下报错都是配置或代码问题,不涉及任何网络工具。
报错一:401 Unauthorized。这是模型接口最常见的。原因通常是 API Key 没填、填错、或者带了多余空格。检查Authorization: Bearer xxx里的 Key 是否和 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 页面里的一致。另外注意 Base URL 不要写成带 UTM 的地址,API 端点就是https://taotoken.net/api。
报错二:local proxy failed。这个报错通常出现在本地工具配置了代理但代理没启动时。解决方法是检查工具的代理设置,把代理关掉或指向正确的本地端口。注意:这里说的是本地开发工具的代理配置,不是让你去用什么网络工具,纯粹是配置项排查。
报错三:reading choices 相关错误。比如Cannot read property 'choices' of undefined或reading 'choices'。这通常是接口返回结构和你解析的字段不匹配。先打印完整响应体,确认返回的是{"choices":[...]}还是错误对象。如果返回的是{"error":{...}},说明请求本身失败了,先解决错误对象里的 message。
报错四:OAuth 相关错误。如果你用的是 Claude Code 这类工具,配置时可能遇到 OAuth 认证问题。这时候检查三件套:Base URL 是否为https://taotoken.net/api,API Key 是否有效,Model ID 是否在支持列表里。Claude Code 的接入说明在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite ,按文档一步步来。
除了接口报错,Cursor 本身的报错也要会看:
| 报错信息 | 原因 | 解决 |
|---|---|---|
Couldn't read row 0, col -1 | 列下标为 -1 | 用 getColumnIndexOrThrow 或判断 -1 |
CursorWindowAllocationException | Cursor 未关闭或数据量过大 | finally 中 close,分页查询 |
IllegalStateException: attempt to re-open an already-closed object | 关闭后又使用 | 关闭后不要再调用方法 |
moveToNext返回 false 但仍有数据 | 游标位置错误 | 检查是否混用 moveToFirst |
StaleDataException | Cursor 数据失效 | 重新查询,不要跨线程复用 |
排查顺序建议:先看 Logcat 完整栈,定位到具体行;再检查列名和下标;最后检查关闭逻辑。如果拿不准,把栈和代码贴到模型对话里,让它帮你分析。记住,模型是辅助,最终还是要你理解 Cursor 的生命周期。
6. 语义一致收尾:把 Cursor 生命周期管好,比什么都重要
写到这里,Cursor 的查询、移动、读取、关闭这条链路已经完整了。最后说几个我踩过的坑,都是实战里真金白银换来的。
第一个坑:在Adapter的getView里查询数据库拿 Cursor,结果每个 item 都开一个 Cursor,滑动时内存暴涨。正确做法是在后台查一次,把数据转成 List 再交给 Adapter。Cursor 是短生命周期对象,不要长期持有。
第二个坑:moveToFirst()和while(moveToNext())混用,导致第一行永远读不到。记住:moveToFirst()已经把游标放到第一行,接下来要用do-while或for配合isAfterLast()。
第三个坑:在 finally 里关闭 Cursor 时没判断 null,导致空指针。正确写法是if (cursor != null && !cursor.isClosed())。
第四个坑:跨线程复用 Cursor。Cursor 不是线程安全的,一个线程查完关闭,另一个线程再用就会StaleDataException。要么每个线程独立查询,要么查完转成数据集合再传递。
如果你在排查这些报错时需要模型辅助,记得用 TaoToken 的 API Key 接入,Base URL 是https://taotoken.net/api,Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 获取,文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。长期编码场景用 Coding Plan,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。配置时三件套对齐:Base URL、API Key、Model ID。
Cursor 这东西,用对了就是数据访问的利器,用错了就是内存泄漏的源头。把close()放在 finally 里,把列下标提到循环外,把moveToFirst的返回值判断好,你的数据访问逻辑就稳了。