Android SharedPreferences最佳实践

栏目: IOS · Android · 发布时间: 5年前

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

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

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

  • SharedPreferences是怎么工作的

  • SharedPreferences使用中有哪些坑

  • 怎么来避免SharedPreferences的那些问题

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

源码分析

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

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

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

sp.getBoolean(“key1”)

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

Android SharedPreferences最佳实践

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

Android SharedPreferences最佳实践

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

Android SharedPreferences最佳实践

Android SharedPreferences最佳实践

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

Android SharedPreferences最佳实践

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

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

Android SharedPreferences最佳实践

Android SharedPreferences最佳实践

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

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

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

Android SharedPreferences最佳实践

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

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

Android SharedPreferences最佳实践

Android SharedPreferences最佳实践

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

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

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

Android SharedPreferences最佳实践

这里所不同的是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是什么?

Android SharedPreferences最佳实践

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

Android SharedPreferences最佳实践

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

Android SharedPreferences最佳实践

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

总结

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

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

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

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

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

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

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

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

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

Android SharedPreferences最佳实践


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

《裂变:秒懂人工智能的基础课》

《裂变:秒懂人工智能的基础课》

王天一 / 电子工业出版社·博文视点 / 2018-6-13 / 59.00元

人工智能是指通过普通计算机程序实现的人类智能技术,这一学科不仅具有非凡的科学意义,对人类自身生存方式的影响也在不断加深。本书作为人工智能领域的入门读物,内容围绕人工智能的核心框架展开,具体包括数学基础知识、机器学习算法、人工神经网络原理、深度学习方法与实例、深度学习之外的人工智能和实践应用场景等模块。本书力图为人工智能初学者提供关于这一领域的全面认识,也为进一步的深入研究建立坚实的基础。一起来看看 《《裂变:秒懂人工智能的基础课》》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器