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)

发生的机型统计如下:

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

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)

当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);

接下来看下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: ");
            strcat(message, query);
        }
        env->ReleaseStringUTFChars(sqlString, query);

        throw_sqlite3_exception(env, connection->db, message);
        free(message);
        return 0;
    }
    return reinterpret_cast<jlong>(statement);
}

这里刚好和日志中的 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,
  const void *zSql,
  int nBytes,
  sqlite3_stmt **ppStmt,
  const void **pzTail
){
  int rc;
  rc = sqlite3Prepare16(db,zSql,nBytes,SQLITE_PREPARE_SAVESQL,ppStmt,pzTail);
  assert( rc==SQLITE_OK || ppStmt==0 || *ppStmt==0 );
  return rc;
}

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

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

SQLITE_PRIVATE const char *sqlite3ErrStr(int rc){
  static const char* const aMsg[] = {
    "unable to open database file",
  }

}

根据 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");
}

全局检索 SQLITE_CANTOPEN 关键字,耗费脑细胞的逐步分析调用链: sqlite3Prepare16()->&#x7701;&#x7565;&#x90E8;&#x5206;&#x6D41;&#x7A0B;->sqlite3BtreeBeginTrans()->lockBtree()->sqlite3PagerOpenWal()

接下来看下,sqlite3BtreeBeginTrans():

SQLITE_PRIVATE int sqlite3BtreeBeginTrans(Btree *p, int wrflag){

  do {

    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;
}

接着继续查看, lockBtree():

static int lockBtree(BtShared *pBt){
  int rc;
  MemPage *pPage1;
  int nPage;
  int nPageFile = 0;
  int nPageHeader;

  rc = sqlite3PagerSharedLock(pBt->pPager);
  if( rc!=SQLITE_OK ) return rc;

    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;
        }
      }

      rc = SQLITE_NOTADB;
    }else{
      setDefaultSyncFlag(pBt, SQLITE_DEFAULT_SYNCHRONOUS+1);
    }

  page1_init_failed:
  releasePageOne(pPage1);
  pBt->pPage1 = 0;
  return rc;
}

先来看下, sqlite3PagerSharedLock():

SQLITE_PRIVATE int sqlite3PagerSharedLock(Pager *pPager){
  int rc = SQLITE_OK;

  if( !pagerUseWal(pPager) && pPager->eState==PAGER_OPEN ){

    rc = pager_wait_on_lock(pPager, SHARED_LOCK);

  }

  return rc;
}

接下来看下, sqlite3PagerOpenWal():

SQLITE_PRIVATE int sqlite3PagerOpenWal(
  Pager *pPager,
  int *pbOpen
){
  int rc = SQLITE_OK;

  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 ){

    if( !sqlite3PagerWalSupported(pPager) ) return SQLITE_CANTOPEN;

    sqlite3OsClose(pPager->jfd);

    rc = pagerOpenWal(pPager);
    if( rc==SQLITE_OK ){
      pPager->journalMode = PAGER_JOURNALMODE_WAL;
      pPager->eState = PAGER_OPEN;
    }
  }else{
    *pbOpen = 1;
  }

  return rc;
}

通过一些列的分析, 推断是因数据库不支持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

Original: https://blog.csdn.net/hexingen/article/details/126538093
Author: 新根
Title: Android 9.0 SQLiteCantOpenDatabaseException SQLITE_CANTOPEN(不支持WAL模式)源码分析定位

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/815923/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球