赞
踩
SharedPreferences 线程安全
以 ArrayMap<File, SharedPreferencesImpl> 缓存在内存,多次调用 getSharedPreferences 不影响性能
SharedPreferences 开启一个线程,异步加载文件数据
get 操作线程安全,getXxx方法是直接操作内存,从内存的 mMap中,根据传入的key读取value
文件加载完之前,调用 get 会阻塞在 awaitLoadedLocked() 方法,阻塞线程
put 操作线程安全,基于 Editor 类
文件加载完之前,调用 put 会在 edit() 方法中调用 awaitLoadedLocked() 方法,阻塞线程
Editor editor = preferences.edit()
editor.put()
put / remove / clear 等写操作,先记录在 mModified 中,commit / apply 调用后
将所有 mModified 同步到 mMap(成功后清空 mModified) 以及 文件
如果有执行 clear()方法,会清空mMap,不会清空 mModified
清空 mMap后,会遍历 mModified,将其中写记录同步到 mMap
sharedPreferences.edit()
.putString("key1", "value1")
.clear()
.commit();
这种写法,会先清空 mMap,然后继续把 key1,value,写入mMap和磁盘
用来存储一些 比较小的键值对集合,
最终会在手机的 /data/data/package_name/shared_prefs/目录下生成一个 xml 文件存储数据
通过 ContextImpl.getSharedPreferences 方法能够获取SharedPreferences对象
通过 getXxx/putXxx 方法能够进行读写操作
commit 方法(只有一个commit操作时同步写入文件,超过一个commit时,异步写入文件)
apply 异步写文件
把写入文件的任务,放入一个单线程的线程池的任务队列
异步写入文件的任务,会被添加在一个全局队列中,
在Activity#onStop执行之前,会阻塞onStop,等待任务执行完。
所以异步 apply 太多,会阻塞 UI线程,导致Activity泄露
SP 性能与文件大小相关,不同业务类型数据,拆分到不到文件保存
第一次 getSharedPreferences,会加载 SP 文件进内存,过大的 SP 文件会导致阻塞,甚至会导致 ANR
每次 apply、commit,都会把全部的数据一次性写入磁盘
SP 跨进程,只是在 getSharedPreference() 是,判断文件是否修改,有改动则重新加载文件。
数据不同步
9、apply太多会阻塞 Activity#onStop()
ActivityThread # handleStopActivity() 中 QueuedWork.waitToFinish(),
API 11以上,会阻塞 onStop,等待 Sp写入文件任务完成
class ContextImpl extends Context { @Override public SharedPreferences getSharedPreferences(String name, int mode) { if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; //4.4以下,默认文件名 } } File file; synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths.get(name); if (file == null) { // 创建一个对应路径 /data/data/packageName/name 的 File 对象 file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } // 这里调用了 getSharedPreferences(File file, int mode) 方法 return getSharedPreferences(file, mode); } @Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; // 这里使用了 synchronized 关键字,确保了 SharedPreferences 对象的构造是线程安全的 synchronized (ContextImpl.class) { // 获取SharedPreferences 对象的缓存,并复制给 cache final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 以参数 file 作为 key,获取缓存对象 sp = cache.get(file); if (sp == null) {// 如果缓存中不存在 SharedPreferences 对象 checkMode(mode); if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) { if (isCredentialProtectedStorage() && !getSystemService(UserManager.class) .isUserUnlockingOrUnlocked(UserHandle.myUserId())) { throw new IllegalStateException("SharedPreferences in credential encrypted " + "storage are not available until after user is unlocked"); } } // 构造一个 SharedPreferencesImpl 对象 sp = new SharedPreferencesImpl(file, mode); // 放入缓存 cache 中,方便下次直接从缓存中获取 cache.put(file, sp); // 返回新构造的 SharedPreferencesImpl 对象 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. // 如果由其他进程修改了这个 SharedPreferences 文件,我们将会重新加载它 sp.startReloadIfChangedUnexpectedly(); } // 程序走到这里,说明命中了缓存,SharedPreferences 已经创建,直接返回 return sp; } }
1)缓存未命中, 才构造SharedPreferences对象,多次调用 getSharedPreferences方法,不影响性能,
因为有缓存机制
2)SharedPreferences对象的 创建过程是线程安全的,因为使用了synchronized 关键字
3)如果命中了缓存,并且参数mode 使用 Context.MODE_MULTI_PROCESS,
会调用sp.startReloadIfChangedUnexpectedly()方法。
在 startReloadIfChangedUnexpectedly 方法中,会判断是否由其他进程修改过这个文件,如果有,会重新从磁盘中读取文件加载数据。
final class SharedPreferencesImpl implements SharedPreferences { SharedPreferencesImpl(File file, int mode) { mFile = file; //创建灾备文件,命名为prefsFile.getPath() + ".bak" mBackupFile = makeBackupFile(file); mMode = mode; //mLoaded代表是否已经加载完数据 mLoaded = false; //解析 xml 文件得到的键值对就存放在mMap中 mMap = null; //顾名思义,这个方法用于加载 mFile 这个磁盘上的 xml 文件 startLoadFromDisk(); } //创建灾备文件,用于当用户写入失败的时候恢复数据 private static File makeBackupFile(File prefsFile) { return new File(prefsFile.getPath() + ".bak"); } //加载mFile private void startLoadFromDisk() { synchronized (this) { mLoaded = false; } //【注意:这里我们可以看出,SharedPreferences 是通过开启一个线程来异步加载数据的】 new Thread("SharedPreferencesImpl-load") { public void run() { //这个方法才是真正负责从磁盘上读取 xml 文件数据 loadFromDisk(); } }.start(); } private void loadFromDisk() { synchronized (SharedPreferencesImpl.this) { //如果正在加载数据,直接返回 if (mLoaded) { return; } //如果备份文件存在,删除原文件,把备份文件重命名为原文件的名字 //我们称这种行为叫做回滚 if (mBackupFile.exists()) { //删除原来的文件 mFile.delete(); //把备份文件全路径改成 mFile的全路径,此时mFile读取的就是备份文件的了 mBackupFile.renameTo(mFile); } } // Debugging if (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 { //读取数据并且将数据解析为Map str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024); map = XmlUtils.readMapXml(str); } catch (XmlPullParserException | IOException e) { Log.w(TAG, "getSharedPreferences", e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { } synchronized (SharedPreferencesImpl.this) { //加载数据成功,设置 mLoaded 为 true mLoaded = true; if (map != null) { //将解析得到的键值对数据赋值给 mMap mMap = map; //将文件的修改时间戳保存到 mStatTimestamp 中 mStatTimestamp = stat.st_mtime; //将文件的大小保存到 mStatSize 中 mStatSize = stat.st_size; } else { mMap = new HashMap<>(); } //通知唤醒所有等待的线程 notifyAll(); } } }
1)将传进来的参数file以及mode分别保存在mFile以及mMode中
2)创建一个.bak 备份文件,写入失败时进行恢复工作
3)将存放键值对的mMap初始化为null
4)调用 startLoadFromDisk()方法加载数据
5)如果有备份文件,直接使用备份文件进行回滚
(删除原文件,把备份文件重命名为原文件的名字/用备份文件覆盖原文件)
6)第一次调用getSharedPreferences方法的时候,会从磁盘中加载数据,
数据的加载是通过【开启一个子线程调用loadFromDisk方法进行异步读取的】
7)将解析得到的键值对数据保存在mMap中
8)将文件的修改时间戳以及大小分别保存在mStatTimestamp以及mStatSize中
(保存这两个值有什么用呢?我们在分析getSharedPreferences方法时说过,如果有其他进程修改了文件,
并且mode为MODE_MULTI_PROCESS,将会判断重新加载文件。
如何判断文件是否被其他进程修改过,没错,根据文件修改时间以及文件大小即可知道)
9)调用notifyAll()方法通知唤醒其他等待线程,数据已经加载完毕
final class SharedPreferencesImpl implements SharedPreferences { public String getString(String key, @Nullable String defValue) { //synchronized 关键字用于保证 getString 方法是线程安全的 synchronized (this) { //方法 awaitLoadedLocked() 用于确保加载完数据并保存到 mMap 中才进行数据读取 awaitLoadedLocked(); //根据 key 从 mMap中获取 value String v = (String)mMap.get(key); //如果 value 不为 null,返回 value,如果为 null,返回默认值 return v != null ? v : defValue; } } private void awaitLoadedLocked() { if (!mLoaded) { BlockGuard.getThreadPolicy().onReadFromDisk(); } //前面我们说过,mLoaded 代表数据是否已经加载完毕 while (!mLoaded) { try { //等待数据加载完成之后才返回继续执行代码 wait(); } catch (InterruptedException unused) { } } } }
1)getXxx方法是线程安全的,因为使用了synchronize关键字
2)getXxx方法是直接操作内存的,直接从内存中的mMap中根据传入的key读取value
3)getXxx方法有可能会卡在 awaitLoadedLocked() 方法,从而导致线程阻塞等待
(第一次调用getSharedPreferences方法时,会创建一个线程去异步加载和解析xml文件,
加载完成后把 mLoaded 设为true。加载完成之前调用 getXXX() 会阻塞调用线程。
异步加载xml完成时,会调用 notifyAll来唤醒所有等待线程。)
public void put(String key, Object value) {
...
SharedPreferences.Editor editor = preferences.edit();
editor.putString(key, String.valueOf(value));
editor.putBoolean(key, Boolean.parseBoolean(String.valueOf(value)));
editor.putFloat(key, Float.parseFloat(String.valueOf(value)));
editor.putInt(key, Integer.parseInt(String.valueOf(value)));
editor.putLong(key, Long.parseLong(String.valueOf(value)));
editor.apply();
...
}
put 操作,基于 Editor 类,它的实现类是 EditorImpl
final class SharedPreferencesImpl implements SharedPreferences { @Override public Editor edit() { synchronized (mLock) { //这里会阻塞线程,要等待 加载xml文件的线程完成加载后才能往下执行的 awaitLoadedLocked(); } //返回了一个 EditorImpl 对象 return new EditorImpl(); } } public final class EditorImpl implements Editor { /** * putXxx/remove/clear等写操作方法都不是直接操作 mMap 的, * 而是将所有的写操作先记录在 mModified 中, * 等到 commit/apply 方法被调用, * 才会将所有写操作同步到内存中的 mMap 以及磁盘中 */ private final Map<String, Object> mModified = Maps.newHashMap(); private boolean mClear = false; public Editor putString(String key, @Nullable String value) { synchronized (this) { mModified.put(key, value); return this; } } ... public Editor remove(String key) { synchronized (this) { mModified.put(key, this); return this; } } ... }
public final class EditorImpl implements Editor { public boolean commit() { /* 前面我们分析 put 的时候说过,写操作的记录是存放在 mModified 中的。 * commitToMemory() 方法就负责将 mModified 保存的写记录同步到内存中的 mMap 中 * 将 mModified 同步到 mMap 之后,清空 mModified 历史记录 * 并且返回一个 MemoryCommitResult 对象 */ MemoryCommitResult mcr = commitToMemory(); // enqueueDiskWrite 方法负责将数据落地到磁盘上 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; } }
1)调用 commitToMemory() 将 mModified 同步到 mMap 中,并清空 mModified
2)调用 enqueueDiskWrite() 将数据写入文件
3)同步等待写磁盘操作完成(commit() 方法会同步阻塞等待)
4)通知监听者(可以通过registerOnSharedPreferenceChangeListener方法注册监听)
5)返回执行结果:true / false
public final class EditorImpl implements Editor { /** * 内存数据同步 * 把 EditorImpl.mModified 中数据,同步到 SharedPreferences.mMap */ private MemoryCommitResult commitToMemory() { MemoryCommitResult mcr = new MemoryCommitResult(); synchronized (SharedPreferencesImpl.this) { if (mDiskWritesInFlight > 0) { mMap = new HashMap<String, Object>(mMap); } //将 mMap 赋值给 mcr.mapToWriteToDisk, // mcr.mapToWriteToDisk 指向的就是最终写入磁盘的数据 mcr.mapToWriteToDisk = mMap; //mDiskWritesInFlight 代表的是“此时需要将数据写入磁盘,但还未处理或未处理完成的次数” //将 mDiskWritesInFlight 自增1(这里是唯一会增加 mDiskWritesInFlight 的地方) mDiskWritesInFlight++; ... synchronized (this) { //只有调用clear()方法,mClear才为 true if (mClear) { if (!mMap.isEmpty()) { mcr.changesMade = true; //当 mClear 为 true,清空 mMap mMap.clear(); } mClear = false; } // 遍历 mModified for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); // 获取 key Object v = e.getValue(); // 获取 value //当 value 的值是 "this" 或者 null,将对应 key 的键值对数据从 mMap 中移除 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; ... } //将 mModified 同步到 mMap 之后,清空 mModified 历史记录 mModified.clear(); } } return mcr; } }
1)mDiskWritesInFlight自增1
(mDiskWritesInFlight 表示 等待执行写入操作的次数
只有 commitToMemory() 对 mDiskWritesInFlight 进行增加, 其他地方都是减)
2)将mcr.mapToWriteToDisk 指向mMap,mcr.mapToWriteToDisk 就是最终需要写入磁盘的数据
3)判断 mClear的值,如果是true,清空mMap(调用clear()方法,会设置mClear为true)
4)同步 mModified 数据到 mMap 中,然后 清空mModified
5)最后返回一个 MemoryCommitResult 对象,这个对象的 mapToWriteToDisk 参数指向了最终需要写入磁盘的mMap
6)需要注意的是,
commitToMemory()方法中,当mClear为true,会清空mMap,但不会清空mModified
所以依然会遍历mModified,将其中保存的写记录同步到mMap中。
所以下面这种写法是错误的:
sharedPreferences.edit()
.putString("key1", "value1") // key1 不会被 clear 掉,commit 之后依旧会被写入磁盘中
.clear()//只会清空 SharedPreferences.mMap,然后继续把 mModified 同步到 清空的 mMap里
.commit();
final class SharedPreferencesImpl implements SharedPreferences { private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { //创建一个 Runnable 对象,该对象负责写磁盘操作 final Runnable writeToDiskRunnable = new Runnable() { @Override public void run() { synchronized (mWritingToDiskLock) { //最终通过文件操作将数据写入磁盘的方法了 writeToFile(mcr); } synchronized (SharedPreferencesImpl.this) { //写入磁盘后,将 mDiskWritesInFlight 自减1,代表写磁盘的需求减少一个 mDiskWritesInFlight--; } //commit时,postWriteRunnable为null; apply时 postWriteRunnable 不为null if (postWriteRunnable != null) { postWriteRunnable.run(); } } }; //如果传进的参数 postWriteRunnable 为 null,那么 isFromSyncCommit 为 true //温馨提示:从上面的 commit() 方法源码中,可以看出调用 commit() 方法传入的 postWriteRunnable 为 null 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) { //如果此时只有一个 commit 请求(注意,是 commit 请求,而不是 apply )未处理,那么 wasEmpty 为 true wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { //当只有一个 commit 请求未处理,那么无需开启线程进行处理,直接在本线程执行 writeToDiskRunnable 即可 writeToDiskRunnable.run(); return; } } //将 writeToDiskRunnable 方法线程池中执行 //程序执行到这里,有两种可能: // 1. 调用的是 commit() 方法,并且当前不止一个 commit 请求未处理 // 2. 调用的是 apply() 方法 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); } private void writeToFile(MemoryCommitResult mcr) { ... } }
public final class EditorImpl implements Editor { public void apply() { //将 mModified 保存的写记录同步到内存中的 mMap 中,并且返回一个 MemoryCommitResult 对象 final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; //这里会把 写入文件的任务添加到一个队列中 //在Activity#onStop执行之前,会阻塞onStop,等待任务执行完 QueuedWork.add(awaitCommit); Runnable postWriteRunnable = new Runnable() { public void run() { awaitCommit.run(); QueuedWork.remove(awaitCommit); } }; // 将数据落地到磁盘上,注意,传入的 postWriteRunnable 参数不为 null,所以在 // enqueueDiskWrite 方法中会开启子线程异步将数据写入到磁盘中 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // Okay to notify the listeners before it's hit disk // because the listeners should always get the same // SharedPreferences instance back, which has the // changes reflected in memory. notifyListeners(mcr); } }
1)commitToMemory()方法,将 mModified 中记录的写操作同步回写到内存 SharedPreferences.mMap 中。
此时, 任何的 get 方法都可以获取到最新数据了
2)通过 enqueueDiskWrite() 方法,调用writeToFile将方法将所有数据【异步写入到文件中】
3)写入文件的任务添加到一个队列中,在Activity#onStop执行之前,会阻塞onStop,等待任务执行完
ActivityThread # handleStopActivity() 中 QueuedWork.waitToFinish(),
API 11以上,会阻塞 onStop,等待 Sp写入文件任务完成
/**有SharedPreference频繁apply()导致卡顿的原因*/ public final class ActivityThread { ... private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) { ActivityClientRecord r = mActivities.get(token); if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) { return; } r.activity.mConfigChangeFlags |= configChanges; StopInfo info = new StopInfo(); performStopActivityInner(r, info, show, true, "handleStopActivity"); updateVisibility(r, show); if (!r.isPreHoneycomb()) { /* 内部会等队列中所有任务执行完 * SharedPreferences中apply()的任务都会添加到QueuedWork内部队列中 * 而处理该队列的线程只有一个HandlerThread * 那么大量的apply()操作会导致这里要等待,从而导致UI卡顿 */ QueuedWork.waitToFinish(); } info.activity = r; info.state = r.state; info.persistentState = r.persistentState; mH.post(info); mSomeActivitiesChanged = true; } ... }
但后续调用getSharedPreferences会从内存缓存中获取。
再把异步回写磁盘的任务,放到一个单线程的线程池队列中等待执行
apply不需要等待写入磁盘完成,而是马上返回。
mDiskWritesInFlight == 1 等待写入的次数为1,直接在调用commit的线程同步回写文件
mDiskWritesInFlight > 1 异步写入的任务放到一个单线程的线程池队列中等待执行
只有一个写入任务时,commit会阻塞调用线程,直到写入磁盘完成才返回
没有使用跨进程的锁,就算使用 MODE_MULTI_PROCESS,SharedPreferences 跨进程频繁读写有可能导致数据全部丢失。
根据线上统计,SP 大约会有万分之一的损坏率
SP 性能与文件大小相关,不同业务类型数据,拆分到不到文件保存,将频繁修改的条目单独隔离出来
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。