当前位置:
文档之家› 12讲存储优化(上):常见的数据存储方法有哪些
12讲存储优化(上):常见的数据存储方法有哪些
可能有些同学会问了,Android的这两种设备加密方法跟应用的加密有什么不同,我们在应用存储还需要单独的给敏感文件加 密吗? 我想说的是,设备加密方法对应用程序来说是透明的,它保证我们读取到的是解密后的数据。对于应用程序特别敏感的数据, 我们也需要采用RSA、AES、chacha20等常用方式做进一步的存储加密。
全量写入。无论是调用commit()还是apply(),即使我们只改动其中的一个条目,都会把整个内容全部写到文件。而且即使 我们多次写入同一个文件,SP也没有将多次修改合并为一次,这也是性能差的重要原因之一。
卡顿。由于提供了异步落盘的apply机制,在崩溃或者其他一些异常情况可能会导致数据丢失。所以当应用收到系统广播, 或者被调用onPause等一些时机,系统会强制把所有的SharedPreferences对象数据落地到磁盘。如果没有落地完成,这时 候主线程会被一直阻塞。这样非常容易造成卡顿,甚至是ANR,从线上数据来看SP卡顿占比一般会超过5%。
12讲存储优化(上):常⻅的数据存储方法有哪些
通过专栏前面我讲的I/O优化基础知识,相信你肯定了解了文件系统和磁盘的一些机制,以及不同I/O方式的使用场景以及优缺 点,并且可以掌握如何在线上监控I/O操作。 万丈高楼平地起,在理解并掌握这些基础知识的同时,你肯定还想知道如何利用这些知识指导我们写出更好的代码。 今天我来结合Android系统的一些特性,讲讲开发过程中常⻅存储方法的优缺点,希望可以帮你在日常工作中如何做出更好的 选择。
我们也可以替换通过复写Application的getSharedPreferences方法替换系统默认实现,比如优化卡顿呢?在今天的Sample中我也提供了一个简单替换实现。
public class MyApplication extends Application { @Override public SharedPreferences getSharedPreferences(String name, int mode) { return SharedPreferencesImpl.getSharedPreferences(name, mode); }
2. Android存储安全 除了数据的分区隔离,存储安全也是Android系统非常重要的一部分,存储安全首先考虑的是权限控制。 第一,权限控制 Android的每个应用都在自己的应用沙盒内运行,在 Android 4.3之前的版本中,这些沙盒使用了标准Linux的保护机制,通过 为每个应用创建独一无二的Linux UID来定义。简单来说,我们需要保证微信不能访问淘宝的数据,并且在没有权限的情况下 也不能访问系统的一些保护文件。 在Android 4.3引入了SELinux(Security Enhanced Linux)机制进一步定义Android应用沙盒的边界。那它有什么特别的呢? 它的作用是即使我们进程有root权限也不能为所欲为,如果想在SELinux系统中干任何事情,都必须先在专⻔的安全策略配置 文件中赋予权限。 第二,数据加密 除了权限的控制,用户还会担心在手机丢失或者被盗导致个人隐私数据泄露。加密或许是一个不错的选择,它可以保护丢失或 被盗设备上的数据。 Android有两种设备加密方法:全盘加密和文件级加密。全盘加密是在Android 4.4中引入的,并在Android 5.0中默认打开。它 会将/data分区的用户数据操作加密/解密,对性能会有一定的影响,但是新版本的芯片都会在硬件中提供直接支持。 我们知道,基于文件系统的加密,如果设备被解锁了,加密也就没有用了。所以Android 7.0增加了基于文件的加密。在这种 加密模式下,将会给每个文件都分配一个必须用用户的passcode推导出来的密钥。特定的文件被屏幕锁屏之后,直到用户下 一次解锁屏幕期间都不能访问。
2. 存储选项
总的来说,我们需要结合应用场景选择合适的数据存储方法。那Android为应用开发者提供了哪些存储数据的方法呢?你可以 参考存储选项,综合来看,有下面几种方法。
SharedPreferences
ContentProvider
文件
数据库
今天我先来讲SharedPreferences和ContentProvider这两个存储方法,文件和数据库将放到“存储优化”后面两期来讲。
加载缓慢。SharedPreferences文件的加载使用了异步线程,而且加载线程并没有设置线程优先级,如果这个时候主线程读 取数据就需要等待文件加载线程的结束。这就导致出现主线程等待低优先级线程锁的问题,比如一个100KB的SP文件读取 等待时间大约需要50~100ms,我建议提前用异步线程预加载启动过程用到的SP文件。
第二,ContentProvider的使用。 为什么Android系统不把SharedPreferences设计成跨进程安全的呢?那是因为Android系统更希望我们在这个场景选择使用 ContentProvider作为存储方式。ContentProvider作为Android四大组件中的一种,为我们提供了不同进程甚至是不同应用程序 之间共享数据的机制。 Android系统中比如相册、日历、音频、视频、通讯录等模块都提供了ContentProvider的访问支持。它的使用十分简单,你可 以参考官方文档。 当然,在使用过程也有下面几点需要注意。
讲到这里,如果你对SharedPreferences机制还不熟悉的话,可以参考《彻底搞懂SharedPreferences》。
坦白来讲,系统提供的SharedPreferences的应用场景是用来存储一些非常简单、轻量的数据。我们不要使用它来存储过于 复杂的数据,例如HTML、JSON等。而且SharedPreference的文件存储性能与文件大小相关,每个SP文件不能过大,我们不 要将毫无关联的配置项保存在同一个文件中,同时考虑将频繁修改的条目单独隔离出来。
什么是分区呢?分区简单来说就是将设备中的存储划分为一些互不重叠的部分,每个部分都可以单独格式化,用作不同的目 的。这样系统就可以灵活的针对单独分区做不同的操作,例如在系统还原(recovery)过程,我们不希望会影响到用户存储的
数据。
从上面的表中你可以看到,每个分区非常独立,不同的分区可以使用的不同的文件系统。其中比较重要的有: /system分区:它是存放所有Google提供的Android组件的地方。这个分区只能以只读方式mount。这样主要基于稳定性和 安全性考虑,即使发生用户突然断电的情况,也依然需要保证/system分区的内容不会受到破坏和篡改。 /data分区:它是所有用户数据存放的地方。主要为了实现数据隔离,即系统升级和恢复的时候会擦除整个/system分区,但 是却不会影响/data的用户数据。而恢复出厂设置,只会擦除/data的数据。 /vendor分区:它是存放厂商特殊系统修改的地方。特别是在Android 8.0以后,隆重推出了“Treble”项目。厂商OTA时可以 只更新自己的/vendor分区即可,让厂商能够以更低的成本,更轻松、更快速地将设备更新到新版Android系统。
稳定性 ContentProvider在进行跨进程数据传递时,利用了Android的Binder和匿名共享内存机制。简单来说,就是通过Binder传递 CursorWindow对象内部的匿名共享内存的文件描述符。这样在跨进程传输中,结果数据并不需要跨进程传输,而是在不同进 程中通过传输的匿名共享内存文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的。
}
对系统提供的SharedPreferences的小修小补虽然性能有所提升,但是依然不能彻底解决问题。基本每个大公司都会自研一套 替代的存储方案,比如微信最近就开源了MMKV。 下面是MMKV对于SharedPreferences的“六要素”对比。
你可以参考MMKV的实现原理和性能测试报告,里面有一些非常不错的思路。例如利用文件锁保证跨进程的安全、使用mmap 保证数据不会丢失、选用性能和存储空间更好的Protocol Buffer代替XML、支持增量更新等。 根据I/O优化的分析,对于频繁修改的配置使用mmap的确非常合适,使用者不用去理解apply()和commit()的差别,也不用担心 数据的丢失。同时,我们也不需要每次都提交整个文件,整体性能会有很大提升。
启动性能 ContentProvider的生命周期默认在Application onCreate()之前,而且都是在主线程创建的。我们自定义的ContentProvider类 的构造函数、静态代码块、onCreate函数都尽量不要做耗时的操作,会拖慢启动速度。
可能很多同学都不知道ContentProvider还有一个多进程模式,它可以和AndroidManifest中的multiprocess属性结合使用。这 样调用进程会直接在自己进程里创建一个push进程的Provider实例,就不需要跨进程调用了。需要注意的是,这样也会带来 Provider的多实例问题。
常⻅的数据存储方法
Android为我们提供了很多种持久化存储的方案,在具体介绍它们之前,你需要先问一下自己,什么是存储? 每个人可能都会有自己的答案,在我看来,存储就是把特定的数据结构转化成可以被记录和还原的格式,这个数据格式可以是 二进制的,也可以是XML、JSON、Protocol Buffer这些格式。 对于闪存来说,一切归根到底还是二进制的,XML、JSON它们只是提供了一套通用的二进制编解码格式规范。既然有那么多 存储的方案,那我们在选择数据存储方法时,一般需要考虑哪些关键要素呢? 1. 关键要素 在选择数据存储方法时,我一般会想到下面这几点,我把它们总结给你。
Android的存储基础
在讲具体的存储方法之前,我们应该对Android系统存储相关的一些基础知识有所了解。 1. Android分区 I/O优化中讲到的大部分知识更侧重Linux系统,对于Android来说,我们首先应该对Android分区的架构和作用有所了解。在我 们熟悉的Windows世界中,我们一般都把系统安装在C盘,然后还会有几个用来存放应用程序和数据的分区。 Android系统可以通过/proc/partitions或者df命令来查看的各个分区情况,下图是Nexus 6中df命令的运行结果。