Android上常见的数据存储方式有哪些呢?
SharedPreferences这种存储数据的方式我们平时用的都对吗?
怎么使用SQLiteDatabase才是安全的?
带着这些问题,我们今天来深入分析一下SharedPreferences和database这两种Android上常见的数据持久化方式。
一、SharedPreferences
1、Preference和sharedPreferences是什么
Preference在Android上是首选项的意思,主要是指FrameWork上的各种UI组件,我们看一下Preference的各个子类:
它们一般用在PreferenceActivity中,当使用这些组件时,设置在组件中的数据会自动进行保存。
说的更加直白一些,Preference就是应用的设置界面。
SharedPreferences是用来存取Preference中设置的数据的,它是key-value键值对的形式存在,Android 3.0后又增加了StringSet的value形式,可以说SharedPreferences就是用来为Preference做数据持久化的。我们也来看看官方对它的说明:
从这个官方说明里我们注意到我们平时容易忽略的两点:(1)对于任何一类的preference(实际就是同一个preference name),SharedPreferences是唯一的;(2)SharedPreferences不支持多进程(这个我们接下来也会分析到)。
2、SharedPreferenced的内部实现
对于怎么使用SharedPreferences,我们就不多讨论了,这是Android最基本的一种数据存储方式了,如果你还不知道如何使用它,那你要保持低调了,不要让人知道你是一个Android的程序员,同时赶紧去找资料学习一下吧。
(1)数据存储格式
SharedPreferences的数据是以xml格式存储的;它的存储位置在我们应用程序私有文件目录下的shared_prefs中,每个preference_name会存储一个xml文件;同时,这些数据都是明文存储的,担心数据泄漏的,记得加密后再写入哦。
具体的文件存储目录是:/data/data/${packageName}/shared_prefs/
我们看一下SharedPreferences的get方法接口:
从这里可以看出SharedPreferences只支持6种数据类型,分别是boolean,float,int,long,String和StringSet,基本StringSet还是在Android3.0后才加入。我们再来看看存储在xml中是什么样子:
可以看出,xml中的标签也是对应的几个。
(2)数据载入和缓存
SharedPreferences会在第一次打开这个Preference时,会启动一个新进程将整个文件中的内容输入到内存中,并进行缓存,以后再用到这个SharedPreferences时,都使用这一个实例。
我们看一下代码,SharedPreferences只是一个接口,它的真实代码在SharedPreferencesImpl.java中。
获取到SharedPreferences只有这一种方法,即使用PreferenceManager获取,最终也是通过这里,也只有这样,才能在context类中缓存已经载入的SharedPreferences。
第一个红框,我们看到,context中的静态成员sSharedPrefs用来缓存SP(以后用它来简写SharedPreferences),不过它的第一层key竟然是packageName,不知道是不是为了给插件留的兼容(纯属瞎猜的);第二个红框,我们看到,如果没有缓存,则会创建一个SP的实例SharedPreferencesImpl,并放入到缓存中。
启动新线程加载,可能主要是为了考虑它要读文件,但如果用到get方法或edit方法时,又是要等到新线程里把文件读完的,而我们平时使用时,很多时候拿到了SP,直接就要用的,或读或写,所以这个新线程起的不是特别必要,但毕竟给了我们预加载的一个SP的可能。
还要注意一点,SPImpl加载时,默认给的Buffer是16K,所以解析一个SP文件还是比较耗内存的。
还有一点,这里try catch中的异常,并不包括所有的异常,所以,如果这个pref文件出了其它问题,后果是很严重的,基本这个应用程序只能清除数据或重装了。
最后,读到的内容被放到了mMap中加以缓存,使用时可以直接从这个map中拿数据,所以,从SP的数据载入看,它为了提高性能,也是在内存中做了一个SP文件内容的映射。
(3)数据写入
SP的数据写入方式是,通过editor将要存储的数据同步或异步的写入内存,并将整个Pref的内容写回到文件中,我们从代码中一步一步来看:
先补一下SharedPreferencesImpl的源码,大家想看全部内容,可以直接访问:http://www.grepcode.com/file/repo1.maven.org/maven2/org.robolectric/android-all/5.0.0_r2-robolectric-1/android/app/SharedPreferencesImpl.java#SharedPreferencesImpl.%3Cinit%3E%28java.io.File%2Cint%29
想了想,还是把edit()的代码贴了出来,因为这里有两个点需要注意和思考,一是,获取到editor之前,必须要等数据加载完成,这个Android的程序员也想把它移除掉,我们觉得可以吗?二是,大家再思考一个问题,SP为什么要通过editor去操作数据呢?为什么不像get方法一样,在SP中加几个put方法直接就搞定了,使用起来还非常的方便?
第一个问题,我觉得可以,毕竟editor只是将要写入数据的一个缓存,等真正去commit时再确认数据是否已经加载完成也是可以的。
第二个问题,我的看法是,SP就是为Preference设计的,用户在设置首选项时,会选择多个后,点击“应用”按钮,让他们一块儿生效,这也符合首选项的操作习惯,而Editor也为了这种场景而存在;同时,Editor还有一个好处,可以攒一堆数据后批量的进行一次写入,提升效率。
Editor中put的数据,都临时存放在自己的成员变量mModified中。
Editor可以通过apply或commit提交数据,两者的区别是,apply是异步提交,不阻塞;commit是同步提交,会阻塞当前线程直到数据写入磁盘完成。注意,apply方法在Android 3.0及以后才支持。
apply方法中,目前我们可以忽略绿色框的那些内容,只看红色部分,commitToMemory()方法将editor数据写入内存,enqueueDiskWrite方法将写操作放入队列,由另外的线程去做写入,没有任何阻塞就返回了,这也就是apply没有阻塞的原因。
注意,绿色部分看起来没有什么用处,实际上这个地方隐藏了一个大坑,后面讲到“主线程对SP的依赖”时,会再讲到这一点。
我们再来看commit方法,它执行完成apply相同的两步后,开始进行wait,而这个writtenToDiskLatch实际上是一个countDownLatch,写完磁盘后会countDown,这时函数才能返回,所以,commit是阻塞的,尽量不要在主线程commit哦。
commitToMemory方法也是有不少看点的,首先,提交到内存后返回值是要携带不少信息的,所以这里返回一个对象MemoryCommitResult,以方便把这些信息带到write线程;其次,如果当前有其它editor也在等待写磁盘(mDiskWritesInFlight > 0,大家自己去源码中看这个标志变量的作用吧),则是要把SP中所有的键值对复制一份出来的,这也是一个数据同步的技巧,大家可以学习一下,但是,如果sp中键值对过多,这里复制一份出来是很占内存的,如果写线程阻塞严重,这里复制出来的份数更多,内存占用就更严重啦;三、后面写入内存时会做一定的判断,如果value==null,表示remove,如果value的值跟原值都一样,这个editor是不需要再执行写操作的,即changesMade=false,节省开销。
这里回想一下,我们在这个分析中提到了几个SP关于内存方面需要注意的点?
从后两个红框我们看以看出,postWriteRunnable为空,表示是commit过来的,此时会直接执行runnable.run,同步完成;如果非空,表示是apply方法过来的,此时会使用一个singleThreadExecutor这样的单线程池,顺次进行文件写入。
注意,这里的线程池是QueuedWork类中的,为什么不在SP内自己定义呢?为什么SP要跟QueuedWork搅来搅去呢?留着这个疑问吧,一会儿我们仍然在对"主线程的依赖"中揭晓。
最后再来看一下写入的过程吧,从代码可以看出,Android采取的是先备份,再写入的过程。如果本次写入失败,则删除写的内容,在下次加载时,会加载备份的文件,如果写入成功,则删除备份文件,不得不说,Android考虑的还是比较周全的。
(4)多进程操作SharedPreferences
从SP的官方说明看,SP是不支持多进程的,注意这里说的是进程,不是线程。但我们再看另外一个值
这是context中的一个Mode值,从它的说明看,它是为多进程而生,从deprecated看,Android又放弃了它,建议我们有多进程,还是自己用contentProvider来解决吧。有点凌乱,这个值到底还起作用吗?我们还是去看源码吧。
这个是方是SharedPreferencesImpl中唯一使用到mMode的地方,也就是我们从Context中获取SP时传入的mode,从使用来看,它只是设置Pref文件的读取权限,是私有,还是开放,跟Multi_process,没什么关系。
再看ContextImpl中获取SP时的代码,可以看出,这个mode的作用就是,标识Pref文件可能被其它进程改写了,你再重新reload一次得一下最新数据吧,同步的安全级太低了,难怪Android官方也不建议用了。
所以结论就是,SP不支持多进程。
3、主线程对SharedPreferenced的依赖
SP的apply提供了异步线程操作,那它不应该很安全吗,怎么又跟主线程扯上关系了呢?要弄清楚这个问题,我们还是先了解一下QueuedWork:
从ActivityThread的调用来看,它在处理一个Service启动时会调用到QueuedWork的waitToFinish方法,实际上,我们在ActivityThread代码中搜一下会发现,在pauseActivty,stopActivity中,都有对这个方法的调用,从而我们可以猜测,QueuedWork这个队列中存放的应该是一些关键事务,ActivityThread的looper中处理的很多事件要想继续进行,都必须等待这个队列中的关键事务执行完。
从waitToFinish方法中可以看出,它果然是把所有队列中的Runnable拿出来顺次执行一下,更加残酷的是,它还是同步的,可见QueuedWork队列中的任务有多么的关键了。
好,我们再回过头我们SP的Editor中的apply方法:
在apply时,Editor把commit完成的这个task放入到了QueuedWork队列中,在写入磁盘完成后才移除;从这里可以看出Android对SP的重视程度,SP不写完,Activity,Service的很多操作就必须要等待,而这些操作都在主线程,这也就是为什么apply仍然跟主线程有关系的原因了。
那为什么Android这么重视SP呢?我的猜想是,SP就是为Preference这个首选项而生的,首选项对全局的影响还是很大的,所以必须等待首选项存储完成。
4、sharedPreferences使用注意事项
SP中存放的是一个个的键值对,而我们平时的很多数据就是键值对,所以我们很自然的把它作为我们存储数据的默认方式,从上面的分析来看,这样做还是有不少弊端的,在讲这些弊端前,我们先看看Android官方对SP的使用建议:
注意“较小”,如果我们存的数据量大,就不要用它啦。
我们来说说SP的弊端吧:
(1)读写速度慢,使用xml格式存储,解析效率本来就低,平时修改任何一个key,都要重写整个文件。
(2)明文存储,太不安全啦。
(3)内存占用高,读写文件内存占用高;高频并发写入时,不断的复制,临时内存会飙升。
(4)如果SP文件损坏,我们无法捕获异常,可能造成应用频繁崩溃。
(5)最最关键的,它会影响到主线程,造成卡顿,甚至造成anr哦。
建议:如果我们有很多值需要保存,改数据库吧,效率会高很多。