1. 从一次崩溃日志说起Cursor 没关应用直接报警android.database.sqlite.DatabaseObjectNotClosedException: Application did not close the cursor or database object that was opened here这个报错做 Android 本地存储的同学大概率都见过。它不像空指针那样立刻崩而是像水龙头没拧紧平时看不出问题等到某个页面反复进出、数据量上来之后日志里突然刷出一大段堆栈告诉你某个 Cursor 或 SQLiteDatabase 对象在 GC 时还没被关闭。这个异常的本质是SQLite 的 Cursor 和 SQLiteDatabase 底层持有 native 资源Java 层的对象被回收时如果对应的 native 资源没释放系统就会在 finalize 阶段抛出这个异常作为警告。它通常出现在 Activity/Fragment 销毁后 Cursor 仍被引用、CursorAdapter 忘记swapCursor、或者异常分支提前return导致finally之外的关闭逻辑被跳过。适合正在写 SQLite 查询、用 CursorAdapter 展示列表、或者接手了老项目需要排查内存泄漏的 Android 开发者。我试过在一个老项目里追这个问题堆栈只给了一个创建位置但代码里同一个 helper 被好几个页面调用光看日志根本定位不到是哪条查询路径漏了关闭。后来我把 Cursor 的创建和关闭统一收口再配合 AI 辅助分析 logcat 堆栈才把几条隐藏的泄漏路径揪出来。这篇就按这个思路先讲清楚异常怎么触发再给一套可复制的 Cursor 封装骨架和 StrictMode 检测配置最后演示怎么用 TaoToken 的统一 Key 接入 AI 通道把 logcat 堆栈丢进去做辅助定位。2. 为什么 Cursor 泄漏这么难查三类典型触发场景2.1 Activity/Fragment 销毁后 Cursor 仍存活最常见的写法是在onCreate或某个点击事件里rawQuery拿到 Cursor然后把它设给 Adapter 或者存成成员变量但销毁时只记得关数据库忘了关 Cursor。Cursor 本身持有指向结果集的 native 指针页面销毁后如果还有引用链比如被 Adapter 持有、被匿名内部类捕获GC 就无法及时回收最终在 finalize 时报警。2.2 CursorAdapter 忘记 swapCursor用CursorAdapter或RecyclerView.Adapter包装 Cursor 时每次数据刷新应该调用swapCursor(newCursor)它内部会帮你关闭旧 Cursor。但很多人直接changeCursor或者干脆重新new一个 Adapter旧 Cursor 就悬在那里了。更隐蔽的是在onDestroy里只调了adapter null没有先swapCursor(null)旧 Cursor 依然没关。2.3 异常分支提前 return 绕过关闭这是 excerpt 里那段代码暴露的问题cursor2在while循环内部创建关闭语句写在循环体里。如果循环中途抛异常或者某个if分支提前returncursor2.close()就被跳过了。即使外层finally里有cursor2.close()也要保证cursor2变量在finally作用域内可见且指向正确对象。很多人把 Cursor 声明在 try 内部finally 根本拿不到引用。注意db.close()不是万能的。关闭 SQLiteDatabase 不会自动关闭所有由它打开的 CursorCursor 必须单独关闭。而且db.close()之后如果还有 Cursor 在用反而会引发新的异常。3. 前置准备用 TaoToken 统一 Key 打通 AI 辅助分析通道排查这类问题光靠肉眼扫代码效率很低尤其是堆栈里只给创建点、不给泄漏点的时候。我的做法是把 logcat 里的完整堆栈整理出来交给 AI 做结构化分析让它帮我列出所有可能的未关闭路径。这里用 TaoToken 的统一 Key 来接入好处是一个 Key 可以走多个模型通道不用为每个模型单独配环境。TaoToken 的 API 地址是https://taotoken.net/api控制台和文档入口如下模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcursor_leakCoding Plan长期编码/Agent 场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcursor_leak控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcursor_leakAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcursor_leak接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcursor_leak拿到 Key 之后你可以用任意 HTTP 客户端调用下面给一个 curl 示例把 logcat 堆栈作为输入让模型帮你分析哪些路径可能漏关 Cursorcurl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [ { role: user, content: 下面是一段 Android logcat 堆栈报 DatabaseObjectNotClosedException。请帮我列出所有可能导致 Cursor 未关闭的代码路径并给出修复建议\n\n粘贴堆栈 } ] }如果你更习惯在 IDE 里直接对话可以走模型对话入口把堆栈贴进去让它按「创建点—使用点—关闭点」三段式帮你梳理。长期做 Android 排查的话Coding Plan 更适合因为它对代码上下文的理解更连贯能记住你项目里的封装习惯。4. 可复制的 Cursor 封装骨架与 StrictMode 配置4.1 一个不容易漏关的 Cursor 封装核心思路是Cursor 的创建和关闭必须成对出现在同一个作用域且关闭逻辑放在finally里变量声明在 try 之外。下面这个骨架可以直接抄public ListMemo queryMemos(SQLiteDatabase db, long start, long end) { ListMemo result new ArrayList(); Cursor cursor null; Cursor inner null; try { cursor db.rawQuery( select id, title from memos where time ? and time ? order by time desc, new String[]{String.valueOf(start), String.valueOf(end)}); while (cursor.moveToNext()) { long id cursor.getLong(0); String title cursor.getString(1); try { inner db.rawQuery( select file_path from memos where id ?, new String[]{String.valueOf(id)}); if (inner.moveToFirst()) { String path inner.getString(0); result.add(new Memo(id, title, path)); } } finally { if (inner ! null) { inner.close(); inner null; } } } } finally { if (cursor ! null) { cursor.close(); } } return result; }关键点有三个第一inner的关闭放在内层finally保证每次循环都释放第二关闭后把inner置为null避免下一轮循环误用已关闭对象第三外层cursor的关闭放在最外层finally无论是否抛异常都会执行。注意这里没有在方法里db.close()因为数据库连接通常由 Helper 统一管理方法内关闭会导致后续查询失败。4.2 StrictMode 检测配置光靠代码规范不够得让系统主动告诉你哪里漏了。在 Application 的onCreate里开启 StrictMode 的detectLeakedSqlLiteObjects和detectLeakedClosableObjectsOverride public void onCreate() { super.onCreate(); if (BuildConfig.DEBUG) { StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .build()); } }开启后只要 Cursor 或 SQLiteDatabase 在 GC 时还没关闭logcat 里就会打出StrictMode相关的泄漏日志比DatabaseObjectNotClosedException更早暴露问题。建议只在 Debug 包开启Release 包关掉避免性能开销。4.3 CursorAdapter 的正确刷新姿势如果你用 CursorAdapter刷新数据时不要直接new新 Adapter而是public void updateData(Cursor newCursor) { Cursor old adapter.swapCursor(newCursor); if (old ! null) { old.close(); } }swapCursor返回旧 Cursor你需要手动关闭它。在onDestroy里也要记得Override protected void onDestroy() { super.onDestroy(); if (adapter ! null) { Cursor old adapter.swapCursor(null); if (old ! null) { old.close(); } } }5. 验证请求与成功结果用 AI 分析 logcat 堆栈配置好 StrictMode 后复现一次泄漏场景logcat 里会出现类似这样的日志StrictMode policy violation; ~duration120 ms: android.os.StrictMode$StrictModeViolation at android.database.sqlite.SQLiteCursor.finalize(SQLiteCursor.java:xxx) at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:xxx)把这段完整堆栈包括上面的Caused by和创建点复制出来通过 TaoToken 的模型对话入口提交提示词可以这样写这是一段 Android StrictMode 泄漏日志报 SQLiteCursor 未关闭。 请按以下格式输出 1. 创建点堆栈中哪一行创建了 Cursor 2. 泄漏原因为什么没关闭 3. 修复建议具体改哪一行代码 4. 是否还有其他潜在泄漏路径 堆栈如下 粘贴完整堆栈实测下来模型能比较准确地从堆栈里定位到rawQuery的调用行并结合你贴的代码片段指出finally缺失或return提前的问题。如果堆栈里信息不够它还会提示你补充哪些上下文比如对应的 Adapter 代码或生命周期方法。验证成功的标志是修复后重新跑一遍StrictMode 不再报同一处泄漏DatabaseObjectNotClosedException也不再出现。如果还有残留把新的堆栈再丢给 AI 做二次分析通常两三轮就能清干净。6. 本篇常见错排查错误一db.close()之后又用 Cursor。关闭数据库不会关闭 Cursor但关闭数据库后 Cursor 可能失效。正确顺序是先关 Cursor再关数据库且数据库关闭要谨慎通常交给 Helper 统一管理。错误二finally里访问不到 Cursor 变量。如果 Cursor 声明在 try 内部finally 拿不到引用。必须把声明提到 try 之外初始化为null。错误三循环内 Cursor 只在外层 finally 关闭。像 excerpt 里那样cursor2在循环内创建如果只在外层 finally 关循环第二次创建时旧对象就泄漏了。必须在循环内每次用完就关。错误四swapCursor后忘记关旧 Cursor。swapCursor返回的旧 Cursor 需要手动关闭否则每次刷新都泄漏一个。错误五StrictMode 只在 Debug 开启Release 漏测。建议在测试包也开启或者用 LeakCanary 配合检测避免只在 Debug 发现。如果你在接入 AI 辅助分析时遇到 Key 配置或请求报错可以走 API Keys 管理页检查 Key 状态或查阅接入文档确认请求格式。长期做 Android 编码排查的话Coding Plan 能保持上下文连贯减少重复贴代码的次数。
企业数字化 ERP 产品动态
相关推荐
Readest电子书阅读器:5分钟安装配置与本地书库管理指南 1. 为什么我最终选择了 Readest1.1 从“能看书”到“看得舒服”的需求升级电子书阅读器这个品类,表面上看早就饱和了——手机自带阅读App、各类商业阅读软件、浏览器插件,随手一抓一大把。但真正长期用下来,你会发现一个尴尬的现实࿱… · 2026/9/26 12:16:36
D30 | 数据标注平台:从 0 搭一个企业级 AI 训练数据生产系统 文章目录 D30 | 数据标注平台:从 0 搭一个企业级 AI 训练数据生产系统 写在前面 一、为什么需要数据标注平台 1.1 数据标注的 3 大痛点 1.2 标注平台的 5 大核心模块 1.3 选型参考 二、标注任务管理:分配 / 进度 / 状态 2.1 任务生命周期 2.2 任务管理实现 2.3 任务分配策略 … · 2026/9/26 12:16:36
【愚公系列】《OpenClaw实战指南》027-销售与客服:微信私域AI打单配置TaoToken实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 12:16:36
ZooKeeper从原理到实战:分布式协调、集群搭建与锁实现 1. 先聊聊:分布式系统的“通病”为什么需要“协调者”我最早接触ZooKeeper,是很多年前第一次搭Hadoop HA(高可用)集群的时候。那时候连它的发音都搞不清楚,只记得网上教程里说“没有ZK,NameNode的自动故障切… · 2026/9/26 13:26:13
第257篇_代驾平台计费规则与服务价格采集 【Python爬虫实战】第257篇:起步价里程费等候费一网打尽——代驾平台计费规则与服务价格采集实战 所属专栏:【Python爬虫实战】从零到企业级爬虫工程师(CSDN 付费专栏) 本篇篇目:第 257 篇(垂直行业爬虫出行服务专场) 难度等级:中级,有 requests + BeautifulSoup 基础… · 2026/9/26 13:26:13
第256篇_家教陪练平台师资与课时费采集 【Python爬虫实战】第256篇:家教到底该花多少钱一小时——陪练老师资质与课时费采集实战 所属专栏:【Python爬虫实战】从零到企业级爬虫工程师(CSDN 付费专栏) 本篇篇目:第 256 篇(垂直本地生活服务数据采集专场 第 7 篇) 难度等级:中级,核心是多维分组聚合与资质分级… · 2026/9/26 13:26:13
人生备份档案馆:如何把记忆当作数据资产来管理 那台旧笔记本我搬了三次家都没舍得扔。某天深夜充电再开机,屏幕亮起来,桌面躺着一个文件:blog_20150911.tar.gz,二百多兆,里面是一千多张照片和三百多篇日志。我盯着那个文件名看了很久,才意识到自己这些年… · 2026/9/26 13:26:07
OpenClaw 自动整理笔记实战:用 TaoToken 统一 Key 打通每日归档流程 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 13:26:07
第258篇_共享单车投放分布与热力数据采集 【Python爬虫实战】第258篇:经纬度网格 + geohash + GeoJSON——共享单车投放分布与热力数据采集实战 所属专栏:【Python爬虫实战】从零到企业级爬虫工程师(CSDN 付费专栏) 本篇篇目:第 258 篇(垂直行业爬虫地理空间专场,高级篇) 难度等级:高级,需要理解经纬度、知道… · 2026/9/26 13:26:07
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46