You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

8.4 KiB

存储优化

目录

  1. Android 的存储基础
  2. 常见的数据存储方法
    • SharedPreferences
    • ContentProvider
    • 文件
    • 数据库
  3. 对象序列化
  4. 数据序列化

Android 的存储基础

常见的数据存储方法

存储就是把特定的数据结构转化成可以被记录和还原的格式,这个数据格式可以是二进制的,也可以是 XML、JSON、Protocol Buffer 这些格式。

SharedPreferences

SharedPreferences 是 Android 中比较常见的存储方法,它可以用来存储一些比较小的键值对集合。但是它也有很多问题:

  1. 跨进程不安全

    由于没有使用跨进程的锁,就算使用 MODE_MULTI_PROCESS,SharedPreferences 在跨进程频繁读写有可能导致数据全部丢失。SP 大约会有万分之一的损坏率。

  2. 加载缓慢

    SP 文件的加载使用异步线程,而且加载线程并没有设置线程优先级,如果这个时候线程读取数据需要等待文件加载线程的结束。这就导致出现主线程等待低优先级线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50~100ms,建议提前用异步线程预加载启动过程用到的 SP 文件。

  3. 全量写入

    无论是调用 commit 还是 apply,即使我们只改动其中的一个条目,都会把整个内容全部写到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次,这也是性能差的重要原因之一。

  4. 卡顿

    由于提供了异步落盘的 apply 机制,在崩溃或者其他一些异常情况可能会导致数据丢失。所以当应用收到系统广播,或者被调用 onPause 等一些时机,系统会强制把所有的 SP 对象数据落地到磁盘。如果没有落地成功,这时候主线程会被一直阻塞。这样非常容易造成卡顿,甚至是 ANR,从线上数据来看,SP 卡断占比一般会超过 5%。

系统提供的 SP 的应用场景是用来存储一些非常简单、轻量的数据。我们不要用它存储一些过于复杂的数据,例如 HTML、JSON 等。而且 SP 的文件存储性能与文件大小相关,每个 SP 文件不能过大,我们不要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来。

我们也可以替换复写 Application 的 getSharedPreferences 方法替换系统默认实现,比如优化卡顿、合并多次 apply 操作,支持跨进程操作等。

当然,还是建议使用微信开源的 MMKV。

ContentProvider

ContentProvider 提供了不同进程甚至不同应用程序之间共享数据的机制。

不过,使用过程中也有几点注意。

  1. 启动性能

    ContentProvider 的生命周期默认在 Application onCreate 之前,而且都是在主线程创建的。我们自定义的 ContentProvider 类的构造函数、静态代码块、onCreate 函数都尽量不要做耗时的操作,会拖慢启动速度。

  2. 稳定性

    ContentProvider 在进行跨进程数据传递时,利用了 Android 的 Binder 和匿名共享内存机制。简单来说,就是通过 Binder 传递 CursorWindow 对象内部的匿名共享文件的文件描述符。这样在跨进程传输中,结果数据不需要跨进程传输,而是在不同进程中通过传输的匿名共享内存文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的。

  3. 安全性

    虽然 ContentProvider 为应用程序之间的数据共享提供了很好的安全机制,但是如果 ContentProvider 是 exported,当支持执行 SQL 语句时就需要注意 SQL 注入的问题。另外如果我们传入的参数是一个文件路径,然后返回文件的内容,这个时候也要校验合法性,不然整个应用的私有数据都有可能被别人拿到。

SQLite

这里直接推荐之前的写的文章:SQLite

对象序列化

更宽泛的来讲,数据存储不一定就是将数据存放到磁盘中,比如放到内存中、通过网络传输也可以算是存储的一种形式。或者我们也可以把这个过程叫做对象或者数据的序列化。

对象序列化就是把一个 Object 对象所有的信息表示成一个字节序列,这包括 Class 信息、继承关系信息、访问权限、变量类型以及数值信息等。

Serializable

Serializable 是 Java 原生的序列化机制,在 Android 中也有被广泛使用。我们可以通过 Serializable 将对象持久化存储,也可以通过 Bundle 传递 Serializable 的序列化数据。

原理:

Serializable 的原理是通过 ObjectInputStream 和 ObjectOutputStream 来实现的。整个序列化过程中使用了大量的反射和临时变量,而且在序列化对象的时候,不仅会序列化当前对象本身,还需要递归序列化对象引用的其他对象。

整个过程计算非常复杂,而且因为存在大量反射和 GC 的影响,序列化的性能会比较差。另外一方面因为序列化文件需要包含的信息非常多,导致它的大小比 Class 文件本身还要大很多,这样又会导致 I/O 读写上的性能问题。

进阶使用:

我们可以通过重写 writeObject 和 readObject 方法来替代默认流程,它会先反射判断是否存在我们自己实现的序列化方法 writeObject 或反序列化方法 readObject。通过这两个方法,我们可以对某些字段做一些特殊修改,也可以实现序列化的加密功能。

writeReplace 和 readResolve 方法这两个方法代理序列化的对象,可以实现自定义返回的序列化实例。那它有什么用呢?我们可以通过它们实现对象序列化的版本兼容,例如通过 readResolver 方法可以把老版本的序列化对象转化成新版本的对象类型。

注意事项:

Serializable 虽然使用起来非常简单,但是也有一些需要注意的事项字段。

  1. 不被序列化的字段

    类的 static 变量以及被声明为 transient 的字段,默认的序列化机制都会忽略该字段,不会进行序列化存储。当然我们也可以使用进阶的 writeReplace 和 readResolve 方法做自定义的序列化存储。

  2. serialVersionUID

    在类实现了 Serializable 接口后,我们需要添加一个 Serial Version ID,它相当于类的版本号。这个 ID 我们可以显式声明也可以让编译器自己计算。通常我建议显式声明会更加稳妥,因为隐式声明假如类发生了一点点变化,进行反序列都会由于 serialVersionUID 改变而导致 InvalidClassException 异常。

  3. 构造方法

    Serializable 的反序列化默认是不会执行构造方法的,它是根据数据流中对 Object 的描述信息创建对象的。如果一些逻辑依赖构造方法,就可能会出现问题。

Parcelable

由于 Java 的 Serializable 的性能较低,Android 需要重新设计一套更加轻量且高效的对象序列化和反序列机制。Parcelable 正是在这个背景下产生的,它核心的作用就是为了解决 Android 中大量跨进程通信的性能问题。

Parcelable 只会在内存中进行序列化操作,并不会将数据存储到磁盘里。使用起来相比 Serializable 会复杂很多,需要手动添加自定义代码。

虽然可以通过取巧的方法可以实现 Parcelable 的永久存储,但是它也存在两个问题。

  1. 系统版本的兼容性

    由于 Parcelable 设计本意是在内存中使用的,我们无法保证所有 Android 版本的 Parcel.app 实现都完全一致。如果不同系统版本实现有所差异,或者有厂商修改了实现,可能会存在问题。

  2. 数据前后兼容性

    Parcelable 并没有版本管理的设计,如果我们类的版本出现升级,写入的顺序以及字段类型的兼容都需要格外注意,这也带来了很大的维护成本。

一般来说,如果需要持久化存储的话,一般还是不得不选择性能更差的 Serializable 方案。

Serial

Twitter 开源的高性能序列化方案 Serial

数据序列化

Serial 性能看起来还不错,但是对象的序列化要记录的信息还是比较多,在操作比较频繁的时候,对应用的影响还是不少的,这个时候可以选择数据的序列化。

Json
Protocol Buffers