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

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

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

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) ``` \**注意事项* * 确保数据库文件名的合法; * 确保数据库表结构调整的版本升级管理;

相关推荐
莞凰5 小时前
昇腾CANN的“灵脉根基“:Runtime仓库探秘
android·人工智能·transformer
NiceCloud喜云6 小时前
Claude Files API 深入:从上传、复用到配额管理的工程化指南
android·java·数据库·人工智能·python·json·飞书
ujainu6 小时前
CANN pto-isa:虚拟指令集如何连接编译与执行
android·ascend
赏金术士7 小时前
第六章:UI组件与Material3主题
android·ui·kotlin·compose
TechMerger8 小时前
Android 17 重磅重构!服役 20 年的 MessageQueue 迎来无锁改造,卡顿大幅优化!
android·性能优化
yuhuofei202111 小时前
【Python入门】Python中字符串相关拓展
android·java·python
dalancon11 小时前
Android Input Spy Window
android
dalancon12 小时前
InputDispatcher派发事件,查找目标窗口
android
我命由我1234513 小时前
Android Framework P3 - MediaServer 进程、认识 ServiceManager 进程
android·c语言·开发语言·c++·visualstudio·visual studio·android runtime
天才少年曾牛14 小时前
Android14 新增系统服务后,应用调用出现 “hidden api” 警告的原因与解决方案
android·frameworks