• Android 9.0 SQLiteCantOpenDatabaseException SQLITE_CANTOPEN(不支持WAL模式)源码分析定位


    最近一直忙着处理降低crash率,在Bugly上最新版本中统计到的一个数据库有关的crash:

    # pool-20-thread-1(323)
    android.database.sqlite.SQLiteCantOpenDatabaseException
    unable to open database file (Sqlite code 14 SQLITE_CANTOPEN), (OS error - 2:No such file or directory)
    
    android.database.sqlite.SQLiteConnection.nativeExecuteForLong(Native Method)
    android.database.sqlite.SQLiteConnection.executeForLong(SQLiteConnection.java:657)
    android.database.sqlite.SQLiteSession.executeForLong(SQLiteSession.java:667)
    android.database.sqlite.SQLiteStatement.simpleQueryForLong(SQLiteStatement.java:107)
    android.database.DatabaseUtils.longForQuery(DatabaseUtils.java:842)
    android.database.DatabaseUtils.longForQuery(DatabaseUtils.java:830)
    android.database.sqlite.SQLiteDatabase.getVersion(SQLiteDatabase.java:1036)
    android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:390)
    android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java:337)
    com.meta.android.mpg.mix.sG4DaaDs.a4Dgsas.a4Dgsas(Unknown Source:3)
    com.meta.android.mpg.mix.gG4Gas.fa$fa.run(Unknown Source:4)
    java.util.concurrent.ThreadPoolExecutor.processTask(ThreadPoolExecutor.java:1187)
    java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
    java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    java.lang.Thread.run(Thread.java:784)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    发生的机型统计如下:
    在这里插入图片描述

    1.源码定位分析

    1.1借助系统日志来辅助定位

    查看bugly 捕捉的有效log:

    08-23 17:13:28.790 5668 5860 E SQLiteLog: (14) cannot open file at line 36906 of [68b898381a]
    4708-23 17:13:28.790 5668 5860 E SQLiteLog: (14) os_unix.c:36906: (2) open(/data/data/com.tools.growth.yhxy/virtual/data/user/0/com.minitech.miniworld.meta/databases/MpgSdk.db-wal) -
    4808-23 17:13:28.791 5668 5860 E SQLiteLog: (14) unable to open database file// 有效日志
    4908-23 17:13:28.791 5668 5860 E SQLiteDatabase: Failed to open database  '/data/data/com.tools.growth.yhxy/virtual/data/user/0/com.minitech.miniworld.meta/databases/MpgSdk.db'.
    5008-23 17:13:28.791 5668 5860 E SQLiteDatabase: android.database.sqlite.SQLiteCantOpenDatabaseException: unable to open database file (Sqlite code 14 SQLITE_CANTOPEN): , while compiling: PRAGMA journal_mode, (OS error - 2:No such file or directory) 
    5108-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
    5208-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:948)
    5308-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:693)
    5408-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:378)
    5508-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:327)
    5608-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:232)
    5708-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:210)
    5808-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:552)
    5908-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:213)
    6008-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:202)
    6108-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:958)
    6208-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:942)
    6308-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:816)
    6408-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:806)
    6508-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:370)
    6608-23 17:13:28.791 5668 5860 E SQLiteDatabase: at android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java:337)
    6708-23 17:13:28.791 5668 5860 E SQLiteDatabase: at com.meta.android.mpg.mix.sG4DaaDs.a4Dgsas.a4Dgsas(Unknown Source:3)
    6808-23 17:13:28.791 5668 5860 E SQLiteDatabase: at com.meta.android.mpg.mix.gG4Gas.fa$fa.run(Unknown Source:4)
    6908-23 17:13:28.791 5668 5860 E SQLiteDatabase: at java.util.concurrent.ThreadPoolExecutor.processTask(ThreadPoolExecutor.java:1187)
    7008-23 17:13:28.791 5668 5860 E SQLiteDatabase: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)
    7108-23 17:13:28.791 5668 5860 E SQLiteDatabase: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    
    • 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

    当Bugly中的当前crash记录上报没有日志时,可以多查看几条记录,日志是一个很好加速定位问题的辅助手段。

    1.2查看源码,锁定报错点

    本篇源码基于android 9.0

    /frameworks/base/core/java/android/database/sqlite/SQLiteConnection.java

        private PreparedStatement acquirePreparedStatement(String sql) {
            PreparedStatement statement = mPreparedStatementCache.get(sql);
            //...
            final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
            //...
            return statement;
        }
    	private static native long nativePrepareStatement(long connectionPtr, String sql);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    接下来看下jni 层的调用。

    /frameworks/base/core/jni/android_database_SQLiteConnection.cpp

    static jlong nativePrepareStatement(JNIEnv* env, jclass clazz, jlong connectionPtr,
            jstring sqlString) {
        SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr);
    
        jsize sqlLength = env->GetStringLength(sqlString);
        const jchar* sql = env->GetStringCritical(sqlString, NULL);
        sqlite3_stmt* statement;
        int err = sqlite3_prepare16_v2(connection->db,
                sql, sqlLength * sizeof(jchar), &statement, NULL);
        env->ReleaseStringCritical(sqlString, sql);
    
        if (err != SQLITE_OK) {
            //构建异常信息
            const char *query = env->GetStringUTFChars(sqlString, NULL);
            char *message = (char*) malloc(strlen(query) + 50);
            if (message) {
                strcpy(message, ", while compiling: "); // less than 50 chars
                strcat(message, query);
            }
            env->ReleaseStringUTFChars(sqlString, query);
    		//抛出异常
            throw_sqlite3_exception(env, connection->db, message);
            free(message);
            return 0;
        }
        return reinterpret_cast<jlong>(statement);
    }
    
    • 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

    这里刚好和日志中的while compiling: PRAGMA journal_mode 匹配,说明 调用sqlite3_prepare16_v2 返回失败结果。

    接着继续, 查看sqlite3_prepare16_v2()

    /external/sqlite/dist/orig/sqlite3.c

    SQLITE_API int sqlite3_prepare16_v2(
      sqlite3 *db,              /* Database handle. */
      const void *zSql,         /* UTF-16 encoded SQL statement. */
      int nBytes,               /* Length of zSql in bytes. */
      sqlite3_stmt **ppStmt,    /* OUT: A pointer to the prepared statement */
      const void **pzTail       /* OUT: End of parsed string */
    ){
      int rc;
      rc = sqlite3Prepare16(db,zSql,nBytes,SQLITE_PREPARE_SAVESQL,ppStmt,pzTail);
      assert( rc==SQLITE_OK || ppStmt==0 || *ppStmt==0 );  /* VERIFY: F13021 */
      return rc;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    因SQLite的源码太过于庞大,只能考虑逆推方式,先根据报错日志,检索关键位置。

    根据unable to open database file匹配到SQLITE_CANTOPEN 字段。

    SQLITE_PRIVATE const char *sqlite3ErrStr(int rc){
      static const char* const aMsg[] = {
       /* SQLITE_CANTOPEN    */ "unable to open database file",
      }
      //... 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    根据cannot open file atSQLITE_CANTOPEN 匹配到sqlite3CantopenError(),用于报错时,拼接打印报错信息:

    接下来看下,sqlite3CantopenError():

    #define SQLITE_CANTOPEN_BKPT sqlite3CantopenError(__LINE__)
    
    SQLITE_PRIVATE int sqlite3CantopenError(int lineno){
      testcase( sqlite3GlobalConfig.xLog!=0 );
      return sqlite3ReportError(SQLITE_CANTOPEN, lineno, "cannot open file"); // 对应日志:(14) cannot open file at line 36906 of [68b898381a]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    全局检索SQLITE_CANTOPEN 关键字,耗费脑细胞的逐步分析调用链:sqlite3Prepare16()->省略部分流程->sqlite3BtreeBeginTrans()->lockBtree()->sqlite3PagerOpenWal()

    接下来看下,sqlite3BtreeBeginTrans():

    SQLITE_PRIVATE int sqlite3BtreeBeginTrans(Btree *p, int wrflag){
      
      //....
      do {
      
    	//循环检查,直到lockBtree返回相应状态
        while( pBt->pPage1==0 && SQLITE_OK==(rc = lockBtree(pBt)) );
        if( rc==SQLITE_OK && wrflag ){
          if( (pBt->btsFlags & BTS_READ_ONLY)!=0 ){
            rc = SQLITE_READONLY;
          }else{
            rc = sqlite3PagerBegin(pBt->pPager,wrflag>1,sqlite3TempInMemory(p->db));
            if( rc==SQLITE_OK ){
              rc = newDatabase(pBt);
            }
          }
        }
        if( rc!=SQLITE_OK ){
          unlockBtreeIfUnused(pBt);
        }
      }while( (rc&0xFF)==SQLITE_BUSY && pBt->inTransaction==TRANS_NONE &&
              btreeInvokeBusyHandler(pBt) )
    
      return rc;
    }
    
    • 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

    接着继续查看,lockBtree():

    static int lockBtree(BtShared *pBt){
      int rc;              /* Result code from subfunctions */
      MemPage *pPage1;     /* Page 1 of the database file */
      int nPage;           /* Number of pages in the database */
      int nPageFile = 0;   /* Number of pages in the database file */
      int nPageHeader;     /* Number of pages in the database according to hdr */
    
      //先获取一个share lock
      rc = sqlite3PagerSharedLock(pBt->pPager);
      if( rc!=SQLITE_OK ) return rc;
      
      //...
      
      //当write version 是2,数据库应该使用wal 模式。若是检查到log文件没有被打开,则进行打开。
        if( page1[19]==2 && (pBt->btsFlags & BTS_NO_WAL)==0 ){
          int isOpen = 0;
          rc = sqlite3PagerOpenWal(pBt->pPager, &isOpen);
          if( rc!=SQLITE_OK ){
    	    //若是打开失败,则进行释放资源操作
            goto page1_init_failed;
          }else{
            setDefaultSyncFlag(pBt, SQLITE_DEFAULT_WAL_SYNCHRONOUS+1);
            if( isOpen==0 ){
              releasePageOne(pPage1);
              return SQLITE_OK;
            }
          }
    	  // 若是打开正常,则返回 SQLITE_NOTADB
          rc = SQLITE_NOTADB;
        }else{
          setDefaultSyncFlag(pBt, SQLITE_DEFAULT_SYNCHRONOUS+1);
        }
       //....
       
      page1_init_failed:
      releasePageOne(pPage1);
      pBt->pPage1 = 0;
      return rc;
    }
    
    • 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

    先来看下,sqlite3PagerSharedLock():

    SQLITE_PRIVATE int sqlite3PagerSharedLock(Pager *pPager){
      int rc = SQLITE_OK;                /* Return code */
    
    
      if( !pagerUseWal(pPager) && pPager->eState==PAGER_OPEN ){
    
        // 获取到shared lock 
        rc = pager_wait_on_lock(pPager, SHARED_LOCK);
        //...
    
      }
     
      return rc;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    接下来看下,sqlite3PagerOpenWal():

    SQLITE_PRIVATE int sqlite3PagerOpenWal(
      Pager *pPager,                  /* Pager object */
      int *pbOpen                     /* OUT: Set to true if call is a no-op */
    ){
      int rc = SQLITE_OK;             /* Return code */
    
      assert( assert_pager_state(pPager) );
      assert( pPager->eState==PAGER_OPEN   || pbOpen );
      assert( pPager->eState==PAGER_READER || !pbOpen );
      assert( pbOpen==0 || *pbOpen==0 );
      assert( pbOpen!=0 || (!pPager->tempFile && !pPager->pWal) );
    
      if( !pPager->tempFile && !pPager->pWal ){
      
        // 关键代码: 若是不支持Write-Ahead-Logging(即)模式, 则返回SQLITE_CANTOPEN。
        if( !sqlite3PagerWalSupported(pPager) ) return SQLITE_CANTOPEN;
    
        /* Close any rollback journal previously open */
        sqlite3OsClose(pPager->jfd);
    
        rc = pagerOpenWal(pPager);
        if( rc==SQLITE_OK ){ // 若是支持Write-Ahead-Logging 模式,则标记打开模式和状态
          pPager->journalMode = PAGER_JOURNALMODE_WAL;
          pPager->eState = PAGER_OPEN;
        }
      }else{
        *pbOpen = 1;
      }
    
      return rc;
    }
    
    • 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

    通过一些列的分析,推断是因数据库不支持Write-Ahead-Logging 模式导致的,因此考虑禁止使用该模式,可以处理该问题

    2.解决方案

    2.1 Android 9及其以上数据库的WAL模式

    Android 9 引入了 SQLiteDatabase 的一种特殊模式,称为“兼容性 WAL(预写日志记录)”,它允许数据库使用 journal_mode=WAL,同时保留每个数据库最多创建一个连接的行为.

    更多有信息,请阅读应用的兼容性 WAL(预写日志记录)

    2.2禁止启用wal 模式的解决方案

    https://stackoverflow.com/questions/53659206/disabling-sqlite-write-ahead-logging-in-android-pie

  • 相关阅读:
    SSN1IXCSA SSN1IXCSB 无限交叉时钟板 全新板卡
    虚拟信用卡:如何获取、推荐平台及对注册开发者账号的应用
    货捕头网电商数据,获取商品详情,Onebound电商API
    FoveaBox:细节差别,另一种DenseBox+FPN的Anchor-free方案 | IEEE TIP 2020
    《HelloGitHub》第 76 期
    leetcode1480.一维数组的动态和
    opcua pubsub 消息的wireshark解码
    Anaconda虚拟环境创建管理流程
    【MyBatis笔记04】MyBatis框架中各种参数类型绑定的方式
    JUnit 5 初探
  • 原文地址:https://blog.csdn.net/hexingen/article/details/126538093