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。

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

Android SharedPreferences最佳实践相关推荐

  1. Android开发最佳实践《IT蓝豹》

    Android开发最佳实践   移动开发Android经验分享应用GoogleMaterial Design 摘要:前 段时间,Google公布了Android开发最佳实践的一系列课程,涉及到一些平时 ...

  2. android 首页接口设计方案,Android开发最佳实践——1.接口设计

    Android开发最佳实践--1.接口设计 一个项目刚开始的时候,最需要确认的就是接口设计了:数据如何传递,使用什么格式什么协议乃至如何保证安全性.如果一个项目的接口设计不合理--比如没有考虑到安全性 ...

  3. [教程] Android PHP 最佳实践视频教程

    这几个月实在太忙了,一直没有时间关顾博客,不过好在日前花费了不少心血的视频教程<Android+PHP最佳实践>已经在华章教育和China-Pub上线了,在这里给大家简短的推荐一下吧:本系 ...

  4. Android开发最佳实践---Futurice之见

    原文链接:https://github.com/futurice/android-best-practices 本文是Futurice公司的Android开发人员总结的最佳实践,遵循这些准则可以避免重 ...

  5. Android开发最佳实践

    原文链接:https://github.com/futurice/android-best-practices 转载来源:http://blog.csdn.net/asce1885/article/d ...

  6. Android UI最佳实践

    为多屏设计(一) - 支持多个屏幕尺寸 参考地址:http://developer.android.com/training/multiscreen/index.html  Android UI设计提 ...

  7. Android SharedPreference最佳实践

    Android提供多种方式保存应用数据,其中一种方式是SharedPreferences,使用键值对保存私有基本的数据.所有的逻辑仅基于以下三个类: SharedPreferences SharedP ...

  8. 安卓软件开发需要学什么软件?Android开发最佳实践手册全网独一份,面试必问

    缘起 经过近十年的发展,Android技术优化日新月异,如今Android 10.0 已经发布,Android系统性能也已经非常流畅,可以在体验上完全媲美iOS.到了各大厂商手里,改源码.自定义系统, ...

  9. 膜拜大牛!Android开发最佳实践手册全网独一份,终获offer

    前言 首先介绍一下自己,计算机水本,考研与我无缘.之前在帝都某公司算法部实习,公司算大公司吧,然而个人爱好偏开发,大二的时候写个一个app,主要是用各种框架. 学习路径:如何循序渐进.阶段性的学习An ...

最新文章

  1. Radio Button详细用法
  2. How to Export Mailboxes with Exchange Server 2010 SP1(带状态查看,清除请求,归档备份)...
  3. python中如何打开csv文件_在Python中从CSV文件读取数据
  4. list排序_「肘后备急码」- C#对象List排序
  5. [你必须知道的.NET]第三十二回,,深入.NET 4.0之,Tuple一二
  6. 华为申请鸿蒙系统邮箱,华为鸿蒙系统
  7. 东芝确定半导体重组计划:保留闪存其他全卖
  8. zookeeper在linux环境安装
  9. http协议编程java_Java与Http协议的详细介绍
  10. Android基于讯飞语音SDK实现语音识别
  11. qt将tablewidget导出为excel
  12. RequestBody Ajax写法
  13. cad缩放_CAD两种缩放技巧,你喜欢那个?
  14. Nginx搭建反向代理服务器
  15. mysql rrd_时间序列数据库rrd启动
  16. NeHe OpenGL系列教程(中英文版附带VC++源码)中英文系列
  17. 基于Stm32的WiFi多功能LED
  18. 订单漏单问题分析及解决方案
  19. 劝说语合理使用计算机,现代汉语语法部分练习,带答案
  20. 探究下疯狂动物城Zootopia为何如此高分的原因

热门文章

  1. 探寻完美 之 JavaScript继承
  2. 浅谈C++ STL中的优先队列(priority_queue)
  3. Extensible Firmware Interface
  4. MySQL介绍及安装(一)
  5. iOS AFN监听网络状态
  6. Object-C---gt;Swift之(一)元组(tuple)类型
  7. RabbitMq(十三)消息发送确认与回调机制
  8. Elasticsearch(一) ES之简介、倒排索引介绍以及 elasticsearch、kibana安装
  9. 威纶通定时循环操作宏_威纶通触摸屏宏指令的使用
  10. 微信小程序提醒并延迟跳转