Android存储基础
1、Android 分区
分区简单来说就是将设备中的存储划分为一些互不重叠的部分,每个部分都可以单独格式化,用作不同的目的。这样系统可以灵活的针对单独的分区进行不同的操作,而不影响其他分区的数据。
2、Android存储安全
- 权限控制
Android的每个应用都在自己的沙盒内运行,在Android4.3之前采用了标准的Linux保护机制,通过为每个应用创建独一无二的Linux UID来定义。在Android4.3引入了SELinux(Security Enhance Linux)机制进一步定义Android应用沙盒的边界。它的作用在于即使进程有root权限也不能为所欲为,必须要在专门的安全策略配置文件中赋予权限。 - 数据加密
除了权限控制,数据加密也是保护数据的不错选择。Android有两种设备加密方法:全盘加密和文件加密。全盘加密是在Android4.4中引入的,并在Android5.0中默认打开的。它会将/data分区的用户数据操作加密/解密,对性能会有一定的影响,但是新版本的芯片都会在硬件中直接提供支持。基于文件系统的加密,如果设备被解锁了,加密就没有用了,所以Android7.0增加了基于文件的加密。在这种模式下,将会给每个文件分配一个必须用户的passcode推导出来的密钥,特定的文件被屏幕锁屏之后,直到用户下一次解锁屏幕期间都不能访问。
常见数据存储方法
1、关键因素
选择存储方法时一般会考虑一下因素:
2、存储选项
- SharePreferences
- ContentProvider
- 文件
- 数据库
1)SharePreferences的使用
- 跨进程不安全
- 加载缓慢。SharePreferences文件的加载使用了异步线程,而且加载线程没有设置线程优先级,如果这个时候主线程读取数据就需要等待文件加载线程的结束。这就导致出现主线程等待低优先级线程锁的问题。
- 全量写入
无论调用commit()还是apply(),即使我们只改动其中一个条目,都会把整个内容全部写到文件。而且多次写入,SP也没有合并写入的操作。 - 卡顿
由于执行了异步落盘的apply机制,在崩溃或者其他异常情况可能会导致数据丢失。所以当应用收到系统广播,或者被调用OnPause等一些时机,系统会强制把所有的SharePreferences对象数据落地到磁盘,如果没有落地完成,这是主线程会一直被阻塞,这样非常容易造成卡顿,很值ANR。
系统提供的SharePreferences的应用场景是用来存储一些非常简单、轻量的数据,不适合复杂和大量的数据。推荐微信开源MMKV。
2)ContentProvider的使用
- 启动性能
ContentProvider的生命周期默认在Application onCreate()之前,而且在主线程创建的,我们自定义的ContentProvider类的构造函数、静态代码,onCreate函数尽量不要做耗时的操作,会拖慢启动速度。 -
稳定性
ContentProvider在跨进程数据传递时,利用了Android的Binder和匿名共享内存机制。就是通过Binder传递CursorWindow对象内部的匿名共享内存的文件描述符,这样在跨进程传输中,结果数据并不需要跨进程传输,而在不同进程中通过传输匿名共享文件描述符来操作同一块匿名内存,这样来实现不同进程访问相同数据的目的。
基于mmap的匿名共享内存机制也是有代价的,当传输的数量非常小的时候,并不一定划算。所以ContentProvider提供了一种call函数,他会直接通过Binder来传输数据。Android的Binder传输是有大小限制的,一般来说是1-2MB,ContentProvider接口的调用参数和call函数调用并没有使用匿名共享机制,当插入很多数据,那么就会出现个很大的插入数组,那么这个操作可能会出现数据超大异常。
-
安全性
虽然ContentProvider提供了很好的安全机制,但如果ContentProvider支持exported,当支持SQL时要注意SQL注入问题,另外如果传入的参数是文件路径,要校验合法性,不然应用的私有数据可能被别人拿到。
对象序列化
对象序列化就是把一个Object对象所有的信息表示成一个字节序列,包括class信息、继承关系信息、访问权限、变量类型以及数值信息等。
1、Serializable
Serializable是Java原生的序列化机制,广泛地在Android 中使用,可以通过Bundle传递Serializable的序列化数据。
Serializable的原理是通过ObjectInputStream和ObjectOutputStream来实现的。整个序列化过程大量地使用了反射和临时变量,整个计算非常复杂,序列化性能比较差。
- Serializable支持替代默认流程的序列化,它会先反射判断是否存在我们自己实现的序列化方法writeObject和反序列化方法readObject。通过这两个方法可以对某些字段做些特殊修改,也可以实现序列化的加密功能。
- writeReplace和readReplace这个两个方法代理序列化对象,可以实现自定义返回的序列化实例。我们可以通过它们实现对象序列化的版本兼容。
Serializable注意事项: - 不被序列化的字段。类的static变量以及被声明为transient字段,默认的序列化机制都会忽略该字段,不会进行序列化存储。当然我们可以通过writeObject和readObject实现自定义序列化存储方案。
- serialVersionUID。在类实现了Serializable接口后,我们需要添加一个Serial Version ID,它相当于版本号,这个ID可以显示声明也可以让编译器自己计算。通常建议显示声明会更加稳妥,因为隐式声明假如发生了一点点变化,进行反序列化都会由于serialVersionUID的变化而引发InvalidClassException异常。
- 构造方法。 Serializable的反序列化默认是不会执行构造函数的,它会根据数据流中对Object的描述信息创建对象,如果一些逻辑依赖构造函数,就可能出现问题。
2、Parcelable
Parcelable是专门针对Android设计的轻量且高效地对象序列化和反序列化实现方案。Parcelable的实现核心都在Parcel.cpp,它只会在内存中进行序列化操作,并不会将数据存储到磁盘里。也可以通过Parcel.cpp的marshall接口获取byte数组,然后存在文件中从而实现永久保存。
Parcelable注意事项:
Parcelable在使用的时候需要手动添加自定义代码,使用起来比Serializable要麻烦一些,正是因为如此Parcelable才不需要使用反射的方法去实现,提高了性能。
- 系统版本兼容性。由于Parcelable设计的本意是在内存中使用的,无法保证所有Android 版本的Parcel.cpp的实现都完全一致,可能会存在兼容问题。
- 数据前后兼容性。Parcelable并没有版本管理的设计,如果出现版本升级,写入的顺序及字段类型兼容需要格外注意,一般来说,如果需要持久存储的话,还是不得不选择性能较差的Serializable方案。
3、Serial
Teitter开源的高性能序列化方案Serial。从实现原理来看Serial就像是把Parcelable和Serializable的有点集合在一起的方案。
- 由于没有使用反射,比传统方案更加高效
- 开发者对于序列化过程的控制较强,可以定义哪些Object、Field需要序列化
- 有很强的debug能力,可以调试序列化的过程
- 有很强的版本管理能力,可以通过版本号和OptionalFieldException做兼容
数据的序列化
1、JSON
JSON是一种轻量级的数据交换格式,它被广泛的使用在网络传输中,很多应用与服务端的通讯业采用JSON进行通讯。
JSON的优势:
- 相比对象序列化方案,速度更快,体积更小。
- 相比二进制的序列化方案,结果可读,易于排查问题。
-
使用方便,支持跨平台、跨语言,支持嵌套使用。
各平台JSON方案对比
2、Protocol Buffers
Protocol Buffers是Google开源的跨语言编码协议,适用于数据量非常大,对性能要求高的方案。
- 性能
使用二进制编码压缩,相比于JSON体积更小,编解码速度更快 - 兼容性
跨语言支持和前后兼容性都不错,也支持基本类型的自动转换,但不支持继承和引用类型。 -
使用成本
开发成本很高,需要定义.proto文件,并用工具生成对应的辅助类。辅助类特有一些序列化的辅助方法,所有要序列化的对象,都需要先转化为辅助类的对象,这让序列化代码跟业务代码大量耦合,是侵入性较强的一种方式。
序列化方案对比
存储监控
1、性能监控
- 正确性
- 时间开销
- 空间开销
2、ROM监控
除了某个存储模块的监控,我们也需要对应用整体的ROM空间做详细监控。ROM监控的两个核心指标是文件总大小与总文件数。
SQLite优化
1、基础知识
1)ORM
ORM(Object Relational Mapping)即关系对象映射,用面向对象的概念把数据库中表和对象关联起来,让我们可以不用关心数据库底层实现。Android中常见ORM框架有greenDAO和Google官方的Room,ORM框架使用简单,但是以牺牲部分执行效率为代价的。
2)进程与线程并发
并发问题会导致SQLiteDatabaseLockedException,SQLite并发有两个维度,一个多进程并发,另一个是多线程并发。
多进程并发
SQLite默认支持多进程并发,它通过文件锁来控制多进程的并发。SQLite锁的粒度并没有非常细,它针对的是整个DB文件,内部有5个状态。多进程可以同时获取SHARED锁来读取数据,但是只有一个进程可以获取EXCLUSIVE锁来写数据库,在EXCLUSIVE模式下,数据库连接在断开之前都不会释放SQLite文件的锁,从而避免不必要的冲突,提高数据库访问速度。
多线程并发
SQLite默认支持多洗那成并发模式,更多进程的锁机制一样,为了实现简单,SQLite锁的粒度都是文件级别,并没有实现表级甚至行级的锁。同一个句柄同一时间只有一个线程在操作,这个时候我们需要打开连接池Connection Pool。多线程可以同时读取数据库数据,但是写数据库依然是互斥的。SQLite提供了Busy Retry的方案,即发生阻塞时会触发Busy Retry,此时可以让线程休眠一段时间后,重新尝试操作。如果出现多个写并发的情况,依然有可能出现SQLiteDatabaseLockedException,这个时候应用可以捕获这个异常,然后等待一段时间后再尝试。
3)查询优化
索引优化
正确使用索引大部分情况下可以大大降低查询速度,索引的建立总体来说有一定的原则:
- 建立正确的索引, 选择最优的索引创建
- 单例索引、多列索引与符合索引的选择
索引要综合数据表中不同查询与排序语句一起考虑,如果查询结果集过大,还是希望通过复合索引直接在索引表返回查询结果。 - 索引字段的选择
整型类型索引效率会远高于字符串索引,而对于主键SQLite会默认帮我们建立索引,所以主键尽量不要用复杂字段。
页大小与缓存大小
对于SQLite的DB文件来说,页(page)是最小的存储单位,每个表对应的数据在整个DB文件都是通过一个一个页存储,属于同一个表不同的页以B树(B-tree)的方式组织索引,每一个表都是一个B树。
跟文件系统的页缓存一样,SQLite会将读过的页缓存起来,用来加快下一次读取速度。页大小默认1024Byte,缓存大小默认1000页。每个页永远只会存放一个表或一组索引的数据,即不可能同一个页存放多个表或索引数据。增大页的大小不能不断提升性能,在拐点之后会起反作用,建议选择4KB作为默认的page size以获得更好的性能。
其他优化
- 慎用select *
- 正确使用事务
-预编译与参数绑定,缓存被编译后的SQL语句 - 对于blob或超大的text列,可能会超出一个页的大小,导致出现超大页,建议将这些列单独拆表,或者放到表字段的后面
- 定期整理或清理无用或可删除的数据。
2、SQLite其他特性
1)损害与修复
2)加密与安全
数据库的安全主要有两个方面,一个是防注入,一个是加密。防注入可以通过静态安全扫描的方式,而加密一般会使用SQLClipher支持。
SQLite的加解密都是以页为单位,默认使用AES算法加密,加解密的耗时跟选用的密钥长度有关。
网友评论