本地耐久化存储数据,依据事务场景选择合适的存储计划;有高效读写、空间限制等不同的事务场景,了解各耐久化计划的优缺陷,选择适当的耐久化计划。

本文在介绍耐久化计划,依据不同的计划,论证了需求考虑文件校验的可行性,以及详细的校验计划;

1、MMKV

CRC算法参考链接

鉴于 MMKV 自身已做 CRC 算法的校验,本计划不做过多研讨。

关于 MMKV 的 CRC 校验处理,详细能够参考 MMKV 源码(链接)。

2、SharedPreferences

文件损坏首要有以下两种场景:

  • 运转中,检测文件损坏(此种场景能够忽略);
  • 进程重启,检测文件损坏;

因为 SP 计划是对 XML 文件进行操作,XML 文件归于固定格局的文件,所以文件损坏或缺失,解析 XML 文件即可抛出反常,咱们能够比较容易的处理该场景。

2.1、操作损坏文件场景

  • 向 XML 文件写入恣意文本

    • 不符合 XML 文件标准,可能无法拜访文本内容;从头写入数据,可覆盖文件并能够正常读取内容;
    • 写入不符合 XML 标准的内容,能够拜访文本内容;从头写入新(原)数据,可(不行)覆盖文件并能够正常读取内容;
  • 文件损坏

    • 无法拜访文件内容;从头写入数据,可覆盖文件并能够正常读取内容;
  • 文件无拜访权限

    • 无法拜访文件内容;从头写入数据,可覆盖文件并能够正常读取内容;
// 刺进不符合 xml 标准的内容,不能够拜访
aaaaa<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="test_sp_key">123456</string>
    aaaaa
</map>
// 刺进不符合xml标准的内容,能够拜访
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="test_sp_key">123456</string>
    aaaaa
</map>
// 刺进包含乱码的内容(非UTF-8编码字符)
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="test_sp_key">123456</string>
</map>

文件不符合 xml 标准

Cannot read /data/user/0/packageName/shared_prefs/test_sp.xml
org.xmlpull.v1.XmlPullParserException: Unexpected token (position:TEXT test char, hello...@1:42 in java.io.InputStreamReader@896f9fb) 
	at com.android.org.kxml2.io.KXmlParser.next(KXmlParser.java:432)
	at com.android.org.kxml2.io.KXmlParser.next(KXmlParser.java:313)
	at com.android.internal.util.XmlUtils.readValueXml(XmlUtils.java:1399)
	at com.android.internal.util.XmlUtils.readMapXml(XmlUtils.java:741)
	at android.app.SharedPreferencesImpl.loadFromDisk(SharedPreferencesImpl.java:171)
	at android.app.SharedPreferencesImpl.access$000(SharedPreferencesImpl.java:59)
	at android.app.SharedPreferencesImpl$1.run(SharedPreferencesImpl.java:140)

文件包含乱码(非UTF-8编码字符)

Cannot read /data/user/0/packageName/shared_prefs/
org.xmlpull.v1.XmlPullParserException: Unexpected token (position:TEXT ������@1:7 in java.io.InputStreamReader@bf537d4) 
    at com.android.org.kxml2.io.KXmlParser.next(KXmlParser.java:432)
    at com.android.org.kxml2.io.KXmlParser.next(KXmlParser.java:313)
    at com.android.internal.util.XmlUtils.readValueXml(XmlUtils.java:1399)
    at com.android.internal.util.XmlUtils.readMapXml(XmlUtils.java:741)
    at android.app.SharedPreferencesImpl.loadFromDisk(SharedPreferencesImpl.java:171)
    at android.app.SharedPreferencesImpl.access$000(SharedPreferencesImpl.java:59)
    at android.app.SharedPreferencesImpl$1.run(SharedPreferencesImpl.java:140)

2.2、结构解析

SharePreference 缓存计划,首要触及到4个操作,文件加载和解析、数据读取、数据写入、数据删去。

从上述4个方面剖析: 考虑到进程运转中,运用 MAP 缓存计划,即使文件写入失败,读取是从内存中加载,所以能够拜访到合法的数据。

针对进程重启后的场景,会从头触发 XML 文件的解析,假如文件损坏,则会抛出反常处理。

  1. 文件加载和解析
  • SP 在创立缓存目标时,首先会解析 XML 文件,并将解析的数据写入缓存池中;
  1. 数据读取
  • SP 凭借缓存计划,读取只需求从缓存加载;
  1. 数据写入
  • 写入缓存,将数据写入缓存池(HashMap),Key-Value 方法存储;
  • 写入文件,假如文件格局损坏、无读写权限或不存在时,会创立新的文件并赋予文件读写权限;生成文件输出流,然后将缓存 map 逐条转成 XML 格局数据, 并将 XML 数据流写入文件;
  1. 数据删去
  • 删去归于数据写入的一个子项,首要针对内存的操作,将内存中缓存的条目删去,写入文件操作同数据写入部分的写入文件流程。

一文搞懂耐久化结构,不明白来打我

注意事项

  • 读写操作的线程堵塞问题

— 鉴于 SP 初始化时,会在子线程解析文件,所以在读写时,会有目标锁,假如还在解析文件中,则会 block 主线程(引入 ANR 危险);运用主张:考虑在子线程创立 SP 目标(懒加载计划);

  • 全量写入问题

— 每次提交是全部条目写入文件,不是增量写入文件(写入操作会触及较多的IO操作);运用主张:读多写少场景考虑运用 SP 计划;

2.3、计划

首要思路:操作文件呈现的反常状况处理,保证进程的稳定。

针对xml文件的加载与读写操作,需求考虑操作中呈现的反常状况处理,针对呈现的反常状况做好默许值响应,事务模块做好默许值处理即可。

注意事项不对文件做校验工作(该计划不考虑文件内容被篡改的危险,仅从文件损坏视点考虑)

2.4、定论

不需求做文件校验处理,也能够处理文件损坏的场景,只需求在事务上对默许数据做好兜底战略即可。

3、DiskLruCache

3.1、文件输入输出流

DiskLruCache 对文件的操作,首要是经过FileInputSream、FileOutStream 进行,所以在操作上,需求考虑数据流的运用标准,在运用完成后,及时封闭数据,防止资源未封闭呈现资源走漏问题。

InputputStream 内办法含义简介:

  • read() 办法,从源地址(网络通道或磁盘等)读取数据到缓冲区;
  • close() 办法,封闭输出流;

OutputStream 内办法含义简介:

  • write() 办法,写入数据到缓冲区;
  • close() 办法,封闭输出流;
  • flush()办法,将缓冲区的数据输出到目的地(网络通道或磁盘等);

OutputStream 为什么有 flush() 办法?

因为向磁盘、网络写入数据的时分,出于功率的考虑,操作系统并不是输出一个字节就立刻写入到文件或许发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上便是一个byte[]数组),比及缓冲区写满了,再一次性写入文件或许网络。关于许多 IO 设备来说,一次写一个字节和一次写1K个字节,花费的时间几乎是完全相同的,所以 OutputStream 有个 flush() 办法,能强制把缓冲区内容输出。

通常状况下,咱们不需求调用这个 flush() 办法,因为缓冲区写满 OutputStream 会自动调用,最后 OutputStream 目标收回时会调用 finalize() 办法,该办法会自动调用 flush() 办法。

3.2、结构解析

3.2.1、journal 日志文件介绍

journal 文件作为 DiskLruCache 缓存计划的日志文件,记载了最近的操作记载,每一行是一条操作记载数据,journal 日志文件记载类型首要有4类,分别是 DIRTY、CLEAN、REMOVE、READ;

记载数据格局:类型[空格]KEY([空格]数据巨细)

[]:标识空格

():该部分内容可无

open DiskLruCache 缓存时,会对 journal 文件进行逐行解析,并将有用的数据放入 MAP 缓存池内,详细可参考 3.3.2.2 小节剖析;

详细标识含义如下所示:

// 用于盯梢创立或更新的文件
DIRTY 63b6677a0dfa1a48b7a91f7581af5d20
// 符号文件写入成功
CLEAN 63b6677a0dfa1a48b7a91f7581af5d20 1657
// 符号文件删去成功
REMOVE c78f32b5eeab84186e6f19e9304b9d4a
// 符号文件读取成功
READ 8df1397acc54cffdad3a1fd8854e3cc6

3.2.2、缓存结构

DiskLruCache 缓存计划,首要触及4类操作,日志文件加载和解析、读操作、写操作和删去操作;

DiskLruCache 会自动办理和数据上限,所以一般状况下,不需求咱们自动调用删去操作,由结构自身内部办理;

从上述4个方面剖析:

  1. open 操作

一般在构建 DiskLruCache 目标时触发,本操作触及日志文件解析,依据日志文件的格局剖析,每行是一条操作记载,所以需求逐行解析,将解析的 CLEAN 记载放入 MAP 缓存池,遇到 REMOVE、DIRTY 记载,则从缓存池中将数据移除;

MAP 以 K-V 的 方法记载这个每条数据

  • Key 是缓存文件去掉后缀的文件名;
  • Value 是 Entry 数据结构,记载着每条数据的缓存文件和数据巨细;
  1. get 操作

从 MAP 缓存池中查找数据记载,假如没有则返回空;假如有则创立一个 Snapshot 目标,用于数据流的读取操作,并写入 READ 记载到日志文件;

  1. edit 操作

从 MAP 缓存池查找数据 Entry,无则创立一条并写入缓存池;有则直接运用;经过Entry 创立文件编辑器 Editor,用于数据流的写入操作,写入 DIRTY 记载到日志文件;当用户数据流写入完成,触发 commit 操作时,写入 CLEAN 记载到日志文件,标志数据写入成功;触发 abort 操作时,写入 REMOVE 记载到日志文件,并将缓存池中的数据删去;

  1. remove 操作

从 MAP 缓存池中删去数据,写入 REMOVE 记载到日志文件;

删去分两种状况:一种是用户自动删去,一种是达到上限自动删去;操作过程相同;

一文搞懂耐久化结构,不明白来打我

  1. 关于日志文件巨细的问题?

日志文件假如持续写入,则文件巨细无法得到操控,因而 DiskLruCache 选用日志文件重建的方法,操控日志文件巨细的持续增长;

触发重建的条件有两个:

  • 操作记载达到 2000 条;
  • 操作记载超越 MAP 缓存池巨细;

当上述两个条件一起满足时,则触发日志文件的重建,日志文件依据 MAP 条目,生成 CLEAN 有用记载;

注意事项:

  • open 操作,创立 DiskLruCache 目标时,会自动触发日志文件的读取和解析,因而主张放入子线程操作;在构建 DisLruCache 目标时,能够选用懒加载方法;
  • 日志文件巨细操控取决于DiskLruCache缓存空间的设定,假如缓存空间设定过大且缓存文件数量多且小时,因为操作记载数据较多,则日志文件也相对较大;

3.3、计划

3.3.1、记载 MD5 计划调研

a、自定义文件作为MD5的存储计划

选用独自文件缓存 MD5 数据,每个缓存文件以 KEY 为文件名、MD5 为文件内容的方法保存;

长处:

  • 对外没有额定技能依赖;
  • 对 DiskLruCache 代码侵入较小;

缺陷:

  • 生成大量文件;
  • 需求额定办理文件的读取、写入和删去的机遇;
  • 完本钱钱较高;

b、MMKV 作为 MD5 的存储计划(✅)

凭借 MMKV 缓存计划记载 MD5 数据,已 Key-Value 方法保存,能够高效的完成 MD5 数据办理和拜访;

长处:

  • 对 DiskLruCache 代码侵入较小;
  • 接入本钱较低;

缺陷:

  • 依赖 MMKV 结构;
  • 需求额定办理文件的读取、写入和删去的机遇;
  • 存在 MMKV 文件损坏,导致所有缓存文件失效危险;

c、journal 日志文件作为 MD5 的存储计划

选用 journal 日志文件缓存 MD5 数据,结合日志文件的数据结构,在 CLEAN 记载行内写入对应的 MD5 数据,CLEAN 记载行标识数据有用,能够保证MD5数据有用性,日志文件按照操作记载次序存储,假如文件后续有 REMOVE 操作行,则能够标识该文件已移除,在缓存池内同样会将缓存条目删去,因而能够及时的更新 MD5 数据的有用性;

长处:

  • MD5 数据存储价值较小,在本身已有的日志文件系统内做记载;
  • 写入、读取、删去操作,会同步到日志文件内,关于MD5数据能够及时进行更新;

缺陷:

  • 代码侵入性强,对 DiskLruCache 稳定性带来危险;
    • 需求对 journal 文件的解析和写入做修改;
    • Entry 的数据结构做适当调整,价值比较高;
  • DiskLruCache 版别晋级保护带来不便;
  • 完本钱钱较高;
  • 存在 journal 文件损坏,导致所有缓存文件失效危险;

定论:

鉴于上述计划优缺陷,各计划针对 DiskLruCache 都有一定的侵入性,从接入本钱考虑,目前选用 MMKV 作为 MD5 数据记载的计划。

3.3.2、计划设计

1、MD5 办理机遇剖析

MD5 的办理首要触及3个方面,写入、读取和删去,因而需求针对 DiskLruCache 缓存结构进行拆解,在适当机遇触发 MD5 数据的办理操作。

  1. 写入 MD5
  • 因为写入数据选用文件输出流的方法,所以考虑在写入文件流时,进行 MD5 核算;
  • 在输出流写入完成后,获取整个输出流核算的 MD5 数据,便是整个文件的 MD5 数据;
  1. 校验 MD5
  • 读取文件的 MD5 数据,经过文件输入流核算 MD5 数据;
  • 读取缓存的 MD5 数据;
  • MD5 进行比照校验;
  1. 删去 MD5
  • 缓存文件删去时,同步删去 MD5 记载;

2、MD5 办理结构

MD5 办理结构,首要触及3个操作的完成,经过代理计划,用于监听数据的写入和删去操作;在读取数据,进行 MD5 校验。

  1. 监听写入操作,核算 MD5

DiskLruCache 缓存是针对文件进行操作,所以写入文件经过 FileOutputStream 进行,在写入操作时,咱们将写入流进行代理,监听 write 操作,并核算 MD5 数据,当写入完成时,获取核算的MD5数据,并将数据缓存到 MMKV 文件内即可;

  1. 监听移除操作,删去 MD5

因为 DiskLruCache 结构内的缓存池选用 LinkedHashMap 记载,所以经过代理 LinkedHashMap,监听内存移除操作,同步移除 MD5 数据;

  1. 读取操作,校验 MD5

读取数据时,将缓存 MD5 数据与文件流核算的 MD5 数据进行比照,假如校验经过,则返回数据流;假如校验失败,则删去数据和 MD5 记载,返回 NULL 数据流即可;

一文搞懂耐久化结构,不明白来打我

读取操作

inline fun <T> applyGet(k: String, getBlock: (inputStream: InputStream?) -> T?): T? {
    val md5K = encodeMD5(k)
    return try {
        val cache = get()
        val snapshot = cache?.get(md5K)
        if (null != snapshot) {
            snapshot.getInputStream(DEFAULT_CACHE_INDEX)?.use { inputStream ->
                // 读取缓存数据,并进行MD5校验
                if (checkMD5 && inputStream is FileInputStream) {
                    val parent = cache.directory.absolutePath
                    // 获取记载的 MD5 值
                    val cacheMD5 = Cache.asMMKV(toMD5File(cacheFile), parent).applyGet { it.decodeString(md5K) }
                    // 获取缓存内容的 MD5 值
                    val readMD5 = encodeMD5(File(parent, "$md5K.$DEFAULT_CACHE_INDEX"))
                    val newStream = if (null != readMD5 && cacheMD5 == readMD5) inputStream else null
                    // 假如MD5校验不经过,则删去缓存文件
                    if (null == newStream) remove(k)
                    getBlock.invoke(newStream)
                } else {
                    getBlock.invoke(inputStream)
                }
            }
        }
    } catch (e: Exception) {
        null
    }
}

写入操作

inline fun applyPut(k: String, putBlock: (outputStream: OutputStream) -> Boolean): String? {
    val md5K = encodeMD5(k)
    var filePath: String? = null
    try {
        val cache = get()
        val editor = cache?.edit(md5K)
        if (null != editor) {
            val output = editor.newOutputStream(DEFAULT_CACHE_INDEX).let { os ->
                if (checkMD5) MD5OutputStreamProxy(os) else os
            }
            val result = output.use { outputStream ->
                val tempResult = putBlock.invoke(outputStream)
                if (tempResult && outputStream is MD5OutputStreamProxy) {
                    val parent = cache.directory.absolutePath
                    // 记载缓存内容的 MD5 值
                    Cache.asMMKV(toMD5File(cacheFile), parent).applyPut { it.encode(md5K, outputStream.getMD5()) }
                }
                tempResult
            }
            if (result) {
                editor.commit()
            } else {
                editor.abort()
            }
            cache.flush()
            filePath = "${cache.directory.absolutePath}${File.separator}$md5K.$DEFAULT_CACHE_INDEX"
        }
    } catch (e: Exception) {}
    return filePath
}

3.4、Database

数据库是一种具有特定标准的文件,已表结构方法记载文本内容,在拜访数据库时,需求按照相关数据格局进行操作,否则会抛出反常。

首要思路,针对不同的过错场景做针对性的处理;

3.4.1、Room

1、Room 与 SQLiteDatabase 联系图

一文搞懂耐久化结构,不明白来打我

Room 是一种 ORM(目标联系映射) 的数据库拜访结构,经过对 SQLiteDatabase 层的封装,将数据库拜访变成一种面向目标的操作方法,隔离了繁琐的 SQL 句子操作,使咱们更方便的操作数据库;

Room 经过代理的方法,将 Android SQLiteDatabase 数据库层进行隔离和封装,OpenHelper 类用于翻开数据库文件,并构建 Java 层的 Sqlite 数据库的代理目标;

在数据库翻开过程中呈现的反常状况,SQLiteDatabase 会进行处理或抛出;

  • SQLiteDatabaseCorruptException 反常,会删去并从头创立数据库文件;
  • SQLiteCantOpenDatabaseException 反常,直接抛出反常,一般不会呈现,除非呈现歹意修改文件权限;
  • SQLiteException 反常,封闭数据库文件,并抛出反常;
  • 其他反常,直接抛出反常;

Room 数据库封装了 SQLiteDatabase 版别校验机制,在数据库文件翻开(onOpen办法回调)时,经过 room_master_table 表记载的数据库文件 hash 值进行校验,判别数据库内容是否改变;假如改变则会抛出反常1

2、数据库翻开时序图

一文搞懂耐久化结构,不明白来打我

数据库被创立时,文件权限被设置为660(可读可写不行执行);

3、数据库翻开时回调函数介绍

// 数据库配置函数
onConfigure(db)
 ->
// 数据库创立/降级/晋级,只会回调其中一个
onCreate(db)/onDowngrade(db)/onUpgrade(db)
 ->
 // 数据库翻开
onOpen(db)

3.4.2、反常状况处理战略

  1. 数据库内容调整,版别不匹配问题?

数据库内容结构调整,假如不做对应的版别晋级,则翻开 DB 文件时,文件的 Hash 值校验不经过不经过,抛出反常,反常信息参考附录信息,反常1

编译期将预制一个 db 文件的 hash 值写入 room_master_table 表内 identity_hash 字段;

Room 计划翻开数据库时,进行 db 文件的校验;

// 代码内固定文件的 hash 值
new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(3) {
      @Override
      public void createAllTables(SupportSQLiteDatabase _db) {
        _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
        _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '624fe991d905e44a18ac0de917f71846')");
      }
      ...
    }, "624fe991d905e44a18ac0de917f71846", "53403c8c536d8f7f961fbd2d9235c249");
// 翻开 DB 文件,校验文件 hash 值
private void checkIdentity(SupportSQLiteDatabase db){
...
  if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
    throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
            + " you've changed schema but forgot to update the version number. You can"
            + " simply fix this by increasing the version number.");
  }
  ...
}

影响:DB 文件版别办理反常,导致文件无法翻开,数据无法写入,影响正常的事务;

战略:鉴于 DB 文件在创立或晋级时,会写入或更新 room_master_table 表内记载的 hash 值;在翻开 DB 文件进行校验时,保证 Hash 校验经过;

数据库表结构调整时,做好版别的晋级配置和版别号办理,编译生成的 hash 值与 db 文件内记载的 hash 一致,保证文件校验经过。 2. 数据库文件名不合法问题?

运用内选用 id 生成 DB 文件名,来保证用户信息的独立存储;

当 id 呈现不合法字符时,导致数据库文件名不符合文件命名规则,数据库文件创立失败,抛出反常,反常信息参考附录信息,反常3

影响: DB 文件无法创立,数据无法写入,影响正常的事务;

战略: 避免数据库文件名不合法,动态创立 DB 文件时,对文件名进行 MD5 处理,将 MD5 值作为新的文件名,保证文件名的唯一性与合法性;

案例:

// 对 id 进行 MD5 处理
private fun validDatabaseFileName(id :String):String{
    return "${DATABASE_NAME_PREFIX}${MD5Utils.encodeMD5(id)}.db"
}
  1. 其他反常

歹意损坏数据库文件或修改数据库文件权限,在加载数据库文件时,无法运用数据库文件;

影响: DB 文件无法创立,数据无法写入,影响正常的事务;

战略:封闭DB,并从头翻开数据库文件;

针对不能自行康复的反常类型,考虑选用删去数据库文件,并走 reopen 流程;

  • SQLiteCantOpenDatabaseException 文件权限反常,导致数据库文件无法拜访;
  • SQLiteDiskIOException 文件读写内容反常,非数据库文件;

SQLiteCantOpenDatabaseException 和 SQLiteDiskIOException 反常无法自行康复,能够考虑删去数据库文件,并重置db目标引证,再次运用数据库时能够从头走创立和翻开流程,创立一个新的数据库文件;

当数据库文件无法翻开时,无法获取到db目标,所以只能经过文件名来定位数据库文件,做删去数据库文件处理,删去数据库文件需求一起删去数据库相干系的文件(.db、.db-shm、*.db-wal);

// 数据库发生不能自行康复的反常,做删去数据库文件处理
private fun deleteDBFile(db: RoomDatabase?, e: Throwable?, dbName: String) {
    if (!isHandleException(e)) return
    try {
        // 封闭数据库
        if (db?.isOpen == true) db.close()
        // 获取数据库文件
        val dbFile = ApplicationHolder.get().getDatabasePath(dbName)
        // 删去数据库相关文件,db/db-shm/db-wal
        SQLiteDatabase.deleteDatabase(dbFile)
    } catch (e: Exception) {
    }
}
// 过滤需求处理的反常场景
private fun isHandleException(e: Throwable?): Boolean {
    return when (e) {
        is SQLiteCantOpenDatabaseException,
        is SQLiteDiskIOException -> true
        else -> false
    }
}

危险点:该计划经过自测具有一定的可行性,但是对事务影响较大,需求结合事务场景慎重考虑;

3.4.3、反常信息附录

反常1:

// 表结构调整,导致数据库版别不匹配问题
 java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
	at androidx.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:154)
	at androidx.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:135)
	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onOpen(FrameworkSQLiteOpenHelper.java:195)
	at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:427)
	at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:316)
	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
	at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
	at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)

反常2:

// 数据库文件损坏
Failed to open database '/data/user/0/packageName/databases/global_database.db'.
  android.database.sqlite.SQLiteDiskIOException: disk I/O error (code 522 SQLITE_IOERR_SHORT_READ): , while compiling: PRAGMA journal_mode
  	at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
  	at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:1045)
  	at android.database.sqlite.SQLiteConnection.executeForString(SQLiteConnection.java:788)
  	at android.database.sqlite.SQLiteConnection.setJournalMode(SQLiteConnection.java:405)
  	at android.database.sqlite.SQLiteConnection.setWalModeFromConfiguration(SQLiteConnection.java:335)
  	at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:258)
  	at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:205)
  	at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:505)
  	at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:206)
  	at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:198)
  	at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:918)
  	at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:898)
  	at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:762)
  	at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:751)
  	at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:373)
  	at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:316)
  	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(FrameworkSQLiteOpenHelper.java:145)
  	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
  	at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
  	at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)

反常3:

// 数据库文件名不合法问题
java.lang.IllegalArgumentException: File pidGK+IhdVCOCcTHwItmUfNAz/vgmp99F7G4ZyoSVomUbY=.db contains a path separator
	at android.app.ContextImpl.makeFilename(ContextImpl.java:2871)
	at android.app.ContextImpl.getDatabasePath(ContextImpl.java:921)
	at android.content.ContextWrapper.getDatabasePath(ContextWrapper.java:351)
	at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:370)
	at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:316)
	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase(
	at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:106)
	at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:622)
	at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.java:399)

*注意事项

  • 保证数据库文件名的合法;
  • 保证数据库表结构调整的版别晋级办理;