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

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

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

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)

*注意事项

  • 确保数据库文件名的合法;
  • 确保数据库表结构调整的版本升级管理;

如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料


在这里插入图片描述
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )

  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值