深入理解Android中的SharedPreferences

2024-05-27 09:18

本文主要是介绍深入理解Android中的SharedPreferences,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

SharePreferences每个开发都一直在用,在用法上没什么好说的。但是今天看到一篇总结SharePreferences的文章,觉得总结的挺全的。一些平时大家不怎么关注的点都讲到了。

提出问题

SharedPreferences作为Android中数据存储方式的一种,我们经常会用到,它适合用来保存那些少量的数据,特别是键值对数据,比如配置信息,登录信息等。不过要想做到正确使用SharedPreferences,就需要弄清楚下面几个问题:
(1)每次调用getSharedPreferences时都会新建一个SharedPreferences对象吗?
(2)在UI线程中调用getXXX有可能导致ANR吗?
(3)为什么SharedPreferences只适合用来存放少量数据,SharedPreferences对应的就是普通的xml文件,为什么不能存放大量数据?
(4)commit和apply有什么区别?
(5)SharedPreferences每次写入时是增量写入吗?

源码分析

要想弄清楚上面几个问题,需要查看SharedPreferences的源码。先从Context的getSharedPreferences开始:

public SharedPreferences getSharedPreferences(String name, int mode) {return mBase.getSharedPreferences(name, mode);
}

我们知道Android中的Context类体系其实是使用了装饰者模式,而被装饰对象就这个mBase,它其实就是一个ContextImpl对象,看ContextImpl的getSharedPreferences方法:

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {SharedPreferencesImpl sp;synchronized (ContextImpl.class) {if (sSharedPrefs == null) {sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();}final String packageName = getPackageName();ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);if (packagePrefs == null) {packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();sSharedPrefs.put(packageName, packagePrefs);}// At least one application in the world actually passes in a null// name.  This happened to work because when we generated the file name// we would stringify it to "null.xml".  Nice.if (mPackageInfo.getApplicationInfo().targetSdkVersion <Build.VERSION_CODES.KITKAT) {if (name == null) {name = "null";}}sp = packagePrefs.get(name);if (sp == null) {File prefsFile = getSharedPrefsFile(name);sp = new SharedPreferencesImpl(prefsFile, mode);packagePrefs.put(name, sp);return sp;}}if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {// If somebody else (some other process) changed the prefs// file behind our back, we reload it.  This has been the// historical (if undocumented) behavior.sp.startReloadIfChangedUnexpectedly();}return sp;
}

可以看到这里使用到了单例模式,sSharedPrefs 是一个ArrayMap,packagePrefs也是一个ArrayMap,它们的关系是这样的:
packagePrefs存放文件name与SharedPreferencesImpl键值对,sSharedPrefs存放包名与ArrayMap键值对。注意sSharedPrefs是static变量,也就是一个类只有一个实例,因此你每次getSharedPreferences其实拿到的都是同一个SharedPreferences对象。
这里回答第一个问题,对于一个相同的SharedPreferences name,获取到的都是同一个SharedPreferences对象,它其实是SharedPreferencesImpl对象。

SharedPreferencesImpl构造方法:

SharedPreferencesImpl(File file, int mode) {mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;startLoadFromDisk();
}

与mBackupFile有关的等后面说,看startLoadFromDisk方法:

private void startLoadFromDisk() {synchronized (this) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {synchronized (SharedPreferencesImpl.this) {loadFromDiskLocked();}}}.start();
}

实际上是调用loadFromDiskLocked方法:

private void loadFromDiskLocked() {if (mLoaded) {return;}if (mBackupFile.exists()) {mFile.delete();mBackupFile.renameTo(mFile);}// Debuggingif (mFile.exists() && !mFile.canRead()) {Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");}Map map = null;StructStat stat = null;try {stat = Os.stat(mFile.getPath());if (mFile.canRead()) {BufferedInputStream str = null;try {str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);map = XmlUtils.readMapXml(str);} catch (XmlPullParserException e) {Log.w(TAG, "getSharedPreferences", e);} catch (FileNotFoundException e) {Log.w(TAG, "getSharedPreferences", e);} catch (IOException e) {Log.w(TAG, "getSharedPreferences", e);} finally {IoUtils.closeQuietly(str);}}} catch (ErrnoException e) {}mLoaded = true;if (map != null) {mMap = map;mStatTimestamp = stat.st_mtime;mStatSize = stat.st_size;} else {mMap = new HashMap<String, Object>();}notifyAll();
}

可以看到对于一个SharedPreferences文件name,第一次调用getSharedPreferences时会去创建一个SharedPreferencesImpl对象,它会开启一个子线程,然后去把指定的SharedPreferences文件中的键值对全部读取出来,存放在一个Map中。如果我们在UI线程中这样子写:

SharedPreferences sp = getSharedPreferences("test", Context.MODE_PRIVATE);
String name = sp.getString("name", null);

调用getString时那个SharedPreferencesImpl构造方法开启的子线程可能还没执行完(比如文件比较大时全部读取会比较久),这时getString当然还不能获取到相应的值,必须阻塞到那个子线程读取完为止,getString方法:

public String getString(String key, String defValue) {synchronized (this) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}
}

显然这个awaitLoadedLocked方法就是用来等this这个锁的,在loadFromDiskLocked方法的最后我们也可以看到它调用了notifyAll方法,这时如果getString之前阻塞了就会被唤醒。那么现在这里有一个问题,我们的getString是写在UI线程中,如果那个getString被阻塞太久了,这时就会出现ANR,因此要根据具体情况考虑是否需要把SharedPreferences的读写放在子线程中。
这里回答第二个问题,在UI线程中调用getXXX可能会导致ANR。同时可以回答第三个问题,SharedPreferences只能用来存放少量数据,如果一个SharedPreferences对应的xml文件很大的话,在初始化时会把这个文件的所有数据都加载到内存中,这样就会占用大量的内存,有时我们只是想读取某个xml文件中一个key的value,结果它把整个文件都加载进来了,显然如果必要的话这里需要进行相关优化处理。

SharedPreferences的getXXX的实现基本都是一样,这里就不逐个分析了。

SharedPreferences的初始化和读取比较简单,写操作就相对复杂了点,我们知道写一个SharedPreferences文件都是先要调用edit方法获取到一个Editor对象:

public Editor edit() {synchronized (this) {awaitLoadedLocked();}return new EditorImpl();
}

其实拿到的是一个EditorImpl对象,它是SharedPreferencesImpl的内部类:

public final class EditorImpl implements Editor {private final Map<String, Object> mModified = Maps.newHashMap();private boolean mClear = false;......
}

可以看到它有一个Map对象mModified,用来保存“脏数据”,也就是你每次put的时候其实是把那个键值对放到这个mModified 中,最后调用apply或者commit才会真正把数据写入文件中,比如看putString:

public Editor putString(String key, String value) {synchronized (this) {mModified.put(key, value);return this;}
}

其它putXXX代码基本也是一样的。EditorImpl类的关键就是apply和commit,不过它们有一些区别,先看commit方法:

public boolean commit() {MemoryCommitResult mcr = commitToMemory();SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);try {mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;}notifyListeners(mcr);return mcr.writeToDiskResult;
}

关键有两步,先调用commitToMemory,再调用enqueueDiskWrite,commitToMemory就是产生一个“合适”的MemoryCommitResult对象mcr,然后调用enqueueDiskWrite时需要把这个对象传进去,commitToMemory方法:

private MemoryCommitResult commitToMemory() {MemoryCommitResult mcr = new MemoryCommitResult();synchronized (SharedPreferencesImpl.this) {// We optimistically don't make a deep copy until// a memory commit comes in when we're already// writing to disk.if (mDiskWritesInFlight > 0) {// We can't modify our mMap as a currently// in-flight write owns it.  Clone it before// modifying it.// noinspection uncheckedmMap = new HashMap<String, Object>(mMap);}mcr.mapToWriteToDisk = mMap;mDiskWritesInFlight++;boolean hasListeners = mListeners.size() > 0;if (hasListeners) {mcr.keysModified = new ArrayList<String>();mcr.listeners =new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());}synchronized (this) {if (mClear) {if (!mMap.isEmpty()) {mcr.changesMade = true;mMap.clear();}mClear = false;}for (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue();// "this" is the magic value for a removal mutation. In addition,// setting a value to "null" for a given key is specified to be// equivalent to calling remove on that key.if (v == this || v == null) {if (!mMap.containsKey(k)) {continue;}mMap.remove(k);} else {if (mMap.containsKey(k)) {Object existingValue = mMap.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}}mMap.put(k, v);}mcr.changesMade = true;if (hasListeners) {mcr.keysModified.add(k);}}mModified.clear();}}return mcr;
}

这里需要弄清楚两个对象mMap和mModified,mMap是存放当前SharedPreferences文件中的键值对,而mModified是存放此时edit时put进去的键值对。mDiskWritesInFlight表示正在等待写的操作数量。可以看到这个方法中首先处理了clear标志,它调用的是mMap.clear(),然后再遍历mModified将新的键值对put进mMap,也就是说在一次commit事务中,如果同时put一些键值对和调用clear,那么clear掉的只是之前的键值对,这次put进去的键值对还是会被写入的。遍历mModified时,需要处理一个特殊情况,就是如果一个键值对的value是this(SharedPreferencesImpl)或者是null那么表示将此键值对删除,这个在remove方法中可以看到:

public Editor remove(String key) {synchronized (this) {mModified.put(key, this);return this;}
}

commit接下来就是调用enqueueDiskWrite方法:

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final Runnable writeToDiskRunnable = new Runnable() {public void run() {synchronized (mWritingToDiskLock) {writeToFile(mcr);}synchronized (SharedPreferencesImpl.this) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run();}}};final boolean isFromSyncCommit = (postWriteRunnable == null);// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) {boolean wasEmpty = false;synchronized (SharedPreferencesImpl.this) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

先定义一个Runnable,注意实现Runnable与继承Thread的区别,Runnable表示一个任务,不一定要在子线程中执行,一般优先考虑使用Runnable。这个Runnable中先调用writeToFile进行写操作,写操作需要先获得mWritingToDiskLock,也就是写锁。然后执行mDiskWritesInFlight–,表示正在等待写的操作减少1。最后判断postWriteRunnable是否为null,调用commit时它为null,而调用apply时它不为null。
Runnable定义完,就判断这次是commit还是apply,如果是commit,即isFromSyncCommit为true,而且有1个写操作需要执行,那么就调用writeToDiskRunnable.run(),注意这个调用是在当前线程中进行的。如果不是commit,那就是apply,这时调用QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable),这个QueuedWork类其实很简单,里面有一个SingleThreadExecutor,用于异步执行这个writeToDiskRunnable。
这里就可以回答第四个问题了,commit的写操作是在调用线程中执行的,而apply内部是用一个单线程的线程池实现的,因此写操作是在子线程中执行的。

说一下那个mBackupFile,SharedPreferences在写入时会先把之前的xml文件改成名成一个备份文件,然后再将要写入的数据写到一个新的文件中,如果这个过程执行成功的话,就会把备份文件删除。由此可见每次即使只是添加一个键值对,也会重新写入整个文件的数据,这也说明SharedPreferences只适合保存少量数据,文件太大会有性能问题。
这里回答第五个问题,SharedPreferences每次写入都是整个文件重新写入,不是增量写入。

SharedPreferences几种模式:

Context.MODE_PRIVATE:应用私有,只有相同的UID才能进行读写
Context.MODE_MULTI_PROCESS:多进程安全标志,Android2.3之前该标志是默认被设置的,Android2.3开始需要自己设置。
MODE_APPEND:首次创建时如果文件存在不会删除文件。
注意这些模式可以使用位与进行设置,比如MODE_PRIVATE | MODE_APPEND。

http://blog.csdn.net/u012619640/article/details/50940074

这篇关于深入理解Android中的SharedPreferences的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1007066

相关文章

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

【生成模型系列(初级)】嵌入(Embedding)方程——自然语言处理的数学灵魂【通俗理解】

【通俗理解】嵌入(Embedding)方程——自然语言处理的数学灵魂 关键词提炼 #嵌入方程 #自然语言处理 #词向量 #机器学习 #神经网络 #向量空间模型 #Siri #Google翻译 #AlexNet 第一节:嵌入方程的类比与核心概念【尽可能通俗】 嵌入方程可以被看作是自然语言处理中的“翻译机”,它将文本中的单词或短语转换成计算机能够理解的数学形式,即向量。 正如翻译机将一种语言

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

深入手撕链表

链表 分类概念单链表增尾插头插插入 删尾删头删删除 查完整实现带头不带头 双向链表初始化增尾插头插插入 删查完整代码 数组 分类 #mermaid-svg-qKD178fTiiaYeKjl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

深入理解RxJava:响应式编程的现代方式

在当今的软件开发世界中,异步编程和事件驱动的架构变得越来越重要。RxJava,作为响应式编程(Reactive Programming)的一个流行库,为Java和Android开发者提供了一种强大的方式来处理异步任务和事件流。本文将深入探讨RxJava的核心概念、优势以及如何在实际项目中应用它。 文章目录 💯 什么是RxJava?💯 响应式编程的优势💯 RxJava的核心概念