SQLite IllegalStateException 的隐蔽 bug 修复

摘要:java.lang.IllegalStateException: Couldn’t read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.

发生场景:查询数据库

报错日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java.lang.RuntimeException: An error occurred while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:325)
at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:354)
at java.util.concurrent.FutureTask.setException(FutureTask.java:223)
at java.util.concurrent.FutureTask.run(FutureTask.java:242)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)
Caused by: java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
at android.database.CursorWindow.nativeGetLong(Native Method)
at android.database.CursorWindow.getLong(CursorWindow.java:511)
at android.database.CursorWindow.getInt(CursorWindow.java:578)
at android.database.AbstractWindowedCursor.getInt(AbstractWindowedCursor.java:69)
at com.pplive.media.upload.db.UploadDataBaseManager.searchAllUploads(UploadDataBaseManager.java:146)
at com.pplive.media.upload.UploadManager.searchAllUploads(UploadManager.java:400)
at com.suning.mobile.ebuy.uvideo.MainActivity$GetReLunchDataTask.doInBackground(MainActivity.java:1201)
at com.suning.mobile.ebuy.uvideo.MainActivity$GetReLunchDataTask.doInBackground(MainActivity.java:1196)
at android.os.AsyncTask$2.call(AsyncTask.java:305)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
... 4 more
java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it.
at android.database.CursorWindow.nativeGetLong(Native Method)
at android.database.CursorWindow.getLong(CursorWindow.java:511)
at android.database.CursorWindow.getInt(CursorWindow.java:578)
at android.database.AbstractWindowedCursor.getInt(AbstractWindowedCursor.java:69)
at com.pplive.media.upload.db.UploadDataBaseManager.searchAllUploads(UploadDataBaseManager.java:146)
at com.pplive.media.upload.UploadManager.searchAllUploads(UploadManager.java:400)
at com.xxxx.zzzz.cccc.uvideo.MainActivity$GetReLunchDataTask.doInBackground(MainActivity.java:1201)
at com.xxxx.zzzz.cccc.uvideo.MainActivity$GetReLunchDataTask.doInBackground(MainActivity.java:1196)
at android.os.AsyncTask$2.call(AsyncTask.java:305)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)

最开始一看这个日志「Couldn’t read row 0, col -1 from CursorWindow. Make sure the Cursor is initialized correctly before accessing data from it」,应该是 SQLite 查询的时候字段写错了吧,因为这个情况之前也遇到过,所以 google 验证一下,确实很多都是因为字段写错了。于是我返回代码中去查看字段。

很遗憾的是,数据库相关字段我都是用 public static final String XXXX 的形式来定义,并且在创建数据库和查询表的时候,都是使用的 XXXX ,那么照理来说是不会出现建表和查询时,填错大小写或者多一个空格少一个空格的情况,那么这种情况被 pass。

接着又去检查 SQL 语句,因为之前会碰到一个 SQL 语句当中插入一个值,而没有用空格,导致的 SQL 语句不正确的情况,例如:”select from”+TABLENAME ,这样就不对,应该是:”select from “+TABLENAME,但是如果是 SQL 语句的问题,那么不应该是报这个错误呀,应该会报 SQL 语句相关的错误才对,于是这个情况也被 pass。

于是接着从日志当中的 -1 入手,cursor 的 getColumnIndex() 方法注解是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
* If you expect the column to exist use {@link #getColumnIndexOrThrow(String)} instead, which
* will make the error more clear.
*
* @param columnName the name of the target column.
* @return the zero-based column index for the given column name, or -1 if
* the column name does not exist.
* @see #getColumnIndexOrThrow(String)
*/
int getColumnIndex(String columnName);

这么说,当前列如果不存在的话,那么这个方法的返回值是 -1 ,那么上面的报错日志就合理啦,于是我在查询之前打印了一下当前数据库的情况:

1
2
UploadInfo{status=0, ppfeature='null', fid='null', categoryId=0, state=0, size='null', name='null', localPath='null', id=6, progress=0, token='null', isPause=false, channel_web_id='null', pic_upload_url='null', startTime=1516106247809, length=0, channelId=0, userId=0, maxSpeed=0, minSpeed=0, fileState=0, isStop=false}

对的,没问题,我要查询的那一列是存在的,并且这里的字段和建表语句是符合的。要查询的那一列不存在这种情况也被 pass。

到了这里,我感觉我已经把可能出现问题的地方都检查过了,但是还有一个地方,日志当中有这么一句:

at android.database.AbstractWindowedCursor.getInt(AbstractWindowedCursor.java:69)

是不是 getInt 的问题?? 看看源码注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Returns the value of the requested column as an int.
*
* <p>The result and whether this method throws an exception when the
* column value is null, the column type is not an integral type, or the
* integer value is outside the range [<code>Integer.MIN_VALUE</code>,
* <code>Integer.MAX_VALUE</code>] is implementation-defined.
*
* @param columnIndex the zero-based index of the target column.
* @return the value of that column as an int.
*/
int getInt(int columnIndex);

当结果为默认值」,「列值为空」,「不是 integer 类型」, 「integer 类型超出范围」,这几种情况下,该方法会不会引起异常,是由现实情况决定的。

现实情况决定的??那就是可以理解成,跟设备相关咯(这个想法后来被证实是正确的,确实跟设备型号相关,上述异常在部分设备上不会抛出)。

那么这个注释的意思就是说:上述四种情况,我们去获取它,那么结果在不同的设备上不一样,有的设备上是可以正常获取到 SQLite 中定义的初始值的,有的就会直接抛异常。

以上几种情况,根据项目代码的一些限定和过滤以及之前的单元测试(业务相关),只有「当结果为默认值」这种情况可能会发生,也就是程序只创建了数据库和数据表,但是还没有往里面插入数据,就查询数据,那么这时数据表中的字段就应该是默认值。

怎么解决?

既然出问题的是默认值,那么我就在应该返回默认值的时候,手动去赋一个默认值。

保证每次调用 cursor 的 getXXX 方法时,都需要检查返回值是否为空。用下面的代码去替代系统 cursor 的 getXXX 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private long getLong(Cursor cursor, String indexName) {
long value = 0;
int columnIndex = -1;
try {
columnIndex = cursor.getColumnIndex(indexName);
if (!cursor.isNull(columnIndex)) {
value = cursor.getLong(columnIndex);
}
} catch (Throwable tr) {
LogUtils.error("columnIndex:" + columnIndex + "e:" + tr.getMessage());
}
return value;
}
private int getInt(Cursor cursor, String indexName) {
int value = 0;
int columnIndex = -1;
try {
columnIndex = cursor.getColumnIndex(indexName);
if (!cursor.isNull(columnIndex)) {
value = cursor.getInt(columnIndex);
}
} catch (Throwable tr) {
LogUtils.error("columnIndex:" + columnIndex + "e:" + tr.getMessage());
}
return value;
}
private String getString(Cursor cursor, String indexName) {
String value = null;
int columnIndex = -1;
try {
columnIndex = cursor.getColumnIndex(indexName);
if (!cursor.isNull(columnIndex)) {
value = cursor.getString(columnIndex);
}
} catch (Throwable tr) {
LogUtils.error("columnIndex:" + columnIndex + "e:" + tr.getMessage());
}
return value;
}

使用后,异常完美解决。

还有一种思路没有实践: 既然是默认值的问题,那么在 SQLiteOpenHelper 的 onCreate 方法中建表的时候,就将默认值写死就好啦,就是在每一个字段的建表语句后加上一个 DEFAULT ,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(String.format(
"CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " // BaseColumns._ID
+ "%s TEXT DEFAULT null, " // FIELD_LOCAL_PATH
+ "%s TEXT DEFAULT null, " // FIELD_NAME
+ "%s TEXT DEFAULT null, " // FIELD_TOKEN
+ "%s TEXT DEFAULT null, " // FIELD_SIZE
+ "%s INTEGER DEFAULT 0, " // FIELD_STATE
+ "%s TEXT DEFAULT null, " // FIELD_PPFEATURE
+ "%s TEXT DEFAULT null, " // FIELD_FID
+ "%s TEXT DEFAULT null, "// FIELD_CATEGORY_ID
+ "%s TEXT DEFAULT null, "// FIELD_CHANNEL_WEB_ID
+ "%s INTEGERvDEFAULT 0, " // CHANNEL_ID
TABLE_UPLOAD, BaseColumns._ID, FIELD_LOCAL_PATH, FIELD_NAME, FIELD_TOKEN, FIELD_SIZE, FIELD_STATE,
FIELD_PPFEATURE, FIELD_FID, FIELD_CATEGORY_ID, FIELD_CHANNEL_WEB_ID,CHANNEL_ID));
}

后面可以实践下。

共82.3k字
0%
.gt-container a{border-bottom: none;}