ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android 中关于 Cursor 类的介绍:从查询到遍历的完整实践

Android 中关于 Cursor 类的介绍:从查询到遍历的完整实践 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空集返回 falsemoveToNext()移动到下一行移动成功 true越界 falsemoveToLast()定位到最后一行有数据 truemoveToPosition(int)跳到绝对位置位置有效 truegetCount()返回总行数intgetColumnIndex(String)按列名找下标不存在返回 -1getColumnIndexOrThrow(String)同上但抛异常不存在抛 IllegalArgumentExceptiongetString(int)/getInt(int)按下标取值对应类型isAfterLast()是否在最后一行之后booleanisClosed()是否已关闭booleanclose()关闭释放资源void理解这张表你就理解了 Cursor 的 80%。剩下的 20% 在于什么时候用 while什么时候用 for以及怎么保证异常路径下也能关闭。下一节先讲前置准备把查询入口和 Cursor 的关系理清楚。2. TaoToken 前置准备用 API Key 打通模型辅助排查 Cursor 报错写 Cursor 相关代码时最烦的不是写不出来而是遇到CursorWindowAllocationException、IllegalStateException: Couldnt read row 0, col -1这类报错时不知道从哪查。我的做法是把报错栈和代码片段丢给模型让它帮我定位是列名拼错、游标越界还是没关闭。这里就需要一个稳定的模型调用入口。TaoToken 提供统一的 API 接入官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。它的作用是让你用一个 Key 就能调用多种模型不用在多个平台之间来回切换。对于 Android 开发者来说最实用的场景就是把Cursor报错日志贴进去让模型帮你分析是getColumnIndex返回 -1 还是moveToNext逻辑写反了。前置准备分三步。第一步注册并拿到 API Key入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。第二步如果你只是临时问几个问题直接用模型对话页面 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 就行不用写代码。第三步如果你想把模型能力集成到自己的调试工具或 CI 里那就用 API Key 走接口。这里要强调一个原则TaoToken 是模型调用入口不是数据库工具更不是让你把生产库直连上去。它只负责帮你分析代码和日志Cursor 的关闭、遍历逻辑还是得你自己写对。我试过把一段while(cur.moveToNext())漏掉close()的代码贴进去模型很快就指出「异常路径下没有 finally 关闭」这个提醒很实用。对于长期做 Android 开发、经常需要模型辅助排查的同学可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它的定位是给持续编码场景用的比单次对话更适合日常开发节奏。控制台入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你用的是 Claude Code 这类命令行编码工具接入配置可以参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 。配置时记住三件套Base URL、API Key、Model ID缺一不可。Base URL 填https://taotoken.net/apiKey 用你申请的那串Model ID 按文档里支持的模型名填。这三样对齐了请求才能通。前置准备做完下一节进入正题把 Cursor 的查询、遍历、关闭写成可复制的配置和代码片段。3. 可复制配置Cursor 遍历与资源释放的标准写法这一节是全文的核心直接给可复制的代码。先说查询入口。Android 里拿到 Cursor 主要有两种方式SQLiteDatabase.query()和rawQuery()。前者参数化、防注入后者灵活但需要自己拼 SQL。无论哪种返回的都是 Cursor。先看一个标准的查询 遍历 关闭模板这是你应该背下来的结构public ListPeople queryAllPeople(SQLiteDatabase db) { ListPeople 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): ListPeople { val result mutableListOfPeople() 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: Couldnt 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_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 页面里试不用写 curl。三块验证都过了说明你的 Cursor 逻辑和模型接入都是通的。下一节集中讲报错排查。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 对照这一节把真实遇到的报错列出来对照解决。先声明以下报错都是配置或代码问题不涉及任何网络工具。报错一401 Unauthorized。这是模型接口最常见的。原因通常是 API Key 没填、填错、或者带了多余空格。检查Authorization: Bearer xxx里的 Key 是否和 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 页面里的一致。另外注意 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/apiAPI Key 是否有效Model ID 是否在支持列表里。Claude Code 的接入说明在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite 按文档一步步来。除了接口报错Cursor 本身的报错也要会看报错信息原因解决Couldnt read row 0, col -1列下标为 -1用 getColumnIndexOrThrow 或判断 -1CursorWindowAllocationExceptionCursor 未关闭或数据量过大finally 中 close分页查询IllegalStateException: attempt to re-open an already-closed object关闭后又使用关闭后不要再调用方法moveToNext返回 false 但仍有数据游标位置错误检查是否混用 moveToFirstStaleDataExceptionCursor 数据失效重新查询不要跨线程复用排查顺序建议先看 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/apiKey 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 获取文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。长期编码场景用 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。配置时三件套对齐Base URL、API Key、Model ID。Cursor 这东西用对了就是数据访问的利器用错了就是内存泄漏的源头。把close()放在 finally 里把列下标提到循环外把moveToFirst的返回值判断好你的数据访问逻辑就稳了。
返回列表