内容简介: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方法,我们看下这个方法。
其中包含一个mSharedPrefsPaths对象,它是ArrayMap类型,我们可以在App中创建多个sp文件,mSharedPrefsPaths中就是存储了不同sp文件名和sp文件的对应关系。这里的getSharedPreferencesPath方法实际上就是在磁盘上创建了一个xml文件。查看上图最后一行的getSharedPreferences方法。
我们看到这个方法实际返回了一个SharedPreferencesImpl对象,看下SharedPreferencesImpl的构造方法。
其中调用了startLoadFromDisk方法,startLoadFromDisk在子线程里调用了loadFromDisk,执行线程之前将mLoaded设置为false,再来看下这个loadFromDisk方法。
这个方法的代码很多,我们只看最核心的部分,它通过XmlUtils.readMapXml()将文件读取到mMap中,mMap是一个HashMap,并且将mLoaded设置为true,大家记住mLoaded这个变量,后面还会遇到它。也就是说,sp文件的内容被读取到内存并且缓存到mMap中了,后续对sp的操作都与内存中的缓存有关。既然sp文件的内容会缓存到内存中,如果文件中存储了大量数据,就会占用很大的内存空间,这点需要特别注意。
SharedPreferences的创建过程讲完了,下面我们来看一下put过程。put操作首先要调用edit()方法,
又见到了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的使用并不是那么简单的,使用不当可能会导致程序异常,我们对上面提到的一些问题进行一下总结:
-
sp配置不要全部都写在一个文件中,这样不仅第一次加载会很慢,也会占用大量内存。最好是根据一定规则分成多个sp文件。比如频繁和不频繁写入的配置就分别存储在两个不同的文件中。
-
sp文件的写入是全量写入,即使改了一条配置,写入的时候也会对整个文件进行操作,因此最好能批量操作,不要每次都commit。
-
启动的时候需要读取sp的配置最好异步进行,如果一定要同步读取,启动的sp文件要尽可能的小。
-
不要将太大的配置项(包括key和value)存储在sp中,否则会占用大量内存。
-
获取SharedPreferences对象的时候会读取sp文件,如果文件没有读取完,就执行了get和put操作,可能会出现需要等待的情况,因此最好提前获取SharedPreferences对象。
-
每次调用edit方法都会创建一个新的EditorImpl对象,不要频繁调用edit方法。
-
apply方法虽然是在线程中异步将配置写入文件,但是如果任务很多,而且每个任务执行时间很长,也可能会导致Activity或Service在stop的时候出现ANR。
欢迎关注我的微信公众号,收到最新的推送文章
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- vue项目实践004~~~一篮子的实践技巧
- HBase实践 | 阿里云HBase数据安全实践
- Spark 实践:物化视图在 SparkSQL 中的实践
- Spark实践|物化视图在 SparkSQL 中的实践
- HBase实践 | 数据人看Feed流-架构实践
- Kafka从上手到实践-实践真知:搭建Zookeeper集群
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Pattern Recognition and Machine Learning
Christopher Bishop / Springer / 2007-10-1 / USD 94.95
The dramatic growth in practical applications for machine learning over the last ten years has been accompanied by many important developments in the underlying algorithms and techniques. For example,......一起来看看 《Pattern Recognition and Machine Learning》 这本书的介绍吧!