sqlite在Android上的一個bug:SQLiteCantOpenDatabaseException when nativeExecuteForCursorWindow

更多內容在這裏查看html

https://ahangchen.gitbooks.io/windy-afternoon/content/java

  

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)

先給出結論,git

這是sqlite在Android系統上的一個bug,在須要創建索引的sql語句頻繁執行時,會發生這個異常。github

(若是你是在SQLiteDatabase執行open()時看到的這個exception,那應該是線程衝突的問題,跟這篇文章講的不是同一個)sql

根本緣由是sqlite臨時文件目錄不可用。windows

解決方案是第一次創建鏈接時設置臨時文件目錄。網絡

 

在項目裏遇到了這樣一個奇怪的crash,長期佔據各個版本crash上報榜首,但在開發中一直不能重現。app

在許多查DB的代碼路徑裏,都會在moveToFirst(),getCount()等須要執行fillWindow的地方出現這個crash。dom

 網絡上的解決方案:ide

谷歌搜索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時就會失敗。

 

從上面的分析看出,觸發這個路徑須要幾個條件:

  1. 執行的sql語句須要創建索引,
  2. B樹不平衡
  3. 沒有設置過環境變量
  4. 分配的內存不足以新建新的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)

相關文章
相關標籤/搜索