本地持久化存储数据,根据业务场景选择合适的存储方案;有高效读写、空间限制等不同的业务场景,了解各持久化方案的优缺点,选择适当的持久化方案。
本文在介绍持久化方案,根据不同的方案,论证了需要考虑文件校验的可行性,以及具体的校验方案;
1、MMKV
CRC算法参考链接
鉴于 MMKV 自身已做 CRC 算法的校验,本方案不做过多研究。
关于 MMKV 的 CRC 校验处理,详细可以参考 MMKV 源码(链接 )。
2、SharedPreferences
文件损坏主要有以下两种场景:
运行中,检测文件损坏(此种场景可以忽略);
进程重启,检测文件损坏;
因为 SP 方案是对 XML 文件进行操作,XML 文件属于固定格式的文件,所以文件损坏或缺失,解析 XML 文件即可抛出异常,我们可以比较容易的处理该场景。
2.1、操作损坏文件场景
向 XML 文件写入任意文本
不符合 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 规范
less
复制代码
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编码字符)
less
复制代码
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 文件的解析,如果文件损坏,则会抛出异常处理。
文件加载和解析
SP 在创建缓存对象时,首先会解析 XML 文件,并将解析的数据写入缓存池中;
数据读取
数据写入
写入缓存,将数据写入缓存池(HashMap),Key-Value 形式存储;
写入文件,如果文件格式损坏、无读写权限或不存在时,会创建新的文件并赋予文件读写权限;生成文件输出流,然后将缓存 map 逐条转成 XML 格式数据, 并将 XML 数据流写入文件;
数据删除
删除属于数据写入的一个子项,主要针对内存的操作,将内存中缓存的条目删除,写入文件操作同数据写入部分的写入文件流程。
注意事项
-- 鉴于 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*** 小节分析;
具体标识含义如下所示:
```objectivec
// 用于跟踪创建或更新的文件
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 数据结构,记录着每条数据的缓存文件和数据大小;
2. get 操作
从 MAP 缓存池中查找数据记录,如果没有则返回空;如果有则创建一个 Snapshot 对象,用于数据流的读取操作,并写入 READ 记录到日志文件;
3. edit 操作
从 MAP 缓存池查找数据 Entry,无则创建一条并写入缓存池;有则直接使用;通过Entry 创建文件编辑器 Editor,用于数据流的写入操作,写入 DIRTY 记录到日志文件;当用户数据流写入完成,触发 commit 操作时,写入 CLEAN 记录到日志文件,标志数据写入成功;触发 abort 操作时,写入 REMOVE 记录到日志文件,并将缓存池中的数据删除;
4. 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 数据;
2. 校验 MD5
* 读取文件的 MD5 数据,通过文件输入流计算 MD5 数据;
* 读取缓存的 MD5 数据;
* MD5 进行对比校验;
3. 删除 MD5
* 缓存文件删除时,同步删除 MD5 记录;
**2、MD5 管理框架**
MD5 管理框架,主要涉及3个操作的实现,通过代理方案,用于监听数据的写入和删除操作;在读取数据,进行 MD5 校验。
1. 监听写入操作,计算 MD5
DiskLruCache 缓存是针对文件进行操作,所以写入文件通过 FileOutputStream 进行,在写入操作时,我们将写入流进行代理,监听 write 操作,并计算 MD5 数据,当写入完成时,获取计算的MD5数据,并将数据缓存到 MMKV 文件内即可;
2. 监听移除操作,删除 MD5
由于 DiskLruCache 框架内的缓存池采用 LinkedHashMap 记录,所以通过代理 LinkedHashMap,监听内存移除操作,同步移除 MD5 数据;
3. 读取操作,校验 MD5
读取数据时,将缓存 MD5 数据与文件流计算的 MD5 数据进行对比,如果校验通过,则返回数据流;如果校验失败,则删除数据和 MD5 记录,返回 NULL 数据流即可;

读取操作
```kotlin
inline fun 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
}
}
```
写入操作
```kotlin
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、数据库打开时回调函数介绍
```scss
// 数据库配置函数
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 文件的校验;
```typescript
// 代码内固定文件的 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 值作为新的文件名,确保文件名的唯一性与合法性;
案例:
```kotlin
// 对 id 进行 MD5 处理
private fun validDatabaseFileName(id :String):String{
return "${DATABASE_NAME_PREFIX}${MD5Utils.encodeMD5(id)}.db"
}
```
3. 其他异常
恶意损坏数据库文件或修改数据库文件权限,在加载数据库文件时,无法使用数据库文件;
影响: DB 文件无法创建,数据无法写入,影响正常的业务;
策略:关闭DB,并重新打开数据库文件;
针对不能自行恢复的异常类型,考虑采用删除数据库文件,并走 reopen 流程;
* SQLiteCantOpenDatabaseException 文件权限异常,导致数据库文件无法访问;
* SQLiteDiskIOException 文件读写内容异常,非数据库文件;
SQLiteCantOpenDatabaseException 和 SQLiteDiskIOException 异常无法自行恢复,可以考虑删除数据库文件,并重置db对象引用,再次使用数据库时可以重新走创建和打开流程,创建一个新的数据库文件;
当数据库文件无法打开时,无法获取到db对象,所以只能通过文件名来定位数据库文件,做删除数据库文件处理,删除数据库文件需要同时删除数据库相关连的文件(*.db、*.db-shm、\*.db-wal);
```kotlin
// 数据库发生不能自行恢复的异常,做删除数据库文件处理
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:
```less
// 表结构调整,导致数据库版本不匹配问题
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:
```php
// 数据库文件损坏
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:
```less
// 数据库文件名非法问题
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)
```
\**注意事项*
* 确保数据库文件名的合法;
* 确保数据库表结构调整的版本升级管理;