一文搞懂持久化框架,不懂来打我

本地持久化存储数据,根据业务场景选择合适的存储方案;有高效读写、空间限制等不同的业务场景,了解各持久化方案的优缺点,选择适当的持久化方案。

本文在介绍持久化方案,根据不同的方案,论证了需要考虑文件校验的可行性,以及具体的校验方案;

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 文件的解析,如果文件损坏,则会抛出异常处理。

  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*** 小节分析; 具体标识含义如下所示: ```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 记录到日志文件; 删除分两种情况:一种是用户主动删除,一种是达到上限自动删除;操作过程相同; ![image.png](https://file.jishuzhan.net/article/1755513607384207361/f8c5ac087a812b8fad039ba0d098ecdd.webp) 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 数据流即可; ![image.png](https://file.jishuzhan.net/article/1755513607384207361/80422eefe58da99bf40785312c187710.webp) 读取操作 ```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 关系图 ![image.png](https://file.jishuzhan.net/article/1755513607384207361/ed0da8cc5d090c7f3d36ebaddeff50b4.webp) Room 是一种 ORM(对象关系映射) 的数据库访问框架,通过对 SQLiteDatabase 层的封装,将数据库访问变成一种面向对象的操作方式,隔离了繁琐的 SQL 语句操作,使我们更方便的操作数据库; Room 通过代理的方式,将 Android SQLiteDatabase 数据库层进行隔离和封装,OpenHelper 类用于打开数据库文件,并构建 Java 层的 Sqlite 数据库的代理对象; 在数据库打开过程中出现的异常情况,SQLiteDatabase 会进行处理或抛出; * SQLiteDatabaseCorruptException 异常,会删除并重新创建数据库文件; * SQLiteCantOpenDatabaseException 异常,直接抛出异常,一般不会出现,除非出现恶意修改文件权限; * SQLiteException 异常,关闭数据库文件,并抛出异常; * 其他异常,直接抛出异常; Room 数据库封装了 SQLiteDatabase 版本校验机制,在数据库文件打开(onOpen方法回调)时,通过 room_master_table 表记录的数据库文件 hash 值进行校验,判断数据库内容是否变化;如果变化则会抛出***异常1***; 2、数据库打开时序图 ![image.png](https://file.jishuzhan.net/article/1755513607384207361/91762df6c262b1db3ac0615685166fce.webp) **数据库被创建时,文件权限被设置为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) ``` \**注意事项* * 确保数据库文件名的合法; * 确保数据库表结构调整的版本升级管理;

相关推荐
编程乐学(Arfan开发工程师)3 小时前
06、基础入门-SpringBoot-依赖管理特性
android·spring boot·后端
androidwork4 小时前
使用 Kotlin 和 Jetpack Compose 开发 Wear OS 应用的完整指南
android·kotlin
繁依Fanyi5 小时前
Animaster:一次由 CodeBuddy 主导的 CSS 动画编辑器诞生记
android·前端·css·编辑器·codebuddy首席试玩官
奔跑吧 android7 小时前
【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
android·bluetooth·bt·aosp13·storagemodule
田一一一11 小时前
Android framework 中间件开发(三)
android·中间件·framework·jni
androidwork15 小时前
掌握 Kotlin Android 单元测试:MockK 框架深度实践指南
android·kotlin
田一一一16 小时前
Android framework 中间件开发(二)
android·中间件·framework
追随远方16 小时前
FFmpeg在Android开发中的核心价值是什么?
android·ffmpeg
神探阿航17 小时前
HNUST湖南科技大学-安卓Android期中复习
android·安卓·hnust
千里马-horse19 小时前
android vlc播放rtsp
android·media·rtsp·mediaplayer·vlc