
1. 先搞清楚 runqueryonbackgroundthread close cursor! 到底在报什么runqueryonbackgroundthread close cursor!这个报错通常出现在你用了CursorAdapter尤其是SimpleCursorAdapter或自定义CursorAdapter配合LoaderManager、startManagingCursor或手动在后台线程跑查询的场景里。它的字面意思是系统在后台线程执行runQueryOnBackgroundThread时发现你传入的 Cursor 已经被关闭了于是抛出IllegalStateException或直接打印close cursor!的警告日志。很多同学第一次遇到它是在“杀死进程、清除缓存”之后重新打开 App列表页直接闪退或者白屏。原因并不神秘Cursor 的生命周期和 Activity/Fragment 的生命周期没有对齐。后台线程还在跑查询主线程已经把 Cursor 关掉了或者 Adapter 换了新 Cursor旧 Cursor 没被正确释放系统在后台线程复用时踩到了已关闭的对象。这个报错适合谁看适合所有还在用CursorAdapter做列表、又不想把整个数据层重写成 Room Flow 的 Android 开发者。你不需要推翻现有架构只要把 Cursor 的“谁创建、谁持有、谁关闭”这三件事理清楚再配一套统一的 Key/API 通道做联调验证就能一次性消除它。下面我会先给可复制的 Cursor 关闭检查清单再给 Logcat 过滤命令最后给 TaoToken 在settings.json里的配置骨架和验证动作。2. TaoToken 前置统一 Key/API 通道让联调不再靠猜排查 Cursor 泄漏时最怕的是“改完不知道有没有生效”。如果你同时还在调模型接口、跑 Agent 或做代码补全建议先把 API 通道统一到 TaoToken这样日志、Key、Base URL 都是一套验证动作可复用。TaoToken 是一个统一的大模型 API 通道官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 这个不加 UTM。它解决的是“多个模型、多个 Key、多个 Base URL 来回切”的问题你只需要在控制台生成一个 Key然后在settings.json或环境变量里指向同一个 API 地址就能在模型对话、Coding Plan、Agent 之间复用。对本次排查来说TaoToken 的作用是当你用 Logcat 确认 Cursor 已经不再泄漏后可以用同一个 Key 去请求模型对话接口确认“通道连通”避免把“网络问题”误判成“Cursor 问题”。控制台入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。如果你长期做编码和 Agent可以看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。注意TaoToken 是 API 通道不是编辑器替代品也不做任何灰色中转。你仍然在 Android Studio 里写代码只是把模型请求指向统一入口。3. 可复制配置Cursor 关闭检查清单 settings.json 骨架3.1 Cursor 关闭检查清单逐条对照先给结论runqueryonbackgroundthread close cursor!的根因90% 是下面五条里至少一条没做到。第一changeCursor里必须关闭旧 Cursor。很多人只调super.changeCursor(newCursor)忘了旧的那个。正确写法是先取旧 Cursor再判断是否同一个对象不同才关。Override public void changeCursor(Cursor newCursor) { Cursor oldCursor getCursor(); super.changeCursor(newCursor); if (oldCursor ! null oldCursor ! newCursor) { oldCursor.close(); } }第二如果你还在用activity.startManagingCursor(cursor)要确认 Activity 销毁时它会被自动关闭。但startManagingCursor已废弃推荐改用LoaderManager或CursorLoader让 Loader 自己管生命周期。第三后台线程查询结束后Cursor 的持有者必须是 Adapter 或 Loader不能是局部变量。局部变量出了作用域没人关后台线程再访问就是已关闭状态。第四onDestroy或onDestroyView里要显式adapter.changeCursor(null)把 Adapter 持有的 Cursor 释放掉再让 Loader 去关。第五swapCursor和changeCursor不要混用。swapCursor返回旧 Cursor 但不关闭需要你自己关changeCursor会帮你关。混用就会出现“以为关了其实没关”或“关了两次”。3.2 Logcat 过滤命令排查阶段先把日志过滤出来别被其他噪音淹没。在终端里执行adb logcat -c adb logcat | grep -iE runqueryonbackgroundthread|close cursor|CursorWindow|StaleDataException如果你想按进程过滤先拿 PIDadb shell pidof com.example.yourapp adb logcat --pid$(adb shell pidof com.example.yourapp) | grep -iE cursor|runqueryWindows 下把grep换成findstr /i。实测下来close cursor!往往和CursorWindow的分配日志挨在一起看到CursorWindow: Window is full就要警惕泄漏。3.3 TaoToken settings.json 配置骨架下面这份骨架可以直接复制到你的settings.json比如 Claude Code 或兼容工具的配置目录。把YOUR_TAOTOKEN_KEY换成你在 API Keys 页面生成的 Key。{ api: { base_url: https://taotoken.net/api, api_key: YOUR_TAOTOKEN_KEY, timeout_ms: 60000, max_retries: 2 }, models: { default: claude-sonnet-4-20250514, fallback: gpt-4o-mini }, logging: { level: info, log_requests: true } }如果你用的是环境变量方式等价写法是export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYYOUR_TAOTOKEN_KEY提示base_url只写https://taotoken.net/api不要在后面拼/v1或/chat/completions具体路径由客户端按模型协议补全。写错路径会返回 404容易被误判成 Key 失效。4. 验证请求与成功结果确认 Cursor 不再泄漏、通道连通4.1 验证 Cursor 是否还会泄漏改完changeCursor后按这个流程走一遍打开列表页 → 快速旋转屏幕三次 → 按 Home 键再回来 → 进入详情页再返回 → 反复五次。然后看 Logcatadb logcat -d | grep -iE close cursor|runqueryonbackgroundthread | wc -l如果输出是0说明这一轮没有触发。再配合adb shell dumpsys meminfo com.example.yourapp | grep -i cursor看 Cursor 数量是否稳定。稳定不增长基本可以确认泄漏点已堵住。4.2 验证 TaoToken 通道连通用curl发一个最小请求确认 Key 和 Base URL 都对curl -s -X POST https://taotoken.net/api/v1/messages \ -H Content-Type: application/json \ -H x-api-key: YOUR_TAOTOKEN_KEY \ -H anthropic-version: 2023-06-01 \ -d { model: claude-sonnet-4-20250514, max_tokens: 64, messages: [{role: user, content: ping}] }成功时你会看到类似{id:msg_...,content:[{type:text,text:pong}]}的返回。如果返回 401检查 Key 是否复制完整返回 404检查base_url是否多写了路径返回超时检查timeout_ms是否太短。你也可以直接在模型对话页面做一次可视化验证 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。输入一句话能正常返回就说明通道没问题。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各协议的路径说明。5. 本篇常见错排查close cursor! 反复出现的六个坑第一个坑在changeCursor里调了oldCursor.close()但oldCursor newCursor时也关了。这会把正在用的 Cursor 关掉后台线程立刻报close cursor!。所以判断条件必须是oldCursor ! newCursor。第二个坑CursorLoader的onLoadFinished里调adapter.changeCursor(cursor)但onLoaderReset里忘了调adapter.changeCursor(null)。Loader 重置时旧 Cursor 会被框架关闭Adapter 还拿着引用后台线程一访问就炸。第三个坑在子线程里手动cursor.close()主线程的 Adapter 还在用。Cursor 不是线程安全的关闭动作必须和持有者在同一生命周期里。第四个坑swapCursor返回的旧 Cursor 没关。swapCursor的语义是“换但不关”返回值就是旧 Cursor你必须自己close()。第五个坑startManagingCursor和手动close()同时用导致双重关闭。已废弃的startManagingCursor会在 Activity 销毁时自动关你再手动关一次就冲突。第六个坑把close cursor!当成网络错误去查 TaoToken 配置。这个报错和 API 通道无关先看 Cursor 生命周期再看通道。如果你确认 Cursor 已经干净但请求还是失败再去 API Keys 页面核对 Key https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。注意不要为了“快速消除报错”而把close()全部注释掉。那样只是把异常藏起来CursorWindow 会持续增长最终 OOM。6. 下一步把 Cursor 检查清单固化通道统一到 TaoToken把第 3.1 节的五条检查清单贴到你的代码 Review 模板里每次改 Adapter 都过一遍。Logcat 过滤命令存成 shell 脚本排查时直接跑。TaoToken 的settings.json骨架放在项目根目录的tools/下团队共用一份Key 走环境变量注入不要提交到 Git。如果你还在做长期编码和 Agent 任务建议把通道切到 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。它和本次的 Cursor 排查不冲突只是让你在验证“通道连通”时少配几套 Key。Claude Code 相关接入说明在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 需要的话按文档把base_url指到https://taotoken.net/api即可。最后留一个我踩过的坑close cursor!有时候不是你的代码直接引起的而是某个第三方库在ContentProvider里返回了已关闭的 Cursor。遇到这种情况先用adb shell dumpsys activity providers看哪个 Provider 被调用再顺着调用栈找。把 Cursor 的创建和关闭都收拢到 Loader 或 Adapter 一处问题就不会再反复。