SharedPreferences最佳实践

        Android开发中,我们经常会用到SharedPreferences,它是一种轻量的数据存储方式,通常用来存储一些简单的配置信息。看了网络上的一些文章,感觉都不是特别满意,因此希望能结合自己的经验和理解写一篇分析SharedPreferences的文章。本文不会讲解SharedPreferences的基本用法,而是会结合源码来分析SharedPreferences的工作原理,以及使用中存在的一些问题。

       通过这篇文章,你可以了解到:

SharedPreferences是怎么工作的

SharedPreferences使用中有哪些坑

怎么来避免SharedPreferences的那些问题

       首先,我们要搞清楚SharedPreferences的本质是什么。它的本质是基于xml文件存储的key-value键值对数据,其存储位置在/data/data/包名/shared_prefs目录下。由于它是存储在应用程序的私有目录下,外部是无法直接访问的。也就是说它实际上就是一个xml文件,和普通的xml没有本质区别,内容也和我们工程代码里的strings.xml文件的内容类似。

源码分析

       下面我们通过对源码的分析,讲解一下它的工作原理。先来看一下SharedPreferences的基本用法。

       SharedPreferences sp = context.getSharedPreferences(“file1”, Context.MODE_PRIVATE);

       sp.edit().putBoolean(“key1”, false).commit();

       sp.getBoolean(“key1”)

       那么我们就从getSharedPreferences()方法开始讲起,实际上Context最终调用的是ContextImpl中的getSharedPreferences方法,我们看下这个方法。

图1

       其中包含一个mSharedPrefsPaths对象,它是ArrayMap类型,我们可以在App中创建多个sp文件,mSharedPrefsPaths中就是存储了不同sp文件名和sp文件的对应关系。这里的getSharedPreferencesPath方法实际上就是在磁盘上创建了一个xml文件。查看上图最后一行的getSharedPreferences方法。

图2

       我们看到这个方法实际返回了一个SharedPreferencesImpl对象,看下SharedPreferencesImpl的构造方法。

图3
图4

       其中调用了startLoadFromDisk方法,startLoadFromDisk在子线程里调用了loadFromDisk,执行线程之前将mLoaded设置为false,再来看下这个loadFromDisk方法。

图5

       这个方法的代码很多,我们只看最核心的部分,它通过XmlUtils.readMapXml()将文件读取到mMap中,mMap是一个HashMap,并且将mLoaded设置为true,大家记住mLoaded这个变量,后面还会遇到它。也就是说,sp文件的内容被读取到内存并且缓存到mMap中了,后续对sp的操作都与内存中的缓存有关。既然sp文件的内容会缓存到内存中,如果文件中存储了大量数据,就会占用很大的内存空间,这点需要特别注意。

       SharedPreferences的创建过程讲完了,下面我们来看一下put过程。put操作首先要调用edit()方法,

图6

       又见到了mLoaded这个变量,我们回忆一下之前的逻辑,在开始开启线程读取sp文件到内存的时候,这个变量被置为false,等线程执行完会置为true,在上图的awaitLoadedLocked方法中,如果发现mLoaded为false,则调用wait方法,此时会阻塞当前线程,直到sp文件读取完成,才调用notifyAll()通知这里被阻塞的线程继续执行。也就是说,如果读取sp文件的操作执行时间很长的话,这里就可能会阻塞主线程导致ANR。

       怎么才能尽可能的避免这个问题呢?首先,我们需要将sp文件根据功能和特点分解为多个小文件,比如根据不同的功能模块进行划分,或者根据读写的频率,也可以根据是否App启动的时候就需要加载。如果每个文件足够小,那么在读取文件到内存的时候,耗时自然也就少了。尤其是在App启动的时候,只需要加载启动时需要的sp配置,可以一定程度上减少启动时间。

       下面继续看源码。edit()方法返回了一个Editor对象,实际的类型是EditorImpl。

       EditorImpl中包含一个HashMap类型成员mModified,调用Editor的方法如putString之后,都只是将数据存储在mModified中。这里只是数据的暂存区,因此如果忘记调用commit或者apply方法,数据其实并没有写入磁盘。有一点需要注意的是我们每次调用edit方法,都会创建一个mModified对象,因此,有必要减少edit方法的调用。

       最后,就是调用commit或者apply方法了。我们知道commit是同步写入,会返回执行结果;而apply方法是异步写入,并不会返回执行结果。下面通过源码来分析下它们的实现。

        commit方法中先后调用了commitToMemory和enqueueDiskWrite。commitToMemory方法的作用是将前面提到的mModified中缓存的数据更新到前面提到的mMap中,这个mMap会被最终写入文件。我们看enqueueDiskWrite方法,它的第二个参数传了null,因此,isFromSyncCommit为true,然后直接执行了writeToDiskRunnable.run()方法,其中通过调用writeToFile将mMap中的配置内容写入sp文件。

       从源码中我们可以看出,commit的执行是同步的,而且是全量的写入。如果不是必要的情况,尽量不要使用commit去保存sp的配置,以防止写文件阻塞主线程。

       我们再来看apply方法的实现有什么不同。

       这里所不同的是enqueueDiskWrite的第二个参数不为null,所以方法内部将写入文件的操作放入了单线程的线程池异步执行:QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable),由于是单线程,来不及执行的Runnable都被放在队列中等待执行 。writeToDiskRunnable里面执行了writeToFile将sp写入文件,然后调用了postWriteRunnable的run()方法,这里面又调用了awaitCommit的run()方法,最后调用了mcr.writtenToDiskLatch.await()。那么这个writtenToDiskLatch又有什么作用呢?通过代码,我们发现writeToFile方法里面最终会调用writtenToDiskLatch的countDown方法,也就是说,如果sp文件的写入一直没有执行完,writtenToDiskLatch.await()这个调用就会阻塞在这里,但从实际的执行时序上来看writtenToDiskLatch的countDown的调用又肯定是在await之前的,那么这个await的调用到底有什么作用呢?我们又注意到,这里有一行代码:QueuedWork.add(awaitCommit)。 我们看下这个QueuedWork是什么?

       图中略去了部分代码,add方法实际上就是将runnable加入到一个ConcurrentLinkedQueue中。下面的waitToFinish方法里会去遍历queue中的每个Runnable,并执行它的run方法。那么waitToFinish方法又是在哪里调用的呢?我们根据注释找到了ActivityThread类的handlePauseActivity、handleStopActivity方法,我们来看其中的一个。

       我们看到,在Activity调用onStop的时候,会调用QueuedWork.waitToFinish(),遍历执行其中的runnable。假设我们频繁的调用了apply方法,并紧接着调用了onStop,那么就可能会发生onStop一直等待QueuedWork.waitToFinish执行完成而产生ANR。也就是说,即使是调用了apply方法去异步提交,也不是完全安全的。如果apply方法使用不当,也许会遇到与下图类似的问题。

       上面讲了put操作,由于get操作相对简单一些,这里就不单独分析了。

总结

       从上面的分析我们发现SharedPreferences的使用并不是那么简单的,使用不当可能会导致程序异常,我们对上面提到的一些问题进行一下总结:

1.sp配置不要全部都写在一个文件中,这样不仅第一次加载会很慢,也会占用大量内存。最好是根据一定规则分成多个sp文件。比如频繁和不频繁写入的配置就分别存储在两个不同的文件中。

2.sp文件的写入是全量写入,即使改了一条配置,写入的时候也会对整个文件进行操作,因此最好能批量操作,不要每次都commit。

3.启动的时候需要读取sp的配置最好异步进行,如果一定要同步读取,启动的sp文件要尽可能的小。

4.不要将太大的配置项(包括key和value)存储在sp中,否则会占用大量内存。

5.获取SharedPreferences对象的时候会读取sp文件,如果文件没有读取完,就执行了get和put操作,可能会出现需要等待的情况,因此最好提前获取SharedPreferences对象。

6.每次调用edit方法都会创建一个新的EditorImpl对象,不要频繁调用edit方法。

7.apply方法虽然是在线程中异步将配置写入文件,但是如果任务很多,而且每个任务执行时间很长,也可能会导致Activity或Service在stop的时候出现ANR。

欢迎关注我的微信公众号,收到最新的推送文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容