本地耐久化存储数据,依据事务场景选择合适的存储计划;有高效读写、空间限制等不同的事务场景,了解各耐久化计划的优缺陷,选择适当的耐久化计划。
本文在介绍耐久化计划,依据不同的计划,论证了需求考虑文件校验的可行性,以及详细的校验计划;
1、MMKV
鉴于 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 文件的解析,假如文件损坏,则会抛出反常处理。
- 文件加载和解析
- SP 在创立缓存目标时,首先会解析 XML 文件,并将解析的数据写入缓存池中;
- 数据读取
- SP 凭借缓存计划,读取只需求从缓存加载;
- 数据写入
- 写入缓存,将数据写入缓存池(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 小节剖析;
详细标识含义如下所示:
// 用于盯梢创立或更新的文件
DIRTY 63b6677a0dfa1a48b7a91f7581af5d20
// 符号文件写入成功
CLEAN 63b6677a0dfa1a48b7a91f7581af5d20 1657
// 符号文件删去成功
REMOVE c78f32b5eeab84186e6f19e9304b9d4a
// 符号文件读取成功
READ 8df1397acc54cffdad3a1fd8854e3cc6
3.2.2、缓存结构
DiskLruCache 缓存计划,首要触及4类操作,日志文件加载和解析、读操作、写操作和删去操作;
DiskLruCache 会自动办理和数据上限,所以一般状况下,不需求咱们自动调用删去操作,由结构自身内部办理;
从上述4个方面剖析:
- open 操作
一般在构建 DiskLruCache 目标时触发,本操作触及日志文件解析,依据日志文件的格局剖析,每行是一条操作记载,所以需求逐行解析,将解析的 CLEAN 记载放入 MAP 缓存池,遇到 REMOVE、DIRTY 记载,则从缓存池中将数据移除;
MAP 以 K-V 的 方法记载这个每条数据
- Key 是缓存文件去掉后缀的文件名;
- Value 是 Entry 数据结构,记载着每条数据的缓存文件和数据巨细;
- get 操作
从 MAP 缓存池中查找数据记载,假如没有则返回空;假如有则创立一个 Snapshot 目标,用于数据流的读取操作,并写入 READ 记载到日志文件;
- edit 操作
从 MAP 缓存池查找数据 Entry,无则创立一条并写入缓存池;有则直接运用;经过Entry 创立文件编辑器 Editor,用于数据流的写入操作,写入 DIRTY 记载到日志文件;当用户数据流写入完成,触发 commit 操作时,写入 CLEAN 记载到日志文件,标志数据写入成功;触发 abort 操作时,写入 REMOVE 记载到日志文件,并将缓存池中的数据删去;
- remove 操作
从 MAP 缓存池中删去数据,写入 REMOVE 记载到日志文件;
删去分两种状况:一种是用户自动删去,一种是达到上限自动删去;操作过程相同;
- 关于日志文件巨细的问题?
日志文件假如持续写入,则文件巨细无法得到操控,因而 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 数据的办理操作。
- 写入 MD5
- 因为写入数据选用文件输出流的方法,所以考虑在写入文件流时,进行 MD5 核算;
- 在输出流写入完成后,获取整个输出流核算的 MD5 数据,便是整个文件的 MD5 数据;
- 校验 MD5
- 读取文件的 MD5 数据,经过文件输入流核算 MD5 数据;
- 读取缓存的 MD5 数据;
- MD5 进行比照校验;
- 删去 MD5
- 缓存文件删去时,同步删去 MD5 记载;
2、MD5 办理结构
MD5 办理结构,首要触及3个操作的完成,经过代理计划,用于监听数据的写入和删去操作;在读取数据,进行 MD5 校验。
- 监听写入操作,核算 MD5
DiskLruCache 缓存是针对文件进行操作,所以写入文件经过 FileOutputStream 进行,在写入操作时,咱们将写入流进行代理,监听 write 操作,并核算 MD5 数据,当写入完成时,获取核算的MD5数据,并将数据缓存到 MMKV 文件内即可;
- 监听移除操作,删去 MD5
因为 DiskLruCache 结构内的缓存池选用 LinkedHashMap 记载,所以经过代理 LinkedHashMap,监听内存移除操作,同步移除 MD5 数据;
- 读取操作,校验 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、反常状况处理战略
- 数据库内容调整,版别不匹配问题?
数据库内容结构调整,假如不做对应的版别晋级,则翻开 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"
}
- 其他反常
歹意损坏数据库文件或修改数据库文件权限,在加载数据库文件时,无法运用数据库文件;
影响: 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)
*注意事项
- 保证数据库文件名的合法;
- 保证数据库表结构调整的版别晋级办理;