记一次 CursorWindow 报错问题 -- IllegalStateException: Couldn't read row

656 阅读2分钟

在 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
       ...省略

从报错信息看

  1. 报错行数都是千行以上,且都是第一列,而第一列是自增 id 列
  2. Cursor 初始化异常

分析方向

  1. 报错行的第一列数据有异常 -- 不大可能,第一列是自增 id 列
  2. 读取数据库的代码有问题 -- 读取方法是 Room 框架自动生成的代码,如果是读取类型或读取的列不存在,应该在第一行就报错。所以不太可能
  3. 一次读取的数据量过大 -- 分析报错都是在千行以上,推测可能与此有关

最终发现是一次读取数据量过大的问题导致此 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。

解决方案:

  1. 只读取需要的字段,不要所有的选择语句都使用 SELECET *,减小单个数据量的大小
  2. 每一列的数据量应当尽可能小,不应当存储大量数据,如:存储图片二进制数据
  3. 分页读取,每一页的大小保证不超过 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
}