title: “Crash: 'SQLiteDatabaseCorruptException: file is encrypted or is not a database’的分析与解决”
tags:

先说解决方案与结论

如果使用的Room库,那么需要按照如下方法设置 JournalMode

Room.databaseBuilder(context, XXX::class.java, DATABASE_NAME)
                .setJournalMode(JournalMode.TRUNCATE)
                .build()

如果直接使用SQLite,那么需要按照如下的方法设置JournalMode

SQLiteDatabase.disableWriteAheadLogging
SQLiteDatabase.OpenParams.setJournalMode(String mode)

什么是JournalMode请参看这里:

https://source.android.com/devices/tech/perf/compatibility-wal

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

WAL使数据库的工作方式发生了变化,在一些状态下会抛出此异常。需要按照上文解决方案中的方法,手动关闭WAL模式,即可修复此问题。

排查方向和路径

明确Crash堆栈信息

android.database.sqlite.SQLiteDatabaseCorruptException: file is encrypted or is not a database (Sqlite code 26): , while compiling: PRAGMA journal_mode, (OS error - 2:No such file or directory)
	at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
	at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:925)
	at android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:670)
	at android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:356)
	at android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:319)
	at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:229)
	at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:207)
	at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:511)
	at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:194)
	at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:183)
	at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:880)
	at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:867)
	at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:767)
	at android.app.ContextImpl.openOrCreateDatabase(ContextImpl.java:739)
	at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:289)
	at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:235)
	at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:175)
	at android.arch.persistence.a.a.b$a.a(SourceFile:96)
	at android.arch.persistence.a.a.b.a(SourceFile:54)
	at android.arch.persistence.room.RoomDatabase.query(SourceFile:233)

明确问题影响范围

问题在一开始只有零星的一些,并未引起我太多的注意,但是在正式版本放量后,Crash数量便猛增起来,到了不得不修复的状态。

而且该问题出现的系统版本遍布了5.1.1到11。

尝试修复无果

开始着手修复后,只是拿着关键词在Google和百度上进行搜索,但是惊奇的发现,有同类型错误的出现,但都没有相关的解决方案。

虽说try catch能够捕获该异常,但是这种粗暴的方法,再加上不明所以,总是会让人不爽,于是开始查询源码来排查问题出在那里。

根据堆栈信息可以明确知道,异常是在nativePrepareStatement的方法中抛出的,由于此方法是JNI代码,并无法快捷的直接在AS中索引,所以此处用到了Andoid Code Search网站进行源码搜索。

nativePrepareStatementandroid_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) {
        // Error messages like 'near ")": syntax error' are not
        // always helpful enough, so construct an error string that
        // includes the query itself.
        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;
    ALOGV("Prepared statement %p on connection %p", statement, connection->db);
    return reinterpret_cast<jlong>(statement);
 

https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:frameworks/base/core/jni/android_database_SQLiteConnection.cpp;l=303;drc=android-10.0.0_r30

nativePrepareStatement方法中,可以看到一个关键字", while compiling:"与Crash日志中的"file is encrypted or is not a database (Sqlite code 26): , while compiling: PRAGMA journal_mode, (OS error - 2:No such file or directory)"相匹配,可以确定真正发生Crash的地点是在拼接错误日志之前。

那么可以进一步将发生Crash的方法锁定到sqlite3_prepare16_v2方法中。

由于sqilte3的代码文件过大,建议将库克隆到本地。

https://android.googlesource.com/platform/external/sqlite.git

这里推荐一下Sourcetrail软件,阅读C代码的神器,以sqlite3_prepare16_v2()为起点,通过层层调用可以排查到在lockBtree()方法中有可能返回SQLITE_NOTADB错误码。

同时可以搜索sqlite3.c文件,可以知道在sqlite3ErrStr()方法中,将SQLITE_NOTADB错误码与错误文案(“file is not a database”)进行了关联。

SQLITE_PRIVATE const char *sqlite3ErrStr(int rc){
  static const char* const aMsg[] = {
    ......
    /* SQLITE_NOTADB      */ "file is not a database",
    ......
  ......
  return zErr;

可以分析得知lockBtree()方法就是产生这个Crash的地方,那么着重对该方法进行一个分析。

** Get a reference to pPage1 of the database file. This will ** also acquire a readlock on that file. ** SQLITE_OK is returned on success. If the file is not a ** well-formed database file, then SQLITE_CORRUPT is returned. ** SQLITE_BUSY is returned if the database is locked. SQLITE_NOMEM ** is returned if we run out of memory. static int lockBtree(BtShared *pBt){ int rc; /* Result code from subfunctions */ MemPage *pPage1; /* Page 1 of the database file */ u32 nPage; /* Number of pages in the database */ u32 nPageFile = 0; /* Number of pages in the database file */ u32 nPageHeader; /* Number of pages in the database according to hdr */ /* Do some checking to help insure the file we opened really is ** a valid database file. nPage = nPageHeader = get4byte(28+(u8*)pPage1->aData); sqlite3PagerPagecount(pBt->pPager, (int*)&nPageFile); if( nPage==0 || memcmp(24+(u8*)pPage1->aData, 92+(u8*)pPage1->aData,4)!=0 ){ nPage = nPageFile; if( nPage>0 ){ u32 pageSize; u32 usableSize; u8 *page1 = pPage1->aData; rc = SQLITE_NOTADB; ...... #ifdef SQLITE_OMIT_WAL ...... #else if( page1[18]>2 ){ pBt->btsFlags |= BTS_READ_ONLY; if( page1[19]>2 ){ goto page1_init_failed; /* If the write version is set to 2, this database should be accessed ** in WAL mode. If the log is not already open, open it now. Then ** return SQLITE_OK and return without populating BtShared.pPage1. ** The caller detects this and calls this function again. This is ** required as the version of page 1 currently in the page1 buffer ** may not be the latest version - there may be a newer one in the log ** file. 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); #endif ...... page1_init_failed: releasePageOne(pPage1); pBt->pPage1 = 0; return rc;

我将lockBTree()中和Crash不相关的逻辑进行了去除,通过分析可以知道,rc的默认值就是SQLITE_NOTADB,在随后的逻辑中
通过sqlite3PagerOpenWal()方法尝试通过WAL的方式打开数据库,但是其结果不为SQLITE_OK,导致逻辑直接跳向了page1_init_failed中。

sqlite3PagerOpenWal()方法中又后续调用了pagerOpenWal()等方法,都是WAL的逻辑。

WAL预写式日志(Write-ahead logging,缩写 WAL)是关系数据库系统中用于提供原子性和持久性(ACID属性中的两个)的一系列技术。在使用WAL的系统中,所有的修改在提交之前都要先写入log文件中。

至此,想要解决这个Crash,只需要禁止数据库的WAL模式即可。

  • JOURNAL_MODE:https://sqlite.org/pragma.html#pragma_journal_mode
  • WAL:https://zh.wikipedia.org/zh-hans/%E9%A2%84%E5%86%99%E5%BC%8F%E6%97%A5%E5%BF%97
  • AndroidWAL:https://source.android.com/devices/tech/perf/compatibility-wal
  • nativePrepareStatement: https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:frameworks/base/core/jni/android_database_SQLiteConnection.cpp;l=303;drc=android-10.0.0_r30
  • sqlite:https://android.googlesource.com/platform/external/sqlite.git
  • https://blog.csdn.net/aasmfox/article/details/8026333
  • https://stackoverflow.com/questions/11901460/is-it-possible-to-disable-wal-on-android-sqlite-database
  • https://stackoverflow.com/questions/53659206/disabling-sqlite-write-ahead-logging-in-android-pie/53689702
  • https://github.com/CoatiSoftware/Sourcetrail
  • https://blog.csdn.net/zearot/article/details/51039593
  • https://cs.android.com/android/platform/superproject/+/master:external/rust/crates/
  • http://sqlite.1065341.n5.nabble.com/sqlite-3-7-2-doesn-t-compile-if-SQLITE-OMIT-WAL-is-defined-td45921.html
  • https://www.sqlite.org/src/info/d1ed743b6e
  • https://www.sqlite.org/compile.html
title: “Crash: 'SQLiteDatabaseCorruptException: file is encrypted or is not a database’的分析与解决”tags:先说解决方案与结论方案如果使用的Room库,那么需要按照如下方法设置JournalMode:Room.databaseBuilder(context, XXX::class.java, DATABASE_NAME) .setJournalMode(JournalMode. static String DB_PATH = "/data/data/com.yuzhiyun.chemistry/databases/"; static String DB_NAME = "sqlite3.db"; private SQLiteDatabase db; this.db = SQLiteDatabase.openDatabase(DB_PATH+...
Unlock Music 音乐解锁 在浏览器中解锁加密的音乐文件。 Unlock encrypted music file in the browser. unlock-music项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循 Unlock Music的CLI版本正在开发中。 我们新建了Telegram群组,欢迎加入! 支持的格式 QQ音乐 (.qmc0/.qmc2/.qmc3/.qmcflac/.qmcogg/) 写入封面图片 Moo音乐格式 () QQ音乐Tm格式 (.tm0/.tm2/.tm3/.tm6) QQ音乐新格式 (实验性支持) .mflac 网易云音乐格式 (.ncm) 补全ncm的ID3/FlacMeta信息 虾米音乐格式 (.xm) (测试阶段) 酷我音乐格式 (.kwm) (测试阶段) 酷狗音乐格式 (.kgm) ()
问题描述:用户将Domino升级完成之后,当用户试图访问server上的邮箱库时,碰到错误信息“数据库已损坏,无法分配空间”解答:这个错误信息通常意味着数据库损坏,在这种情况下,我们通常要用Fixup/Compact/Updall这几个命令来尝试修复数据库。 1.load fixup -f (这样的参数会强制Fixup检查数据库中的所有文档) 2.load compact...
  SQLITE_OK           = 0;  返回成功   SQLITE_ERROR        = 1;  SQL错误或错误的数据库   SQLITE_INTERNAL     = 2;  An internal logic error in SQLite   SQLITE_PERM         = 3;  拒绝
在这一行后面,接上本文最下面的代码段。 这些代码很长,我不再解释,直接接上去就得了。 唯一要提的是 DeriveKey 函数。这个函数是对密钥的扩展。比如,你要求密钥是128位,即是16字节,但是如果用户只输入 1个字节呢?2个字节呢?或输入50个字节呢?你得对密钥进行扩展,使之符合16字节的要求。 DeriveKey 函数就是做这个扩展的。有人把接收到的密钥求md5,这也是一个办法,因为m
为了搞毕设重新捡起sqlite3,照着《第一行代码-Android》使用adb进行查看,但发现实际使用真机操作起来是真的麻烦! 主要是两个坑:1.root权限;2.sqlite3文件 (1)root权限,我使用的是小米手机4。刚开始网上各种找root工具,发现全部没用。等我针对MI4进行搜的时候,才发现原来小米手机只要刷开发版,在 设置->授权管理 里就有一个ROOT权限管理。(这里安...
经过跟踪定位到sqlite3_prepare_v2()函数,返回错误码为26. 通过查找26的定义为:#define SQLITE_NOTADB      26   /* File opened that is not a databas Spring4.0+Hibernate4.2.整合出现java.lang.ClassNotFoundException: org.hibernate.engine.FilterDefinition 23163
Retrofit(2.0)入门小错误 -- Could not locate ResponseBody xxx Tried: * retrofit.BuiltInConverters OYMN: 您好,你知道怎么做吗 Unity3D - 使用Mesh创建弧面 妙之先生: 您好,我想请问下threshold的值为什么不是0~Π