---
存储优化
---

#### 目录

1. 思维导图
2. Android 的存储基础
   - Android 分区
   - Android 存储安全
3. 常见的数据存储方法
   1. 关键要素
   2. 存储选项
      - SharedPreferences
      - ContentProvider
      - 文件
      - 数据库
4. 参考

#### 思维导图

![](https://i.loli.net/2019/01/15/5c3dbef17beaf.png)

#### Android 的存储基础

##### Android 分区

Android 系统可以通过 df 命令来查看分区情况，常见的分区如下：

| 分区名  | 解释                                 |
| ------- | ------------------------------------ |
| /system | 操作系统预留，用来存储系统文件和框架 |
| /data   | 存储用户数据                         |
| /cache  | 系统升级过程使用的分区或 recovery    |
| /vendor | 用来存储厂商对 Android 系统的修改    |
| /storge | 外置或内置 sdcard                    |

不同的分区可以使用不同的文件系统。

##### Android 存储安全

除了对数据的分区隔离，存储安全也是 Android 系统非常重要的一部分，它分为两部分：

1. 权限控制
2. 数据加密

#### 常见的数据存储方法

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

在选择某种数据存储方法之前，一般需要考虑哪些关键要素呢？

##### 关键要素

| 要素     | 解释                                                         |
| -------- | ------------------------------------------------------------ |
| 正确性   | 判断它是否靠谱。存储是否完备，有没有支持多线程或者跨进程；内部是否健壮，有没有考虑异常情况下数据的校验和恢复，比如采用双写和备份文件策略； |
| 时间开销 | 时间开销包括 CPU 时间和 I/O 时间，I/O 存储是非常慢的，但是如果存储方法中比如编解码或者加解密等设计的很复杂，整个数据存储过程也会拖慢 CPU 时间 |
| 空间开销 | 即使是相同的数据如果采用不同的编码方式，占用的空间也有所不同。相同的数据所占的空间大小是 XML > Json > Protocol Buffer |
| 安全     | 对于一些敏感的数据，可能需要加密存储等                       |
| 开发成本 | 尽可能低成本                                                 |
| 兼容性   |                                                              |

##### 存储选项

- SharedPreferences

  SP 使用简单，但是问题也很多。常见的问题有：

  1. 非进程安全

     就算使用 MODE_MULTI_PROCESS ，SP 在跨进程读写也会导致数据丢失，大约会有万分之一的损坏率。

  2. 加载缓慢

     SP 文件的加载使用了异步线程，但是并没有设置线程优先级，如果这个时候 getXxx/putXxx 就会阻塞线程。这就导致出现主线程等待低线程优先级锁的问题。

  3. 全量写入

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

  4. 卡顿

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

  所以，SP 只能适用于存储少量数据，每个 SP 文件不能过大，最好能把频繁写的数据隔离开。

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

  即使这样，也不能彻底解决文件，最好使用微信开源的 MMKV。

  [https://github.com/Tencent/MMKV](https://github.com/Tencent/MMKV)

  MMKV 利用文件锁保证跨进程的安全、使用 mmap 保证数据不会丢失、选用性能和存储空间更好的 Protocol Buffer 替换 XML、支持增量更新等。

- ContentProvider

  ContentProvider 支持跨进程通信，但是也有几点需要注意：

  1. 启动性能

     ContentProvider 的生命周期默认在 Application onCreate() 之前，而且都是在主线程创建的。自定义的 ContentProvider 不要再类构造方法、静态代码块、onCreate 方法中做耗时操作。

  2. 稳定性

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

     ![](https://i.loli.net/2019/01/14/5c3c7116737ab.jpg)

     基于 mmap 的匿名共享内存机制也是有代价的，当传输的数据量非常小的时候，可能不一定划算，所以 ContentProvider 提供了一种 call 函数，它会直接通过 Bindler 来传递数据。

     Android 的 Bindler 传输是有大小限制的，一般来说限制是 1~2 MB。ContentProvider 的接口调用参数和 call 函数调用并没有使用匿名共享机制，比如要批量插入很多数据，那么就会出现一个插入数据的数组，如果这个数组太大了，那么这个操作就可能会出现数据超大异常。

  3. 安全性

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

  总的来说，ContentProvider 这套方案实现相对比较笨重，适合传输大的数据。

- SQLite

  总的来说，通过引入 ORM，可以大大的提升我们的开发效率；通过 WAL 模式和连接池，可以提高 SQLite 的并发性能；通过正确的建立索引，可以提升 SQLite 的查询速度；通过调整默认页大小和缓存大小，可以提升 SQLite 的整体性能。

  详见：[SQLite](https://github.com/Omooo/Android-Notes/blob/master/blogs/SQLite.md)

#### 参考

[存储优化（上）：常见的数据存储方法有哪些？](https://time.geekbang.org/column/article/76677)

[存储优化（中）：如何优化数据存储？](https://time.geekbang.org/column/article/76985)

[存储优化（下）：数据库SQLite的使用和优化](https://time.geekbang.org/column/article/77546)