在 Crash 收集平台上,使用 Room 进行数据库的读取时,会报以下异常。
Fatal Exception: java.lang.IllegalStateException: Couldn't read row 4922, col 0 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
at android.database.CursorWindow.nativeGetLong(CursorWindow.java)
at android.database.CursorWindow.getLong(CursorWindow.java:538)
at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:75
...省略
从报错信息看
- 报错行数都是千行以上,且都是第一列,而第一列是自增 id 列
- Cursor 初始化异常
分析方向
- 报错行的第一列数据有异常 -- 不大可能,第一列是自增 id 列
- 读取数据库的代码有问题 -- 读取方法是 Room 框架自动生成的代码,如果是读取类型或读取的列不存在,应该在第一行就报错。所以不太可能
- 一次读取的数据量过大 -- 分析报错都是在千行以上,推测可能与此有关
最终发现是一次读取数据量过大的问题导致此 Crash 的产生
Cursor 继承关系
Cursor → AbstractWindowedCursor → SQLiteCursor
在 Room 中使用的 SQLiteCursor 来读取数据,而 AbstractWindowedCursor 里边就是使用 CursorWindow 进行数据库的读取。
而在 CursorWindow 中有个 sCursorWindowSize 的静态变量,
// CursorWindow.java
private static int getCursorWindowSize() {
if (sCursorWindowSize < 0) {
//com.android.internal.R.integer.config_cursorWindowSize 为常量 2048,因此总大小为 2 MB。
sCursorWindowSize = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_cursorWindowSize) * 1024;
}
return sCursorWindowSize;
}
//构造方法
public CursorWindow(String name, @BytesLong long windowSizeBytes) {
mStartPos = 0;
mName = name != null && name.length() != 0 ? name : "<unnamed>";
//赋值给 mWindowPtr
mWindowPtr = nativeCreate(mName, (int) windowSizeBytes);
if (mWindowPtr == 0) {
throw new AssertionError(); // Not possible, the native code won't return it.
}
mCloseGuard.open("close");
recordNewWindow(Binder.getCallingPid(), mWindowPtr);
}
//每个读取方法都传递了 windowPtr,当当前读取的数据量大于设定时,就会报异常
private static native long nativeGetLong(long windowPtr, int row, int column);
sCursorWindowSize 为 2 MB,当一次读取超过此值时就有可能产生上述 crash。
解决方案:
- 只读取需要的字段,不要所有的选择语句都使用 SELECET *,减小单个数据量的大小
- 每一列的数据量应当尽可能小,不应当存储大量数据,如:存储图片二进制数据
- 分页读取,每一页的大小保证不超过 2MB
附上处理方案代码
//数据库读取方法改为分段读取
@Query("SELECT * FROM YOUR_TABLE_NAME WHERE LIMIT :limit OFFSET :offset")
fun getDataByTypePaging(
limit: Int,
offset: Int
): List<xx>
/**
* @param pageSize 每一页的数量
* @param totalAmount 总数量
* @param getData 分页读取的方法
* @return 返回数据集
*/
private fun <T> getDataByPaging(
pageSize: Int,
totalAmount: Int,
getData: (limit: Int, offset: Int) -> List<T>
): List<T> {
val list: MutableList<T> = mutableListOf()
var page = 0
val pageAmount = totalAmount / pageSize
do {
list.addAll(getData.invoke(pageSize, page * pageSize))
page++
} while (page < pageAmount)
val overSize = totalAmount % pageSize
if (overSize == 0) {
return list
} else {
list.addAll(getData.invoke(overSize, page * pageSize))
}
return list
}