☰
Android CursorWindow: Window is full 报错排查:从 requested allocation 到 config_cursorWindowSize 调优
2026/10/12 19:30:48 网站建设 项目流程

1. 先看清 CursorWindow: Window is full 到底在报什么

CursorWindow: Window is full: requested allocation 1354087 bytes, free space 141032 bytes, window size 2097152 bytes这行日志,是 Android SQLite 在把查询结果从 native 层搬回 Java 层时抛出的。它不是一个普通的 SQL 语法错误,而是内存窗口装不下当前这一行数据。核心检索词就是 Android CursorWindow 窗口溢出,能帮你确认问题方向:SQLite 查询结果集或单行字段太大,超过了 CursorWindow 的容量上限。

先拆日志里的三个数字。window size 2097152 bytes就是 2MB,这是 Android 源码里config_cursorWindowSize的默认值,定义在frameworks/base/core/res/res/values/configs.xml:

<!-- When a database query is executed, the results returned are paginated in pages of size (in KB) indicated by this value --> <integer name="config_cursorWindowSize">2048</integer>

单位是 KB,2048KB 就是 2MB。requested allocation 1354087 bytes是这次要往窗口里塞的数据量,约 1.29MB。free space 141032 bytes是当前窗口剩余空间,约 137KB。1.29MB 要放进只剩 137KB 的窗口,自然放不下,于是 native 层直接放弃并打日志。

这里有个容易误解的点:窗口是 2MB,为什么剩余只有 137KB?因为 CursorWindow 是复用的,前面几行已经占掉了大部分空间。当某一行特别大,或者累计行数太多把窗口填满,再取下一行时就会触发。所以报错不一定代表单行超过 2MB,也可能是「窗口被前面的行占满 + 当前行偏大」共同作用。

适合谁看:正在做 Android 本地存储、用 SQLiteDatabase 或 Room 存了图片/JSON/长文本、查询时偶发这个 warning 甚至直接抛SQLiteBlobTooBigException或IllegalStateException: Couldn't read row的开发者。下面我会从复现、定位、配置到验证一步步走,配置片段可以直接复制。

2. 复现与定位:用 requested allocation 反推是哪一行太大

要排查,先得稳定复现。最直接的方式是往表里塞一条大字段,然后查询。假设你有一张note表:

CREATE TABLE note ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT );

插入一条 content 约 1.3MB 的记录:

SQLiteDatabase db = helper.getWritableDatabase(); String big = new String(new char[1_300_000]).replace('\0', 'A'); ContentValues cv = new ContentValues(); cv.put("title", "big row"); cv.put("content", big); db.insert("note", null, cv);

然后查询:

Cursor cursor = db.query("note", null, null, null, null, null, null); while (cursor.moveToNext()) { String content = cursor.getString(cursor.getColumnIndexOrThrow("content")); } cursor.close();

执行后 Logcat 里就会出现Window is full。注意,moveToNext()本身可能不报,真正触发读取的是getString,因为这时才需要把该行数据从窗口取出来。

定位是哪一行、哪个字段太大,有几个实用手段。第一,用cursor.getColumnCount()和逐列读取,把大字段单独拎出来测。第二,直接查字段长度:

SELECT id, length(content) AS len FROM note ORDER BY len DESC LIMIT 10;

length()对 TEXT 返回字符数,对 BLOB 返回字节数。把 len 最大的几条列出来,基本就能锁定元凶。第三,如果你怀疑是结果集总行数太多而不是单行太大,可以先用SELECT COUNT(*)看总量,再分页取。

我踩过的坑是:一开始只盯着单行 2MB 上限,结果发现单行才 300KB,但一次查了 8000 行,窗口在翻页过程中被填满,照样报。所以判断标准要分两种:单行超限,和累计超限。前者靠length()排序,后者靠分页 + 限制列。

还有一个隐藏点:getColumnIndexOrThrow拿到的列如果包含大 BLOB,即使你不读它,某些实现也会在窗口里预留空间。所以查询时尽量只 SELECT 需要的列,别用SELECT *。

3. 可复制配置:config_cursorWindowSize 调优与查询改造

调优分两个层面:系统级窗口大小,和应用级查询策略。系统级config_cursorWindowSize是 ROM 编译期配置,普通应用改不了,但你可以通过CursorWindow的构造或反射在部分场景影响,更稳妥的是从查询侧控制。

先给一个可直接复制的查询改造片段,核心是分页 + 只取必要列:

public List<Note> queryPaged(SQLiteDatabase db, int limit, int offset) { List<Note> list = new ArrayList<>(); String sql = "SELECT id, title, substr(content, 1, 4096) AS preview " + "FROM note ORDER BY id LIMIT ? OFFSET ?"; Cursor cursor = db.rawQuery(sql, new String[]{String.valueOf(limit), String.valueOf(offset)}); try { while (cursor.moveToNext()) { Note n = new Note(); n.id = cursor.getLong(0); n.title = cursor.getString(1); n.preview = cursor.getString(2); list.add(n); } } finally { cursor.close(); } return list; }

substr(content, 1, 4096)只取前 4096 个字符做预览,真正需要全文时再按 id 单独查。这样单行进入窗口的数据量被压到几 KB,窗口不会被撑爆。

如果你确实需要调大窗口,可以在自定义 ROM 或系统应用里改configs.xml,把 2048 改成 4096 或 8192。但要注意,这个值越大,每个 Cursor 占用的内存越多,多 Cursor 并发时容易 OOM。所以它不是万能药,只是给单行略超 2MB 的场景留余量。

对于 Room 用户,可以用@Query配合LIMIT:

@Query("SELECT id, title, substr(content, 1, 4096) AS preview FROM note ORDER BY id LIMIT :limit OFFSET :offset") fun loadPage(limit: Int, offset: Int): List<NotePreview>

另外,如果字段是 BLOB 存文件,强烈建议改成存文件路径。数据库里只放file_path TEXT,读取时用FileInputStream。这比调窗口大小更治本。

一个可复制的config_cursorWindowSize修改示例(仅系统/ROM 开发):

<!-- frameworks/base/core/res/res/values/configs.xml --> <integer name="config_cursorWindowSize">4096</integer>

改完需要重新编译系统镜像,普通 App 无法在运行时改这个值。所以应用层能做的,还是查询裁剪和分页。

4. 验证请求与成功结果:确认窗口不再溢出

改完之后怎么验证?最直接的是重跑之前的复现代码,观察 Logcat 是否还有Window is full。同时加一段日志打印每行读取的字节数:

int idx = cursor.getColumnIndexOrThrow("content"); while (cursor.moveToNext()) { String content = cursor.getString(idx); Log.d("CursorCheck", "row=" + cursor.getPosition() + " len=" + content.length() + " bytes=" + content.getBytes(StandardCharsets.UTF_8).length); }

成功的结果是:日志里每行 bytes 都远小于窗口剩余空间,且不再出现Window is full。如果用的是分页查询,可以打印总页数和每页行数,确认没有一次性加载全部数据。

再给一个更严格的验证:用adb shell查看进程内存,确认没有因为调大窗口导致内存飙升。

adb shell dumpsys meminfo com.example.app | grep -i "sqlite\|cursor"

如果之前是SQLiteBlobTooBigException,改造后应该变成正常返回。注意,Window is full有时只是 warning,程序还能继续跑,但数据可能被截断或读取失败,所以不能因为没崩溃就忽略。

验证时还要覆盖边界:空表、单行超大、多行累计超大、并发查询。我实测下来,最容易漏的是并发场景——两个 Cursor 同时打开,各自占一个窗口,内存翻倍。所以验证脚本里最好模拟两个线程同时查询。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 对照

虽然本篇主题是 CursorWindow,但很多同学在接入远程模型或调试工具时会混入其他报错。这里做一组对照,避免你被无关日志带偏。

401 Unauthorized:通常出现在调用远程 API 时,Key 无效或过期。检查请求头里的 Authorization 是否正确,Base URL 是否拼错。这跟 CursorWindow 无关,别往 SQLite 方向查。

local proxy failed:本地代理启动失败,常见于端口被占用或配置路径不对。检查端口是否被其他进程占用,配置文件路径是否存在。

reading choices:解析模型返回时字段缺失,通常是响应体不是预期 JSON。打印原始响应体确认。

OAuth相关报错:token 过期或回调地址不匹配。重新走授权流程,确认 redirect URI 与注册一致。

回到 CursorWindow 本身,常见错还有几个。Couldn't read row 0, col 0 from CursorWindow:窗口根本没分配成功,可能是查询返回了超大 BLOB。Row too big to fit into CursorWindow:单行超过窗口上限,必须裁剪字段。IllegalStateException: attempt to re-open an already-closed object:Cursor 提前关闭,跟窗口无关但常一起出现。

排查顺序建议:先看日志里的 requested allocation 和 free space,判断是单行大还是累计大;再用length()排序定位字段;最后决定是裁剪查询还是调窗口。别一上来就改config_cursorWindowSize,那个改动成本高且不一定对症。

如果你在接入远程模型做本地数据问答,遇到 401 或 OAuth 报错,可以对照 TaoToken 的接入文档确认 Base URL 和 Key 配置。文档地址:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。需要生成 Key 的话在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。验证模型连通性可以用模型对话页:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。长期做编码 Agent 的可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite 。

6. 从窗口溢出到存储设计:把大字段请出数据库

CursorWindow 溢出的根因,往往不是窗口太小,而是存储设计把不该放数据库的东西放了进去。图片、音频、长视频、几 MB 的 JSON,这些都不适合直接塞进 SQLite 的 TEXT/BLOB 列。数据库擅长的是结构化、小字段、高频查询;文件系统擅长的是大块二进制。把两者混在一起,窗口溢出只是第一个报错,后面还有备份慢、迁移难、内存高等一连串问题。

我的建议是:数据库里只存元数据和文件路径,文件放getFilesDir()或getExternalFilesDir()。查询时先取路径,需要时再异步加载文件。这样单行数据通常只有几百字节,窗口永远不会满。

如果业务上确实需要把大文本放数据库,至少做三件事:查询时用substr裁剪、分页加载、限制并发 Cursor 数量。这三招组合下来,2MB 窗口基本够用。

最后给一个检查清单,方便你落地:

检查项做法预期
单行字段大小SELECT length(col) ORDER BY DESC最大行 < 1MB
查询列避免SELECT *只取必要列
分页LIMIT/OFFSET或LIMIT ?每页行数可控
大字段存文件路径数据库行变小
并发 Cursor及时 close窗口不叠加

按这个清单走一遍,Window is full基本不会再出现。真遇到单行就是超大的历史数据,再考虑调config_cursorWindowSize,但那是最后手段,不是第一选择。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询