更多內容在這里查看
https://ahangchen.gitbooks.io/windy-afternoon/content/
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) 12-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native Method) 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) 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) 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) 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-14 19:51:30.346 17770-18098/com.company.product W/System.err: at com.company.product.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:147) 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) 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)
先給出結論,
這是sqlite在Android系統上的一個bug,在需要建立索引的sql語句頻繁執行時,會發生這個異常。
(如果你是在SQLiteDatabase執行open()時看到的這個exception,那應該是線程沖突的問題,跟這篇文章講的不是同一個)
根本原因是sqlite臨時文件目錄不可用。
解決方案是第一次建立連接時設置臨時文件目錄。
在項目里遇到了這樣一個奇怪的crash,長期占據各個版本crash上報榜首,但在開發中一直不能重現。
在許多查DB的代碼路徑里,都會在moveToFirst(),getCount()等需要執行fillWindow的地方出現這個crash。
網絡上的解決方案:
谷歌搜索SQLiteCantOpenDatabaseException,多是一些執行SQLiteDatabase open()時線程沖突的問題,與我們這個問題不同。
跟這個問題相關的回答屈指可數,一直沒找到解決方案,最相關的兩種回答來自github:
https://github.com/Raizlabs/DBFlow/issues/380
https://github.com/dxopt/OpenFilesLeakTest/blob/master/bugs-show/AbstractCursor.moveToFirst.md
第一個鏈接與我們的情況相符,但是沒有根本的解決方案,只有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
12-14 19:51:30.346 17770-18098/com.company.package E/SQLiteLog: (14) cannot open file at line 32440 of [bda77dda96] 12-14 19:51:30.346 17770-18098/com.company.package E/SQLiteLog: (14) os_unix.c:32440: (30) open(./etilqs_3P2SKRP0Ge6cj3T) - 12-14 19:51:30.346 17770-18098/com.company.package E/SQLiteLog: (14) statement aborts at 180: [SELECT M.*,…………………
可以看到是打開一個”./etilqs_3P2SKRP0Ge6cj3T”的文件時打開失敗。
先查查這個臨時文件是什么鬼,
在sqlite3.c搜索前綴etilqs_里可以看到這樣的注釋:
/* ** Temporary files are named starting with this prefix followed by 16 random ** alphanumeric characters, and no file extension. They are stored in the ** OS's standard temporary file directory, and are deleted prior to exit. ** If sqlite is being embedded in another program, you may wish to change the ** prefix to reflect your program's name, so that if your program exits ** prematurely, old temporary files can be easily identified. This can be done ** using -DSQLITE_TEMP_FILE_PREFIX=myprefix_ on the compiler command line. ** ** 2006-10-31: The default prefix used to be "sqlite_". But then ** Mcafee started using SQLite in their anti-virus product and it ** started putting files with the "sqlite" name in the c:/temp folder. ** This annoyed many windows users. Those users would then do a ** Google search for "sqlite", find the telephone numbers of the ** developers and call to wake them up at night and complain. ** For this reason, the default name prefix is changed to be "sqlite" ** spelled backwards. So the temp files are still identified, but ** anybody smart enough to figure out the code is also likely smart ** enough to know that calling the developer will not help get rid ** of the file. */ #ifndef SQLITE_TEMP_FILE_PREFIX # define SQLITE_TEMP_FILE_PREFIX "etilqs_" #endif
總之就是臨時文件就對了。
臨時文件源碼追蹤
然后找找這個東西在哪里用的,
/* ** Create a temporary file name in zBuf. zBuf must be allocated ** by the calling process and must be big enough to hold at least ** pVfs->mxPathname bytes. */ static int unixGetTempname(int nBuf, char *zBuf){ static const unsigned char zChars[] = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789"; unsigned int i, j; const char *zDir; /* It's odd to simulate an io-error here, but really this is just ** using the io-error infrastructure to test that SQLite handles this ** function failing. */ SimulateIOError( return SQLITE_IOERR ); zDir = unixTempFileDir(); if( zDir==0 ) zDir = "."; /* Check that the output buffer is large enough for the temporary file ** name. If it is not, return SQLITE_ERROR. */ if( (strlen(zDir) + strlen(SQLITE_TEMP_FILE_PREFIX) + 18) >= (size_t)nBuf ){ return SQLITE_ERROR; } do{ sqlite3_snprintf(nBuf-18, zBuf, "%s/"SQLITE_TEMP_FILE_PREFIX, zDir); j = (int)strlen(zBuf); sqlite3_randomness(15, &zBuf[j]); for(i=0; i<15; i++, j++){ zBuf[j] = (char)zChars[ ((unsigned char)zBuf[j])%(sizeof(zChars)-1) ]; } zBuf[j] = 0; zBuf[j+1] = 0; }while( osAccess(zBuf,0)==0 ); return SQLITE_OK; }
這里可以留意到一個神奇的東西
zDir = unixTempFileDir(); if( zDir==0 ) zDir = ".";
我們的文件是 ./etilqs_3P2SKRP0Ge6cj3T
所以unixTempFileDir()確實是返回了0
那再看下unixTempFileDir();
/* ** Return the name of a directory in which to put temporary files. ** If no suitable temporary file directory can be found, return NULL. */ static const char *unixTempFileDir(void){ static const char *azDirs[] = { 0, 0, 0, "/var/tmp", "/usr/tmp", "/tmp", 0 /* List terminator */ }; unsigned int i; struct stat buf; const char *zDir = 0; azDirs[0] = sqlite3_temp_directory; if( !azDirs[1] ) azDirs[1] = getenv("SQLITE_TMPDIR"); if( !azDirs[2] ) azDirs[2] = getenv("TMPDIR"); for(i=0; i<sizeof(azDirs)/sizeof(azDirs[0]); zDir=azDirs[i++]){ if( zDir==0 ) continue; if( osStat(zDir, &buf) ) continue; if( !S_ISDIR(buf.st_mode) ) continue; if( osAccess(zDir, 07) ) continue; break; } return zDir; }
azDirs[0]是sqlite3_temp_directory,我們沒有設置過,
azDirs[1]和[2]是環境變量,用sqlite3_log打出來是
即環境變量里沒有設置這兩個值,
而另外三個目錄/var/tmp,/usr/tmp,/tmp在Android系統里都是應用不可寫的,
所以會返回0給unixGetTemp,
於是unixGetTemp使用了”.”作為臨時文件的目錄,
那”.”是哪個目錄呢?
使用
system(“ls . > /sdcard/0.txt”);
結果是:
acct adb_keys cache config d data default.prop dev etc firmware fstab.qcom init init.goldfish.rc init.qcom.class_core.sh init.qcom.class_main.sh init.qcom.rc init.qcom.sh init.qcom.usb.rc init.qcom.usb.sh init.rc init.target.rc init.trace.rc init.usb.rc mnt persist proc root sbin sdcard storage storage_int sys system tombstones ueventd.goldfish.rc ueventd.qcom.rc ueventd.rc vendor
這特么是根目錄!當前工作目錄是根目錄我也是醉了。。。
所以在根目錄創建臨時文件一定會失敗!
etilqs臨時文件創建時機
那為什么平時使用都是正常的呢?
找一找這個臨時文件的創建時機:
在unixGetTempname函數里,人為地造一個crash,通過crash堆棧配合addr2line來查看調用棧:
12-19 21:00:45.633 13680-14105/com.company.package E/SQLiteLog: (14) pagerstress;/data/data/com.company.package/databases/push 12-19 21:00:45.633 13680-14105/com.company.package E/SQLiteLog: (14) pager_write_pagelist 12-19 21:00:46.083 3727-3727/? I/DEBUG: #00 pc 00037202 /data/app-lib/com.company.package-1/libqmsqlite.so unixGetTempname 32107 12-19 21:00:46.083 3727-3727/? I/DEBUG: #01 pc 000376a7 /data/app-lib/com.company.package-1/libqmsqlite.so unixOpen 32396 12-19 21:00:46.083 3727-3727/? I/DEBUG: #02 pc 00015ec5 /data/app-lib/com.company.package-1/libqmsqlite.so sqlite3OsOpen 17420 12-19 21:00:46.083 3727-3727/? I/DEBUG: #03 pc 0003a16b /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.093 3727-3727/? I/DEBUG: #04 pc 0003e0c7 /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.093 3727-3727/? I/DEBUG: #05 pc 00038e75 /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.093 3727-3727/? I/DEBUG: #06 pc 00038f55 /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.093 3727-3727/? I/DEBUG: #07 pc 00039445 /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.093 3727-3727/? I/DEBUG: #08 pc 0003add1 /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.093 3727-3727/? I/DEBUG: #09 pc 0003c1f1 /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.093 3727-3727/? I/DEBUG: #10 pc 0003d8df /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.093 3727-3727/? I/DEBUG: #11 pc 0004c2e7 /data/app-lib/com.company.package-1/libqmsqlite.so 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) 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) 12-19 21:00:46.093 3727-3727/? I/DEBUG: #14 pc 00012279 /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.103 3727-3727/? I/DEBUG: 61e75c04 61ced1f7 /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.103 3727-3727/? I/DEBUG: 61e75c24 61ced6ab /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.103 3727-3727/? I/DEBUG: 61e75c50 61d71f4c /data/app-lib/com.company.package-1/libqmsqlite.so 12-19 21:00:46.113 3727-3727/? I/DEBUG: 61e7610c 61cf016f /data/app-lib/com.company.package-1/libqmsqlite.so
使用addr2line –C –f –e 加上面14個pc地址,結果:
pagerOpentemp /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:46566 pagerStress /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:47482 sqlite3PcacheFetchStress /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:40751 btreeGetPage /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:56428 btreeGetUnusedPage /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:56556 allocateBtreePage /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:60283 balance_nonroot /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:61869 sqlite3BtreeInsert /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:62554 sqlite3VdbeExec /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:77746 (discriminator 3) sqlite3Step /media/Software/company/qmsqlite/jni/sqlite/sqlite3.c:71550 sqlite3_blocking_step /media/Software/company/qmsqlite/jni/sqlite/sqlite3_unlock_notify.c:85 (discriminator 1) nativeExecuteForCursorWindow /media/Software/company/qmsqlite/jni/sqlite/SQLiteConnection.cpp:994
整理了一發流程圖如下:
懶得看圖的童鞋還是聽我說吧,
先看sqlite的architecture
因為我們crash的地方是查DB的地方,所以拿query操作來解釋這個architecture是怎么運行的
先用SQL Command Processor解析sql語句,變成類似匯編的命令給Virtual Machine執行,
我們可以用explain plan select …. 這樣的語句來查看virtual machine要執行的命令,比如
explain plan select * from A where A.a in (select b from B)
對應的命令是:
0| Trace| 0| 0| 0| | 00 1| Goto| 0| 56| 0| | 00 2| OpenRead| 0| 4| 0| 13| 00 3| Rewind| 0| 54| 0| | 00 4| null| 0| 1| 0| | 00 5| Once| 0| 17| 0| | 00 6| null| 0| 1| 0| | 00 7| OpenEphemeral| 4| 1| 0| keyinfo(1,BINARY)| 00 8| Integer| 10000| 2| 0| | 00 9| OpenRead| 1| 5| 0| 1| 00 10| Rewind| 1| 16| 0| | 00 11| Column| 1| 0| 3| | 00 12| MakeRecord| 3| 1| 4| b| 00 13| IdxInsert| 4| 4| 0| | 00 14| IfZero| 2| 16| -1| | 00 15| Next| 1| 11| 0| | 01 16| Close| 1| 0| 0| | 00 17| Column| 0| 0| 4| | 00 18| IsNull| 4| 22| 0| | 00 19| Affinity| 4| 1| 0| b| 00 20| NotFound| 4| 22| 4| 1| 00 21| Goto| 0| 39| 0| | 00 22| null| 0| 5| 0| | 00 23| Once| 1| 35| 0| | 00 24| null| 0| 5| 0| | 00 25| OpenEphemeral| 6| 1| 0| keyinfo(1,BINARY)| 00 26| Integer| 10000| 6| 0| | 00 27| OpenRead| 2| 5| 0| 12| 00 28| Rewind| 2| 34| 0| | 00 29| Column| 2| 11| 7| | 00 30| MakeRecord| 7| 1| 4| b| 00 31| IdxInsert| 6| 4| 0| | 00 32| IfZero| 6| 34| -1| | 00 33| Next| 2| 29| 0| | 01 34| Close| 2| 0| 0| | 00 35| Column| 0| 1| 4| | 00 36| IsNull| 4| 53| 0| | 00 37| Affinity| 4| 1| 0| b| 00 38| NotFound| 6| 53| 4| 1| 00 39| Column| 0| 0| 8| | 00 40| Column| 0| 1| 9| | 00 41| Column| 0| 2| 10| | 00 42| Column| 0| 3| 11| | 00 43| Column| 0| 4| 12| | 00 44| Column| 0| 5| 13| | 00 45| Column| 0| 6| 14| | 00 46| Column| 0| 7| 15| | 00 47| Column| 0| 8| 16| | 00 48| Column| 0| 9| 17| | 00 49| Column| 0| 10| 18| | 00 50| Column| 0| 11| 19| | 00 51| Column| 0| 12| 20| | 00 52| ResultRow| 8| 13| 0| | 00 53| Next| 0| 4| 0| | 01 54| Close| 0| 0| 0| | 00 55| Halt| 0| 0| 0| | 00 56| Transaction| 0| 0| 0| | 00 57| VerifyCookie| 0| 3| 0| | 00 58| TableLock| 0| 4| 0| labels| 00 59| TableLock| 0| 5| 0| Items| 00 60| Goto| 0| 2| 0| | 00
可以看到其中需要建立索引,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
http://www.sqlite.org/c3ref/temp_directory.html
PRAGMA temp_store_directory = 'your dir'
這個東西僅對當前SqliteConncetion有效,
在第一次建立sqlite連接的時候(我是重寫了getReadabelDatabase()方法),設置一下臨時文件目錄,like this:
private static boolean mainTmpDirSet = false; @Override public SQLiteDatabase getReadableDatabase() { if (!mainTmpDirSet) { boolean rs = new File("/data/data/com.cmp.pkg/databases/main").mkdir(); Log.d("ahang", rs + ""); super.getReadableDatabase().execSQL("PRAGMA temp_store_directory = '/data/data/com.cmp.pkg/databases/main'"); mainTmpDirSet = true; return super.getReadableDatabase(); } return super.getReadableDatabase(); }
然后再去執行那些繁重的查詢,你會發現問題消失了,
並且sqlite3會在不需要這個臨時文件時自動刪除它,所以你不需要做一套清理邏輯。
於是問題解決!
(轉載請注明出處,
http://www.cnblogs.com/hellocwh/p/5061805.html
如果有什么建議,可以評論或者發郵件給我hellosdk@163.com)