牛笔了!Android性能优化之SharedPreferences(1)

数据序列化系列(待更)

《Android 数据序列化之 JSON》

《Android 数据序列化之 Protocol Buffer 使用》

《Android 数据序列化之 Protocol Buffer 源码分析》

SQLite 存储系列

Android 存储选项之 SQLiteDatabase 创建过程源码分析

Android 存储选项之 SQLiteDatabase 源码分析

数据库连接池 SQLiteConnectionPool 源码分析

SQLiteDatabase 启用事务源码分析

SQLite 数据库 WAL 模式工作原理简介

SQLite 数据库锁机制与事务简介

SQLite 数据库优化那些事儿


前言

本文不是与大家一起探讨SharedPreferences的基本使用,而是结合源码的角度揭秘对SharedPreference使用不当引发的严重后果以及该如何正确使用。

SharedPreferences是Android平台上一个轻量级的存储辅助类,用来保存应用的一些常用配置,它提供了string,set,int,long,float,boolean六种数据类型。最终数据是以xml形式进行存储。在应用中通常做一些简单数据的持久化缓存。SharedPreferences作为一个轻量级存储,所以就限制了它的使用场景,如果对它使用不当将会带来严重的后果。

一、从源码的角度出发

1、SharedPreferences的创建过程

后面统一简称:Sp

通过Context的getSharedPreferences方法得到Sp对象。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里实际调用了ContextImpl的getSharedPreferences()。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从源码可以看到:首先在sSharedPrefs中获取Sp对象,那这个sSharedPrefs是个什么东西?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

sSharedPrefs实际是个Map对象,并且被声明为static final,这就意味着我们整个应用中只存在一个sSharedPrefs对象。如果第一次创建Sp对象此时肯定是获取到的是null,紧接着进入第一个if语句getSharedPrefsFile(name),参数想必大家都猜的到:就是我们创建Sp时传的的name,其实通过名字也可以看得出根据传递name创建一个File:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建name.xml文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

跟踪到这里储存文件的创建我们就找到了。

紧接着new SharedPreferencesImpl(),看下SharedPreferencesImpl的构造方法:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实际上SharedPreferences只是个接口,而真正的实现是SharedPreferencesImpl,我们后续的get,put操作实际也是通过SharedPreferencesImpl对象完成的。

构造方法最后一行:startLoadFromDisk():

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从这里可以看出首先将mLoaded变量赋值为false,起到一个状态的变化作用,在后续我们会说到这个mLoaded变量很重要(其实主要多线程等待),然后开启一个线程loadFromDiskLocked():

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码稍微有点长,但是并不复杂。94行 - 105行都是做一些相关的检查。紧接着向下创建BufferedInputStream对象,将mFile作为参数,mFile还记得吗?它就是根据我们传递的name创建的文件。然后通过XmlUtils.readMapXml()将文件内容写入到map中并返回。在123行将mLoaded设置为true,代表已经将文件里的加载完成,存储在一个map中并且将其赋值给成员变量mMap:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

说道这想必大家已经明白:我们在Sp储存的数据会在本地生成一个.xml文件外,还会将该文件的数据缓存在一个map对象中。如果是第一次创建显然BufferedInputStream不会读取到任何数据,此时XmlUtils.readMapXml()解析返回自然为null,然后mMap = new HashMap();

然后再回到ContextImpl的getSharedPreferences方法最后:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果Sp已经存在了,会判断mode否等于Context.MODE_MULTI_PROCESS,然后如果API小于11:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

没错Context.MODE_MULTI_PROCESS仅仅是重新加载一遍数据到内存mMap,所以指望SharedPreferences实现跨进程通信可以死心了。

说到这,SharedPreference的创建过程就算是讲完了:getSharedPreferences实际返回SharedPrefenercesImpl对象,首先在sSharedPrefs容器中查找,如果未找到则创建Sp的对象并添加到sSharedPrefs。

2、put数据

通过上面的分析getSharedPreferences实际创建的是SharedPreferencesImpl对象。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

此时edit自然是调用的SharedPreferencesImpl的方法:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

还记得我们之前提到的mLoaded变量吗:当我们第一次创建SharedPreferences时候,会将该变量置为false,然后开启线程将文件中的数据完成读取进map之后再将其置为true,读取文件的内容到map是在工作线程,此时edit方法是在主线程,如果此时工作线程读取时间过久,那edit方法将长时间处于等待状态。一旦超过5秒就会发生ANR危险

调用SharedPreferencesImpl的edit方法返回的是EditorImpl对象:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们一些列的put操作,还有clear,remove,apply,commit都是在EditorImpl对象中:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从源码可以得知,我们一些列的put和remove之后是将数据添加进入mModifiled中,mModifiled是一个Map对象,其实从名字也可以看出代表为暂存的。clear仅修改mClear状态。执行操作之后必须要执行commit:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里需要注意的是:我们每次edit都会创建一个新的EditorImpl对象。接着跟踪commit操作:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

commitToMemory():

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下面:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码篇幅有些长,我们只关注重点部分:for循环这里,上面我们提到一系列的put和remove操作都添加进入mModified中,也就是mModified保留着我们当前的改变,通过遍历该容器,与mMap数据做一个比较,比如相同key但是value发生了变化此时修改mMap中的数据。然后mMap就是最后一次commit的数据。最后清空mModified容器。

方法的最后返回MemoryCommitResult,其实从名字也可以看出它的作用:标记本次提交的状态是否发生改变并将结果返回。

此时又回到commit方法:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

调用enqueueDiskWirte:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先writeToDiskRunnable对象,在该对象的方法中执行写入文件操作(就是将最后一次提交之后mMap的数据写回到文件)。

接着向下:

由于commit方法的第二个参数Runnable传递null,故此时siFromSyncCommit为true,可以看到执行writeToDiskRunnable.run,直接在当前线程(UI线程)执行写入文件操作。此时return。

我们在修改数据之后除了选择commit提交之外,还可以使用apply进行提交:首先writeToDiskRunnable对象,在该对象的方法中执行写入文件操作(就是将最后一次提交之后mMap的数据写回到文件)。

使用apply进行提交:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

此时siFromSyncCommit等于false,此时会执行enqueueDiskWrite方法的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

QueuedWork是一个线程池,而且只有一个核心线程,提交的任务到会加入到一个等待队列中按照顺序执行。

那么commit发生在UI线程而apply发生在工作线程。如果保证不阻塞UI线程我们使用apply来提交修改是否就绝对安全了呢?这里先告诉大家答案:绝对不是!!!!,后面会给大家继续分析。

接下来我们先来看下get操作。

3、get数据

我们看get操作做了哪些:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

也就是SharedPreferencesImpl的get操作:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其实通过上面的分析我们已经得到答案:通过SharedPreferenceImpl存储的数据都会在内存中保留一份mMap,这里也是直接在mMap中读取数据即可。

这里要着重说下awaitLoadedLocked方法,之前我们也提到过该方法主要是检查mLoaded变量状态:当我们第一次创建Sp对象时,它会开启一个工作线程将指定的文件中内容加载到mMap中,当加载完成改变mLoaed变量状态;否则awaitLoadedLocked方法会一直等待下去。这里涉及到一个优化点我们后续给大家总结。

二、apply一定安全吗?

上面我们提到过确认提交数据除了commit还可以apply,apply使写入文件操作发生在工作线程中,这样防止IO操作阻塞UI线程;这样真的就绝对安全吗?答案不是的。

我们要去跟踪另外一部分源码:

首先Android四大组件的创建以及生命周期调用都是进程间通信完成的,到我们自己的进程中完成调度过渡任务的是ActivityThread,ActivityThread是我们应用进程的入口。来看下Actvity的onStop回调过程:

ActivityThread.java:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

你没有看错又要等待,等待什么呢?

还记得我们确认提交数据使用apply操作将写入文件操作添加进线程池队列中吗?sPendingWorkFinishers就是SharedPreferencesImpl的enqueueDiskWirte方法的最后一行,当我们使用apply时就会执行如下添加到线程池中任务队列

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

面试复习笔记

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960页Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。


《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

[外链图片转存中…(img-hsv0Ear9-1715342096885)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值