定义
存储就是将特定的数据结构转化成可记录与还原的二进制格式。
Android存储方式
SharedPreferences
- 简介(自我介绍)
我是Android平台上的一个轻量级存储类,为什么说我是轻量级的那?因为我在创建的时候就会把我所知道的都加载进内存。你可能问了,我为什么在一开始就袒露无疑那?这个我们后面慢慢聊。
再来说一下我怎么存数据的吧?我用K-V(Key-Value)键值对的形式保存数据,数据都被我放在一个 xml 文件中,这个文件放在 data/data/
你在什么时候可能需要我那?例如:你想知道某一页面第一次展示给用户的时间,可以用我;你想知道某一功能的引导页是不是展示过,可以用我...
我比较懒,所以,我的初衷是存取一些简单的配置(int,long, boolean, string),如果要存取一些比较复杂的数据,最好将它们用压缩算法变成字符串给我。
如果想知道我比较详细的情况,就去看一下这篇文章吧。
- 性能优化
-
不适合存储size很大的数据
- 在创建时,会把整个文件的数据都加载到内存,有可能会阻塞主线程,造成界面卡顿;
- 使用XmlUtils将内容存储到内存(map对象)时,会短时间产生大量的临时对象,导致系统频繁GC(Stop The World),造成界面卡顿;
- 上步提到的内存对象,在整个运行中,都会存在,占用着大量的内存
- 每一步操作都是 synchronized,提供线程安全的时候,也影响着性能。
所以,如果要存取很大或者复杂的数据,要选用文件存储或者数据库存储,而不是简单轻量的SP
不适合存储特俗符号很多的数据
JSON或者HTML格式存在SP里面时,需要进行转义(对符号和标签进行理解),这样在解析时就需要特殊的处理,CPU的消耗比较大。-
多次edit多次commit/apply
前面提到的文章有说过,每次调用 edit 都会创建 Editor 对象进行写操作,每次 commit 都是在当前线程写入缓存并执行IO操作,所以多次 commit 会迅速占用内存,导致界面卡顿, 所以提供 apply 方法将IO操作放入子线程去执行,但是 apply是将IO操作加入一个叫 QueueWorke 的队列中,该队列只有一个线程工作,如果我们多次 apply,就有可能在应用想要退出时,而因为线程任务没有执行完,而退出不了。所以,我们应该合并多次 edit,一次 commit/apply。
SP不支持跨进程通信
首先SP虽然提供了[跨进程]功能的 Flag-MODE_MULTI_PROCESS,保证了在API 11以上的系统,如果SP已经被读进内存,再次获取这个SP的时候,如果该flag存在,会重读一遍文件。其实,这样的话,使用缓存减少读操作引起的IO操作的优化,就不起作用了,性能会更差。
所以,如果我们的数据需要支持跨进程,希望不要使用SP,而是使用支持跨进程的 ContentProvider,或者网络开源库 MMKV。
ContentProvider
- 简介(自我介绍)
我是四大组件之一哦,而且我的oncreate方法还在application的oncreate之前哦。我一般为存储和获取数据提供统一的接口,并且可以在不同的应用程序之间共享数据哦。因此,我本身就是跨进程的,只与我的跨进程是 Binder 提供的哦。
- 组成
- ContentProvider 内容提供者 (生产者)
- ContentResolver 内容接收者 (消费者)
- URI(统一资源标识符) CP提供的数据的唯一标识。(资源)
URI的格式
content://host/path/resId
组成 | 说明 |
---|---|
content:// | 协议(scheme),用来说明这是一个CP控制的数据 |
host | 同域名差不多,用于唯一标识这个CP,外部访问者可以根据它来找到CP。它在 authorities属性中说明,一般是定义该CP的包.CP类的名称 |
path | 数据库的名称 |
resid | 数据库中的某一条记录。如果URI中包含表示需要获取的记录ID,则返回对应的数据,如果没有,则返回全部数据 |
SQLite 数据库
- 简介
Android内置的事务性数据库管理系统,所以它遵循ACID(Atomicity原子性,Consistency一致性,Isolation隔离性,Durability持久性)四要素。存储在内部存储空间中,路径为 data/data/packename/databases/ 。
SQLite 每个数据都是以单个文件的形式存在,数据都是以 B-Tree 的数据结构形式存储在磁盘上。同时SQLite更改数据的时候默认一条语句就是一个事务,有多少条语句就有多少次磁盘操作。
SQLite的独享锁和共享锁机制描述:
SQLite 在写入数据库前,必须先读这个数据库,为了看它是否已经存在了。为了从数据库文件中读取,第一步是获取一个该数据库文件的共享锁(一个共享锁允许多个数据库连接,在同一时刻从这个数据库文件中读取信息),共享锁将不允许其他连接进行写操作。
在修改一个数据库之前,SQLite首先得获取数据库文件的"Reserved"锁。Reserved锁类似于共享锁,它们都允许其他连接读取数据库。单个 Reserved 锁可以与其他进程的多个共享锁一起协作,然后一个数据库文件只能存在一个 Reserved 锁,因此在某一时刻,只能有一个进程修改数据库文件,接着,将该锁升级成独享锁,一个独享锁允许其他获得共享锁的进程从数据库文件中继续读取数据,但是会阻止新的共享锁生成(为什么)。也就是说,独享锁会防止因大量的读操作而导致写操作不能执行的问题。
从SQLite本身的锁机制来看,它是多线程乃至多进程数据安全的,但是在并发写的时候还是会出现数据库锁住的异常。
ACID:Atomicity Consistency Isolation Durability
- 特点
- 轻量级
SQLite是进程内的数据库引擎,不存在数据库的服务端与客户端。size一般就几百KB。 - 不需要安装
SQLite是Android系统自带的数据库,不需要依赖。 - 单一文件
数据库中的所有信息都包含在一个文件内。 - 跨平台/可移植
除了主流的操作系统 Windows,Linux,它还支持其他的操作系统 - 弱类型字段
同意列中的数据可以是不同类型的。包括:NULL,CHAR,VARCHAR,INTEGER,TEXT,BLOB,DATA,REAL,TIME
- 使用
SQLiteOpenHelper 是 SQLiteDataBase 的一个辅助类,用于生成一个数据库,并对数据库的版本进行管理。
SQLiteOpenHelper 是一个抽象类,我们需要实现以下三个方法:
1. oncreate
在数据库第一次创建的时候被调用,一般我们在该方法生成数据库表
2. onUpdate
当数据库需要升级的时候被调用。一般我们在该方法删除旧表建立新表。
当在程序中调用这个类的方法 getWritableDatabase or getReadableDatabase 返回数据时,如果没有创建数据库,则会自动创建一个数据库。
在之后的调用中,使用SQLiteDataBase提供的增删改查API,对数据库进行操作。
- 数据库框架 GreenDao
- 简介
GreenDAO 是项目中常用的 ORM 框架,其原理是:在编译器生成业务需要的MOdel和DAO文件,业务中可以直接调用相应的DAO文件进行数据库操作,从而避免因反射带来的性能损耗和效率低下(市面上其他的ORM大都基于反射)。
GreenDao是一种Android数据库的ORM(object/relational mapping 对象关系映射)框架,与其他开源框架相比,性能较高,接口灵活,但是学习成本较高。
ORM:面向对象编程,把所有实体看成对象,关系型数据库则采用实体之间的关系(relation)连接数据。简单的说,ORM 就是通过实例对象的语法,完成关系型数据库的操作技术,是"对象-关系映射"(Object/Relational Mapping)的缩写。
数据库的表(table) --> 类(class)
表中的每条记录(record, 行数据) --> 对象(object)
记录中的字段(filed) --> 对象的一条属性(attribute)
优点:
1. 开发简单,将对象模型转化成SQL语句。只需要掌握API,不需要书写SQL语句
2. 面对一个复杂的程序时,大量的SQL语句,会让代码维护艰难;ORM让代码结构更清晰,维护更容易
缺点:
1. 上手比较困难,但后期很顺畅
2. 对于一些复杂的数据库操作,ORM语法更复杂
文件存储
文件存储就是将应用的数据以文件的形式存储在手机设备中。
- 内部存储
存在与内部存储下的文件,存储在与应用包名相同的目录下,默认只能被应用自己访问,外界无法访问。如果我们在创建内部存储文件的时候,将文件属性设置为可读,那么其他 app 可以通过跨进程的方式访问到;如果将文件属性设置为私有(private),那么文件只能被自己访问。
内部存储空间十分有限,另外系统本身和系统应用程序的许多数据也在内部存储空间中。我们在应用中使用的 SP 和 SQLite 存储方式存放的数据也是在内部存储中。
- 访问内部存储的API
API | 路径 | 说明 |
---|---|---|
context.getCacheDir() | /data/data/packageName/cache | 获取应用在内部存储中的 cache路径 |
context.getFilesDir() | /data/data/packageName/files | 获取应用在内部存储中的 files路径 |
context.getDir("video",MODE_PROVATE | /data/data/packageName/app_video | 获取应用在内部存储中的自定义文件夹路径 |
Environment.getDataDirectory() | /data | 获取内部存储的根路径 |
SharePreferences | /data/data/packageName/share_prefs | SP的存储路径 |
- 外部存储
存在与外部存储下的文件,可以被其他应用访问。
外部存储空间一般都比较大。由于其文件可以被外部应用访问,因此,隐私数据不要存在此空间内,如果要存,一定要使用加密算法MD5加密后再存入。
若使用外部存储,需要申请读写外部存储权限。
- 访问外部存储的API
API | 路径 | 说明 |
---|---|---|
context.getExternalFilesDir("") | /storage/emulated/0 or /sdcard/Android/data/packName/files | 获取应用在外部存储中的 files 路径 |
context.getExternalCacheDir("") | /storage/emulated/0 or /sdcard/Android/data/packName/cache | 获取应用在外部存储中的 cache 路径 |
Environment.getExternalStorageDirectory() | /storage/emulated/0 or /sdcard | 获取外部存储的根路径 |
Environment.getExternalStoragePublicDirectory("") | /storage/emulated/0 or /sdcard/ | 获取外部存储的根路径 |
- 奇怪的问题
我们在开发时,如果想测试自己的配置是否生效,常常使用 清理数据,而不是清理缓存,为什么那?
清理数据是清除我们应用的配置,如SP,和数据库,一旦清理,用户再次进入应用,需要重新再设置一遍。
清理缓存主要是清理应用运行期间的缓存,如网络下载的临时图片,从本地磁盘加载的临时数据。
网络存储
网络存储就是把数据放在服务器,而不选择放在本地。这种存储方式,需要根据自己的业务来选择。例如:用户名与密码,这种私密数据是在客户端发生的,但是,我们不能把它们放在本地,所以,我们通过加密并把数据传递给服务器存储。