Sqlite在Android上的一个Bug
SQLiteCantOpenDatabaseException: unable to open database file
先上log
1
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: com.company.product.database.sqlite.SQLiteCantOpenDatabaseException: unable to open database file (code 14)
2
3
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native Method)
4
5
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:913)
6
7
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:819)
8
9
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
10
11
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:159)
12
13
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:147)
14
15
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.AbstractCursor.moveToPosition(AbstractCursor.java:218)
16
17
12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.AbstractCursor.moveToFirst(AbstractCursor.java:258)
Copied!
先给出结论,
  • 这是sqlite在Android系统上的一个bug,在需要建立索引的sql语句频繁执行时,会发生这个异常。
  • (如果你是在SQLiteDatabase执行open()时看到的这个exception,那应该是线程冲突的问题,跟这篇文章讲的不是同一个)
  • 根本原因是sqlite临时文件目录不可用。
  • 解决方案是第一次建立连接时设置临时文件目录。
在项目里遇到了这样一个奇怪的crash,长期占据各个版本crash上报榜首,但在开发中一直不能重现。
在许多查DB的代码路径里,都会在moveToFirst(),getCount()等需要执行fillWindow的地方出现这个crash。
网络上的解决方案:
谷歌搜索SQLiteCantOpenDatabaseException,多是一些执行SQLiteDatabase open()时线程冲突的问题,与我们这个问题不同。
跟这个问题相关的回答屈指可数,一直没找到解决方案,最相关的两种回答来自github:
第一个链接与我们的情况相符,但是没有根本的解决方案,只有try – catch
第二个链接讲的是FD泄露导致打不开文件,于是我排查了app中各种泄露的地方,并且写了一个计算文件句柄数的上报工具,发现用户发生此类crash时,FD都不超过256,低于系统对单个进程默认FD数量1024的限制。排除这个可能。
(但有些时候也有可能是由这个问题引发的,可以用StrictMode detectLeak去排查)
于是先尝试在一些可能触发这个Exception的地方try-catch
再分析用户日志,发现try – catch住这个Exception后是可以继续执行一些DB查询的,
于是全都上了try – catch
重现路径
分析用户日志,发现用户的一些共性,由于业务保密限制这里总结一下,共性是DB中数据量很大,并且查询中有大量的子查询。
于是尝试重现这个问题:
在数据量很大的情况下,多次查询就会重现。
可以重现的话就可以开始打log了。
为了在sqlite native层打log,编译sqlite,使用sqlite3_log来输出自己想观察的信息。
首先我们可以看到sqlite的log
1
12-14 19:51:30.346 17770-18098/com.company.package E/SQLiteLog: (14) cannot open file at line 32440 of [bda77dda96]
2
3
12-14 19:51:30.346 17770-18098/com.company.package E/SQLiteLog: (14) os_unix.c:32440: (30) open(./etilqs_3P2SKRP0Ge6cj3T) -
4
5
12-14 19:51:30.346 17770-18098/com.company.package E/SQLiteLog: (14) statement aborts at 180: [SELECT M.*,…………………
Copied!
可以看到是打开一个”./etilqs_3P2SKRP0Ge6cj3T”的文件时打开失败。
先查查这个临时文件是什么鬼,
在sqlite3.c搜索前缀etilqs_里可以看到这样的注释:
1
/*
2
** Temporary files are named starting with this prefix followed by 16 random
3
** alphanumeric characters, and no file extension. They are stored in the
4
** OS's standard temporary file directory, and are deleted prior to exit.
5
** If sqlite is being embedded in another program, you may wish to change the
6
** prefix to reflect your program's name, so that if your program exits
7
** prematurely, old temporary files can be easily identified. This can be done
8
** using -DSQLITE_TEMP_FILE_PREFIX=myprefix_ on the compiler command line.
9
**
10
** 2006-10-31: The default prefix used to be "sqlite_". But then
11
** Mcafee started using SQLite in their anti-virus product and it
12
** started putting files with the "sqlite" name in the c:/temp folder.
13
** This annoyed many windows users. Those users would then do a
14
** Google search for "sqlite", find the telephone numbers of the
15
** developers and call to wake them up at night and complain.
16
** For this reason, the default name prefix is changed to be "sqlite"
17
** spelled backwards. So the temp files are still identified, but
18
** anybody smart enough to figure out the code is also likely smart
19
** enough to know that calling the developer will not help get rid
20
** of the file.
21
*/
22
#ifndef SQLITE_TEMP_FILE_PREFIX
23
# define SQLITE_TEMP_FILE_PREFIX "etilqs_"
24
#endif
Copied!
总之就是临时文件就对了。
临时文件源码追踪
然后找找这个东西在哪里用的,
1
/*
2
** Create a temporary file name in zBuf. zBuf must be allocated
3
** by the calling process and must be big enough to hold at least
4
** pVfs->mxPathname bytes.
5
*/
6
static int unixGetTempname(int nBuf, char *zBuf){
7
static const unsigned char zChars[] =
8
"abcdefghijklmnopqrstuvwxyz"
9
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
10
"0123456789";
11
unsigned int i, j;
12
const char *zDir;
13
14
/* It's odd to simulate an io-error here, but really this is just
15
** using the io-error infrastructure to test that SQLite handles this
16
** function failing.
17
*/
18
SimulateIOError( return SQLITE_IOERR );
19
20
zDir = unixTempFileDir();
21
if( zDir==0 ) zDir = ".";
22
23
/* Check that the output buffer is large enough for the temporary file
24
** name. If it is not, return SQLITE_ERROR.
25
*/
26
if( (strlen(zDir) + strlen(SQLITE_TEMP_FILE_PREFIX) + 18) >= (size_t)nBuf ){
27
return SQLITE_ERROR;
28
}
29
30
do{
31
sqlite3_snprintf(nBuf-18, zBuf, "%s/"SQLITE_TEMP_FILE_PREFIX, zDir);
32
j = (int)strlen(zBuf);
33
sqlite3_randomness(15, &zBuf[j]);
34
for(i=0; i<15; i++, j++){
35
zBuf[j] = (char)zChars[ ((unsigned char)zBuf[j])%(sizeof(zChars)-1) ];
36
}
37
zBuf[j] = 0;
38
zBuf[j+1] = 0;
39
}while( osAccess(zBuf,0)==0 );
40
return SQLITE_OK;
41
}
Copied!
这里可以留意到一个神奇的东西
zDir = unixTempFileDir();
if( zDir==0 ) zDir = "."; 我们的文件是 ./etilqs_3P2SKRP0Ge6cj3T
所以unixTempFileDir()确实是返回了0
那再看下unixTempFileDir();
1
/*
2
** Return the name of a directory in which to put temporary files.
3
** If no suitable temporary file directory can be found, return NULL.
4
*/
5
static const char *unixTempFileDir(void){
6
static const char *azDirs[] = {
7
0,
8
0,
9
0,
10
"/var/tmp",
11
"/usr/tmp",
12
"/tmp",
13
/* List terminator */
14
};
15
unsigned int i;
16
struct stat buf;
17
const char *zDir = 0;
18
19
azDirs[0] = sqlite3_temp_directory;
20
if( !azDirs[1] ) azDirs[1] = getenv("SQLITE_TMPDIR");
21
if( !azDirs[2] ) azDirs[2] = getenv("TMPDIR");
22
for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){
23
if( zDir==0 ) continue;
24
if( osStat(zDir, &buf) ) continue;
25
if( !S_ISDIR(buf.st_mode) ) continue;
26
if( osAccess(zDir, 07) ) continue;
27
break;
28
}
29
return zDir;
30
}
Copied!
azDirs[0]是sqlite3_temp_directory,我们没有设置过,
azDirs[1]和[2]是环境变量,用sqlite3_log打出来是
即环境变量里没有设置这两个值,
而另外三个目录/var/tmp,/usr/tmp,/tmp在Android系统里都是应用不可写的,
所以会返回0给unixGetTemp,
于是unixGetTemp使用了”.”作为临时文件的目录,
那”.”是哪个目录呢?
使用
1
system(“ls . > /sdcard/0.txt”);
Copied!
结果是:
1
acct
2
adb_keys
3
cache
4
config
5
d
6
data
7
default.prop
8
dev
9
etc
10
firmware
11
fstab.qcom
12
init
13
init.goldfish.rc
14
init.qcom.class_core.sh
15
init.qcom.class_main.sh
16
init.qcom.rc
17
init.qcom.sh
18
init.qcom.usb.rc
19
init.qcom.usb.sh
20
init.rc
21
init.target.rc
22
init.trace.rc
23
init.usb.rc
24
mnt
25
persist
26
proc
27
root
28
sbin
29
sdcard
30
storage
31
storage_int
32
sys
33
system
34
tombstones
35
ueventd.goldfish.rc
36
ueventd.qcom.rc
37
ueventd.rc
38
vendor
Copied!
这特么是根目录!当前工作目录是根目录我也是醉了。。。
所以在根目录创建临时文件一定会失败!
etilqs临时文件创建时机
那为什么平时使用都是正常的呢?
找一找这个临时文件的创建时机:
在unixGetTempname函数里,人为地造一个crash,通过crash堆栈配合addr2line来查看调用栈:
1
12-19 21:00:45.633 13680-14105/com.company.package E/SQLiteLog: (14) pagerstress;/data/data/com.company.package/databases/push
2
12-19 21:00:45.633 13680-14105/com.company.package E/SQLiteLog: (14) pager_write_pagelist
3
12-19 21:00:46.083 3727-3727/? I/DEBUG: #00 pc 00037202 /data/app-lib/com.company.package-1/libqmsqlite.so unixGetTempname 32107
4
12-19 21:00:46.083 3727-3727/? I/DEBUG: #01 pc 000376a7 /data/app-lib/com.company.package-1/libqmsqlite.so unixOpen 32396
5
12-19 21:00:46.083 3727-3727/? I/DEBUG: #02 pc 00015ec5 /data/app-lib/com.company.package-1/libqmsqlite.so sqlite3OsOpen 17420
6
12-19 21:00:46.083 3727-3727/? I/DEBUG: #03 pc 0003a16b /data/app-lib/com.company.package-1/libqmsqlite.so
7
12-19 21:00:46.093 3727-3727/? I/DEBUG: #04 pc 0003e0c7 /data/app-lib/com.company.package-1/libqmsqlite.so
8
12-19 21:00:46.093 3727-3727/? I/DEBUG: #05 pc 00038e75 /data/app-lib/com.company.package-1/libqmsqlite.so
9
12-19 21:00:46.093 3727-3727/? I/DEBUG: #06 pc 00038f55 /data/app-lib/com.company.package-1/libqmsqlite.so
10
12-19 21:00:46.093 3727-3727/? I/DEBUG: #07 pc 00039445 /data/app-lib/com.company.package-1/libqmsqlite.so
11
12-19 21:00:46.093 3727-3727/? I/DEBUG: #08 pc 0003add1 /data/app-lib/com.company.package-1/libqmsqlite.so
12
12-19 21:00:46.093 3727-3727/? I/DEBUG: #09 pc 0003c1f1 /data/app-lib/com.company.package-1/libqmsqlite.so
13
12-19 21:00:46.093 3727-3727/? I/DEBUG: #10 pc 0003d8df /data/app-lib/com.company.package-1/libqmsqlite.so
14
12-19 21:00:46.093 3727-3727/? I/DEBUG: #11 pc 0004c2e7 /data/app-lib/com.company.package-1/libqmsqlite.so
15
12-19 21:00:46.093 3727-3727/? I/DEBUG: #12 pc 0004e317 /data/app-lib/com.company.package-1/libqmsqlite.so (sqlite3_step+334)
16
12-19 21:00:46.093 3727-3727/? I/DEBUG: #13 pc 00063ebd /data/app-lib/com.company.package-1/libqmsqlite.so (sqlite3_blocking_step+6)
17
12-19 21:00:46.093 3727-3727/? I/DEBUG: #14 pc 00012279 /data/app-lib/com.company.package-1/libqmsqlite.so
18
12-19 21:00:46.103 3727-3727/? I/DEBUG: 61e75c04 61ced1f7 /data/app-lib/com.company.package-1/libqmsqlite.so
19
12-19 21:00:46.103 3727-3727/? I/DEBUG: 61e75c24 61ced6ab /data/app-lib/com.company.package-1/libqmsqlite.so
20
12-19 21:00:46.103 3727-3727/? I/DEBUG: 61e75c50 61d71f4c /data/app-lib/com.company.package-1/libqmsqlite.so
21
12-19 21:00:46.113 3727-3727/? I/DEBUG: 61e7610c 61cf016f /data/app-lib/com.company.package-1/libqmsqlite.so
Copied!
使用addr2line –C –f –e 加上面14个pc地址,结果:
1
pagerOpentemp
2
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:46566
3
pagerStress
4
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:47482
5
sqlite3PcacheFetchStress
6
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:40751
7
btreeGetPage
8
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:56428
9
btreeGetUnusedPage
10
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:56556
11
allocateBtreePage
12
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:60283
13
balance_nonroot
14
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:61869
15
sqlite3BtreeInsert
16
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:62554
17
sqlite3VdbeExec
18
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:77746 (discriminator 3)
19
sqlite3Step
20
/media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:71550
21
sqlite3_blocking_step
22
/media/Software/company/qmsqlite/jni/sqlite/sqlite3_unlock_notify.c:85 (discriminator 1)
23
nativeExecuteForCursorWindow
24
/media/Software/company/qmsqlite/jni/sqlite/SQLiteConnection.cpp:994
Copied!
懒得看图的童鞋还是听我说吧,
先看sqlite的architecture
因为我们crash的地方是查DB的地方,所以拿query操作来解释这个architecture是怎么运行的
先用SQL Command Processor解析sql语句,变成类似汇编的命令给Virtual Machine执行,
我们可以用explain plan select …. 这样的语句来查看virtual machine要执行的命令,比如
1
explain plan select * from A where A.a in (select b from B)
Copied!
对应的命令是:
1
0| Trace| 0| 0| 0| | 00
2
1| Goto| 0| 56| 0| | 00
3
2| OpenRead| 0| 4| 0| 13| 00
4
3| Rewind| 0| 54| 0| | 00
5
4| null| 0| 1| 0| | 00
6
5| Once| 0| 17| 0| | 00
7
6| null| 0| 1| 0| | 00
8
7| OpenEphemeral| 4| 1| 0| keyinfo(1,BINARY)| 00
9
8| Integer| 10000| 2| 0| | 00
10
9| OpenRead| 1| 5| 0| 1| 00
11
10| Rewind| 1| 16| 0| | 00
12
11| Column| 1| 0| 3| | 00
13
12| MakeRecord| 3| 1| 4| b| 00
14
13| IdxInsert| 4| 4| 0| | 00
15
14| IfZero| 2| 16| -1| | 00
16
15| Next| 1| 11| 0| | 01
17
16| Close| 1| 0| 0| | 00
18
17| Column| 0| 0| 4| | 00
19
18| IsNull| 4| 22| 0| | 00
20
19| Affinity| 4| 1| 0| b| 00
21
20| NotFound| 4| 22| 4| 1| 00
22
21| Goto| 0| 39| 0| | 00
23
22| null| 0| 5| 0| | 00
24
23| Once| 1| 35| 0| | 00
25
24| null| 0| 5| 0| | 00
26
25| OpenEphemeral| 6| 1| 0| keyinfo(1,BINARY)| 00
27
26| Integer| 10000| 6| 0| | 00
28
27| OpenRead| 2| 5| 0| 12| 00
29
28| Rewind| 2| 34| 0| | 00
30
29| Column| 2| 11| 7| | 00
31
30| MakeRecord| 7| 1| 4| b| 00
32
31| IdxInsert| 6| 4| 0| | 00
33
32| IfZero| 6| 34| -1| | 00
34
33| Next| 2| 29| 0| | 01
35
34| Close| 2| 0| 0| | 00
36
35| Column| 0| 1| 4| | 00
37
36| IsNull| 4| 53| 0| | 00
38
37| Affinity| 4| 1| 0| b| 00
39
38| NotFound| 6| 53| 4| 1| 00
40
39| Column| 0| 0| 8| | 00
41
40| Column| 0| 1| 9| | 00
42
41| Column| 0| 2| 10| | 00
43
42| Column| 0| 3| 11| | 00
44
43| Column| 0| 4| 12| | 00
45
44| Column| 0| 5| 13| | 00
46
45| Column| 0| 6| 14| | 00
47
46| Column| 0| 7| 15| | 00
48
47| Column| 0| 8| 16| | 00
49
48| Column| 0| 9| 17| | 00
50
49| Column| 0| 10| 18| | 00
51
50| Column| 0| 11| 19| | 00
52
51| Column| 0| 12| 20| | 00
53
52| ResultRow| 8| 13| 0| | 00
54
53| Next| 0| 4| 0| | 01
55
54| Close| 0| 0| 0| | 00
56
55| Halt| 0| 0| 0| | 00
57
56| Transaction| 0| 0| 0| | 00
58
57| VerifyCookie| 0| 3| 0| | 00
59
58| TableLock| 0| 4| 0| labels| 00
60
59| TableLock| 0| 5| 0| Items| 00
61
60| Goto| 0| 2| 0| | 00
Copied!
可以看到其中需要建立索引,IdxInsert,于是在sqlite3VdbeExec中会进入
OP_IdxInsert分支,然后
会调用sqlite3BtreeInsert,向B树中插入一个节点,
此时如果pPage满了,会执行balance平衡B树,
在这里面就会btreeGetPage去获取可用的page,
获取page的过程最终会执行sqlite3_malloc,为page分配空间,一旦分配失败,就会在fetch处触发pBase == 0的条件,
于是执行sqlite3PcacheFetchStress,在其中调用pager_write_pagelist时触发pPager->fd == 0的条件(因为page在前面没有分配到空间),
于是触发pagerOpenTemp,往下执行调用unixGetTempname,得到上面所说的那个不正确的文件路径,
执行sqlite3Osopen时就会失败。
从上面的分析看出,触发这个路径需要几个条件:
  • 执行的sql语句需要建立索引,
  • B树不平衡
  • 没有设置过环境变量
  • 分配的内存不足以新建新的page
    所以触发条件还是比较严格的。
unixOpenTempname执行时用一个变量计算临时文件的打开次数,也可以发现确实是一打开这样的文件就会失败(在打开第一个的时候就失败)。
解决方案(Solution)
那么最重要的事情来了,怎么修复呢?
既然是临时文件的目录没有写权限,那就改目录吧!
翻了翻sqlite的一些资料,找到了这样一个programa
1
PRAGMA temp_store_directory = 'your dir'
Copied!
这个东西仅对当前SqliteConncetion有效,
在第一次建立sqlite连接的时候(我是重写了getReadabelDatabase()方法),设置一下临时文件目录,like this:
1
private static boolean mainTmpDirSet = false;
2
@Override
3
public SQLiteDatabase getReadableDatabase() {
4
if (!mainTmpDirSet) {
5
boolean rs = new File("/data/data/com.cmp.pkg/databases/main").mkdir();
6
Log.d("ahang", rs + "");
7
super.getReadableDatabase().execSQL("PRAGMA temp_store_directory = '/data/data/com.cmp.pkg/databases/main'");
8
mainTmpDirSet = true;
9
return super.getReadableDatabase();
10
}
11
return super.getReadableDatabase();
12
}
Copied!
然后再去执行那些繁重的查询,你会发现问题消失了,
并且sqlite3会在不需要这个临时文件时自动删除它,所以你不需要做一套清理逻辑。
于是问题解决!
复制链接