尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Android StaleDataException 排查:Cursor 关闭后访问的配置与验证
1. 先看清这个异常到底在说什么android.database.StaleDataException: Attempted to access a cursor after it has been closed这句话翻译成人话就是你手里那个 Cursor 已经被关掉了但代码还在往上摸数据。它跟空指针不一样空指针是对象没了这个是对象还在、底层数据窗口没了所以一访问就抛。这个异常最典型的触发路径是 Activity 在onResume阶段去恢复界面结果恢复过程中碰到了某个已经close()的 Cursor。堆栈里经常能看到BulkCursorToCursorAdaptor.requery或者CursorWrapper.requery说明是系统在帮你重新查询时发现 Cursor 已经失效了。它适合谁看适合正在写 ContentProvider 查询、用managedQuery或getContentResolver().query拿数据、又在异步线程或生命周期回调里操作 Cursor 的 Android 开发者。尤其是那种「列表能显示但一旋转屏幕或从后台回来就崩」的场景基本就是它。我先把结论放前面这个异常九成不是「Cursor 不该关」而是「关的时机和访问的时机打架了」。你要做的不是不关而是让关闭动作和访问动作在同一个生命周期里排队别让异步线程抢跑。2. 用 TaoToken 统一 Key/API 通道做排查辅助排查这类异常时我习惯把报错堆栈、相关代码片段、以及「我怀疑是生命周期问题」这个判断一起丢给模型让它帮我列出可能的关闭点。但很多同学卡在第一步手上没有稳定的模型调用通道东拼西凑的 Key 换来换去排查节奏全断了。TaoToken 在这里的角色就是一个统一的 Key/API 通道。你不需要为每个模型单独维护一套鉴权和地址用同一个 Key 就能走模型对话、Coding Plan、控制台和 API Keys 管理。对 Android 这种需要反复贴堆栈、反复问「这段代码哪里可能提前 close」的场景通道稳定比什么都重要。具体入口我列一下你按需取模型对话贴堆栈问原因https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Plan长期写 Android 代码、Agent 辅助https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台看用量、管项目https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys生成/轮换 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档看具体怎么调https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaudeCodeAnthropic 相关https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewriteAPI 基础地址是https://taotoken.net/api这个不带 UTM直接用于代码里配置。官网首页是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 想先了解整体能力可以从这里进。注意TaoToken 是模型调用通道不是数据库工具也不替代 Android Studio 的调试器。它的价值在于帮你快速把堆栈翻译成「哪一行可能提前关了 Cursor」以及生成可复制的生命周期检查代码。3. 可复制的 Cursor 生命周期检查清单在动手改代码之前先按这份清单过一遍。我把它做成「是/否」的形式你对着自己的代码勾。3.1 关闭点是否唯一一个 Cursor 只能有一个明确的关闭点。常见错误是在onDestroy关一次在异步回调里又关一次或者在finally里关、在onStop里也关。第二次关闭本身不报错但关闭之后如果还有代码去moveToFirst()或getString()就炸了。检查方法全局搜cursor.close()和Cursor.close()看同一个变量名出现了几次关闭。超过一次就要警惕。3.2 访问点是否都在关闭之前把 Cursor 的「创建 → 访问 → 关闭」画成一条时间线。访问包括moveToFirst、moveToNext、getCount、getString、getInt、requery。只要有一个访问点排在关闭点后面就是隐患。异步线程最容易出问题主线程在onStop关了 Cursor异步线程还在doInBackground里遍历。解决办法不是加锁而是让异步任务持有自己的 Cursor或者用Loader/ViewModel托管。3.3 是否用了 managedQuerycontext.managedQuery(...)返回的 Cursor 由 Activity 托管Activity 会在合适时机自动关闭它。如果你又手动close()就相当于把系统要管的东西提前弄死了onResume恢复时系统再去requery就抛StaleDataException。而getContentResolver().query(...)返回的 Cursor 需要你自己关。这两者的关闭责任完全不同混用就会出问题。3.4 是否在 onResume 里重新查询如果onResume里调用了requery()或重新query()而旧 Cursor 已经被关就会命中BulkCursorToCursorAdaptor.requery这条堆栈。正确做法是在onResume里重新创建 Cursor而不是复用旧的。3.5 是否用了 try-with-resourcesJava 7 之后Cursor实现了Closeable可以用 try-with-resources 自动关闭。但要注意自动关闭发生在 try 块结束时如果你的访问代码在 try 块之外照样会访问到已关闭的 Cursor。// 正确所有访问都在 try 块内 try (Cursor cursor resolver.query(uri, projection, null, null, null)) { if (cursor ! null cursor.moveToFirst()) { do { String name cursor.getString(cursor.getColumnIndex(name)); // 处理数据 } while (cursor.moveToNext()); } } // 这里自动 close// 错误cursor 在 try 外被访问 Cursor cursor null; try { cursor resolver.query(uri, projection, null, null, null); } finally { if (cursor ! null) cursor.close(); } cursor.moveToFirst(); // 已经关了抛 StaleDataException4. 用 Loader / ViewModel 配置骨架消除竞态手动管 Cursor 在异步场景下很容易翻车。更稳的做法是把查询交给LoaderManager或ViewModelLiveData让框架帮你处理生命周期。4.1 Loader 骨架public class MyLoader extends CursorLoader { public MyLoader(Context context, Uri uri, String[] projection) { super(context, uri, projection, null, null, null); } Override public Cursor loadInBackground() { // 这里返回的 Cursor 由 LoaderManager 托管 return super.loadInBackground(); } }在 Activity/Fragment 里Override public void onActivityCreated(Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); getLoaderManager().initLoader(0, null, this); } Override public LoaderCursor onCreateLoader(int id, Bundle args) { return new MyLoader(getActivity(), CONTENT_URI, PROJECTION); } Override public void onLoadFinished(LoaderCursor loader, Cursor data) { // 在这里使用 data不要手动 close adapter.swapCursor(data); } Override public void onLoaderReset(LoaderCursor loader) { // Loader 重置时释放引用不要 close adapter.swapCursor(null); }关键点onLoadFinished里拿到的 Cursor 由 LoaderManager 管理你不要手动close()。onLoaderReset里只做swapCursor(null)把引用交出去。4.2 ViewModel LiveData 骨架public class MyViewModel extends AndroidViewModel { private final LiveDataCursor cursorLiveData; public MyViewModel(NonNull Application application) { super(application); cursorLiveData new LiveDataCursor() { Override protected void onActive() { // 在活跃时查询 Cursor cursor getApplication().getContentResolver() .query(CONTENT_URI, PROJECTION, null, null, null); setValue(cursor); } Override protected void onInactive() { // 不活跃时关闭 Cursor cursor getValue(); if (cursor ! null !cursor.isClosed()) { cursor.close(); } } }; } public LiveDataCursor getCursor() { return cursorLiveData; } }在 Activity 里观察viewModel.getCursor().observe(this, cursor - { if (cursor ! null !cursor.isClosed()) { adapter.swapCursor(cursor); } });这里onInactive负责关闭onActive负责重新查询访问只发生在observe回调里而回调只在活跃时触发天然避开了「关了还访问」的窗口。5. 复现与验证步骤改完代码不能靠感觉要能稳定复现和验证。5.1 复现旧问题在修复前用这个步骤确认异常能稳定出现打开一个使用 Cursor 的列表页面。按 Home 键退到后台等 3 秒。从最近任务回到前台。如果代码里有「onStop 关闭 onResume 复用」的逻辑此时大概率抛StaleDataException。也可以用「旋转屏幕」触发Activity 重建时旧 Cursor 被关新 Activity 如果复用了静态引用就会命中。5.2 验证修复修复后重复上面的步骤观察 Logcatadb logcat | grep -i StaleDataException\|Cursor如果不再出现Attempted to access a cursor after it has been closed说明关闭时机已经对齐。再补一个压力测试连续快速切换前后台 10 次看是否稳定。5.3 用模型辅助确认把修复后的代码片段和 Logcat 输出贴到模型对话里问「这段代码还有没有可能在 Cursor 关闭后访问」。入口用 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 同一个 Key 就能调。如果你在长期维护 Android 项目Coding Plan 更适合反复迭代https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。6. 本篇常见错排查6.1 报错在 onResume但关闭点在 onStop这是最经典的组合。onStop里关了 CursoronResume里又去requery。解决要么在onResume重新query而不是requery要么把关闭点移到onDestroy并确保异步任务不再访问。6.2 managedQuery 和手动 close 混用managedQuery返回的 Cursor 不要手动关。如果你确实需要手动控制改用getContentResolver().query并自己负责关闭。6.3 异步线程持有 Cursor 引用AsyncTask或线程池里持有 Cursor主线程在生命周期回调里关了它。解决异步任务内部自己查询、自己关闭不要把 Cursor 跨线程传递。6.4 try-with-resources 块外访问前面代码示例已经展示过。检查你的try块范围确保所有moveToFirst、getString都在块内。6.5 重复 close第二次close()不抛异常但会让后续访问失效。用if (!cursor.isClosed()) cursor.close();做保护或者确保关闭点唯一。6.6 Loader 里手动 closeonLoadFinished里拿到 Cursor 后手动close()会导致 Loader 下次回调时数据失效。记住Loader 托管的 Cursor 不要手动关。排查时如果拿不准某段代码的关闭责任把代码贴到模型对话里问比翻文档快。API Keys 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 生成接入方式看 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。API 地址统一用https://taotoken.net/api。最后留一个我自己的习惯每次写完 Cursor 相关代码先搜一遍close()再搜一遍moveTo确认两者的行号顺序。这个动作花不了十秒但能挡掉大部分StaleDataException。
RELATED

相关推荐

【爱马仕】Hermes 代理工具 Windows 实操:TaoToken 统一 Key 配置与验证

【爱马仕】Hermes 代理工具 Windows 实操: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/27 19:25:00
webgl开源三维引擎的选择:TaoToken 统一 Key 接入 Three.js/Babylon.js/CesiumJS 的配置骨架

webgl开源三维引擎的选择:TaoToken 统一 Key 接入 Three.js/Babylon.js/CesiumJS 的配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/27 19:25:00
用于服务器测试的 MCP 开发工具:TaoToken 统一 Key 接入与 config.toml 配置骨架

用于服务器测试的 MCP 开发工具:TaoToken 统一 Key 接入与 config.toml 配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/27 19:25:00
MORE NEWS

更多资讯

📰

痞子衡嵌入式:turbo-spiboot 提速实践 - 基于 MCUBoot 协议的 SPI 加载 APP 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

搞定3类浏览器兼容坑 网站兼容浏览器服务哪家好

搞定3类浏览器兼容坑 网站兼容浏览器服务哪家好 网站做好了没人访问,往往不是内容不够好,而是用户打开就报错。很多老板问 网站兼容浏览器服务哪家好…

📰

Agnes 不做通用型智能体:Multi Agent 架构下 CodeAgents 的配置骨架与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

多语言版本各说各话,AI 只信了一半:workTranslation 与 translationOfWork 的 60 天引用数据

多语言版本各说各话,AI 只信了一半:workTranslation 与 translationOfWork 的 60 天引用数据 适用读者:给外贸独立站维护中英双语站的前端与 SEO 工程师;负责让 AI 搜索引擎正确引用自家产品资料的技术负责人;已经上了…

📰

新手避坑指南,Archify 接入 Cursor 与 Claude Code 的真实体验:TaoToken 统一 Key 配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

wordpress字段引入避坑指南

不会代码做WordPress?3步搞定字段引入,省下50%建站报价 自己不会代码想做网站,是不是看着那些后台参数就头大?很多老板在对比 建站报价 时,发现定制开发动辄几万,而模板站又显得太廉价,卡在中间不知所措。其实,WordPress…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬