SharedPreferences是Android提供给我们的用于存储轻量级K-V数据的持久化方案。以XML文件的形式存储在/data/data/packageName/的shared_prefs文件夹。
它提供了 putString()、putString(Set)、putInt()、putLong()、putFloat()、putBoolean() 六种数据类型。(注意没有Double)
使用示例
//根据文件名,获取SharedPreferences对象;mode一般都使用MODE_PRIVATE,只能由该App访问
SharedPreferences sp = SharedPreferences("setting", Context.MODE_PRIVATE)
//根据key,获取指定值
Boolean needInitChannels = sp.getBoolean("isDebug", false)
//获取Editor编辑对象,用于编辑SharedPreferences
SharedPreferences.Editor editor = sp.edit()
editor.putBoolean("isDebug",true)
editor.putLong("isLong",1000)
//同步提交到SharedPreferences文件,获取是否同步成功的结果
Boolean res = editormit()
//异步提交到SharedPreferences文件
editor.apply()
当我们第一次访问一个名为"setting"的SharedPreferences文件,系统会在应用数据目录下(/data/data/packageName/)的shared_prefs文件夹下,创建一个同名的l文件。
存储的xml文件格式如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map><long name="isLong" value="1000" /><boolean name="isDebug" value="true" /><!-- <float name="isFloat" value="1.5" /><string name="isString">Android</string><int name="isInt" value="1" /><set name="isStringSet"><string>element 1</string><string>element 2</string><string>element 3</string></set> -->
</map>
我们在使用SP之前会先通过SharedPreferences()获取SP的实例对象, context的实现类是ContextImpl, 看下ContextImpl的getSharedPreferences实现:
public SharedPreferences getSharedPreferences(String name, int mode) {if (ApplicationInfo().targetSdkVersion <Build.VERSION_CODES.KITKAT) {if (name == null) {name = "null"; //name为null,则文件命名为l}}File file;synchronized (ContextImpl.class) { //加锁同步if (mSharedPrefsPaths == null) {mSharedPrefsPaths = new ArrayMap<>();}file = (name);if (file == null) {file = getSharedPreferencesPath(name);//mSharedPrefsPaths缓存文件名和文件映射mSharedPrefsPaths.put(name, file);}}return getSharedPreferences(file, mode);}
这里有一个重要的参数mSharedPrefsPaths
private ArrayMap<String, File> mSharedPrefsPaths;
它是一个ArrayMap,缓存了文件名和文件对象的映射。初始化获取时会先从缓存里获取对应的文件对象,没有再去创建文件并缓存。
接着通过getSharedPreferences(file, mode)获取SharedPreferences对象:
@Overridepublic SharedPreferences getSharedPreferences(File file, int mode) {SharedPreferencesImpl sp;synchronized (ContextImpl.class) {//先从缓存获取final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();sp = (file);if (sp == null) {checkMode(mode);// ... new一个实例sp = new SharedPreferencesImpl(file, mode);cache.put(file, sp);return sp;}}//.....return sp;}
同样可以看到,这里对SharedPreferences的实例对象SharedPreferencesImpl也进行了缓存。
getSharedPreferences获取缓存:
@GuardedBy("ContextImpl.class")private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {if (sSharedPrefsCache == null) {sSharedPrefsCache = new ArrayMap<>();}final String packageName = getPackageName();ArrayMap<File, SharedPreferencesImpl> packagePrefs = (packageName);if (packagePrefs == null) {packagePrefs = new ArrayMap<>();sSharedPrefsCache.put(packageName, packagePrefs);}return packagePrefs;}
sSharedPrefsCache时ContextImpl的静态变量,缓存了packageName-ArrayMap<File, SharedPreferencesImpl>
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
也就是说sSharedPrefsCache缓存了同一个应用包名的ArrayMap<File, SharedPreferencesImpl>集合,一个文件对应一个SharedPreferencesImpl对象。
也就是说,一个name会对应一个SharedPreferences的File实例,而一个File会对应一个SharedPreferencesImpl实例。并且对File实例和SharedPreferencesImpl实例对象都进行了缓存
首次使用 getSharedPreferences 时,内存中不存在 SP 以及 SP Map 缓存,需要创建 SP 并添加到 ContextImpl 的静态成员变量(sSharedPrefs)中。
sp = new SharedPreferencesImpl(file, mode);
SharedPreferencesImpl构造方法
SharedPreferencesImpl(File file, int mode) {mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;mThrowable = null;startLoadFromDisk();}
static File makeBackupFile(File prefsFile) {return new Path() + ".bak");}
makeBackupFile 用来定义备份文件,命名为 “xml同名.bak”, 该文件在写入磁盘时会用到,用来备份文件,在写入失败异常的情况下,下次使用从备份文件恢复,这样就只需丢弃写入失败的数据,而之前的数据还能恢复。
@UnsupportedAppUsageprivate void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}//开启异步线程从磁盘读取文件,加锁防止多线程并发操作new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();}private void loadFromDisk() {synchronized (mLock) { //加锁if (mLoaded) { //已经加载过return;}//备份文件存在,说明上次写入失败,直接从备份文件恢复到mFileif (ists()) {mFile.delete();ameTo(mFile);}}// Debuggingif (ists() && !mFile.canRead()) {Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");}Map<String, Object> map = null;StructStat stat = null;Throwable thrown = null;try {stat = Os.Path());if (mFile.canRead()) {BufferedInputStream str = null;try {str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);// 从 XML 里面读取数据返回一个 Map,内部使用了 XmlPullParsermap = (Map<String, Object>) adMapXml(str);} catch (Exception e) {Log.w(TAG, "Cannot read " + AbsolutePath(), e);} finally {IoUtils.closeQuietly(str);}}} catch (ErrnoException e) {// An errno exception means the stat failed. Treat as empty/non-existing by// ignoring.} catch (Throwable t) {thrown = t;}synchronized (mLock) {mLoaded = true; //标记加载完成mThrowable = thrown;// It's important that we always signal waiters, even if we'll make// them fail with an exception. The try-finally is pretty wide, but// better safe {if (thrown == null) {if (map != null) {mMap = map;mStatTimestamp = stat.st_mtim;mStatSize = stat.st_size;} else {mMap = new HashMap<>();}}// In case of a thrown exception, we retain the old map. That allows// any open editors to commit and store updates.} catch (Throwable t) {mThrowable = t;} finally {// 唤醒等待的线程,到这文件读取完毕ifyAll();}}}
这里有一个mLoaded标记来标记是否加载完xml文件并转为map,xml解析出来的数据会存到mMap内存。
SP的初始化分析完成,可以知道应用首次使用 SP 的时候会从磁盘读取,之后缓存在内存中。
已获取String数据为例,其他数据类型一样。
@Override@Nullablepublic String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = ((key);return v != null ? v : defValue;}}
@GuardedBy("mLock")private void awaitLoadedLocked() {if (!mLoaded) {// Raise an explicit StrictMode onReadFromDisk for this// thread, since the real read will be in a different// thread and otherwise ignored by ThreadPolicy().onReadFromDisk();}while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}if (mThrowable != null) {throw new IllegalStateException(mThrowable);}}
读数据是会先调用awaitLoadedLocked()根据mLoaded标记判断数据是否加载完成,如果没有则同步等待数据加载完成释放锁。(如果单个 SP 存储的内容过多,导致我们使用 getXXX 方法的时候阻塞,特别是在主线程调用的时候,所以建议在单个 SP 中尽量少地保存数据。)
加载完成,则直接从内存mMap读取返回。
SP 写入数据的操作是通过 Editor 完成的,它也是一个接口,实现类是 EditorImpl,是 SharedPreferencesImpl 的内部类。
通过 SP 的 edit 方法获取 Editor 实例,等到加载完毕直接返回一个 EditorImpl 对象。
@Overridepublic Editor edit() {synchronized (mLock) { //加锁,等待加载完成awaitLoadedLocked();}return new EditorImpl();}
写入数据,以String类型为例:
public final class EditorImpl implements Editor {private final Object mEditorLock = new Object();@GuardedBy("mEditorLock")private final Map<String, Object> mModified = new HashMap<>();@GuardedBy("mEditorLock")private boolean mClear = false;@Overridepublic Editor putString(String key, @Nullable String value) {synchronized (mEditorLock) {mModified.put(key, value);return this;}}//...........
}
用一个map变量mModified保存要修改的数据,后面再将改动保存到 SP 的 mMap mEditorLock加锁保证同步。
修改后要通过commit或者apply方法将修改保存到内存和磁盘。
commit是同步方法且有返回值,apply是异步方法没有返回值。
commit()方法
@Overridepublic boolean commit() {long startTime = 0;if (DEBUG) {startTime = System.currentTimeMillis();}MemoryCommitResult mcr = commitToMemory();queueDiskWrite(mcr, null /* sync write on this thread okay */);try {mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {if (DEBUG) {Log.d(TAG, Name() + ":" + StateGeneration+ " committed after " + (System.currentTimeMillis() - startTime)+ " ms");}}notifyListeners(mcr);return mcr.writeToDiskResult;}
apply()方法
@Overridepublic void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {//等待锁, 文件写入完成后才释放锁mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}if (DEBUG && mcr.wasWritten) {Log.d(TAG, Name() + ":" + StateGeneration+ " applied after " + (System.currentTimeMillis() - startTime)+ " ms");}}};//加入QueuedWork等待执行QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();veFinisher(awaitCommit);}};queueDiskWrite(mcr, postWriteRunnable);notifyListeners(mcr);}
可以看到实际上两个方法的实现很相似,都是先通过commitToMemory()方法将修改同步到内存,再通过enqueueDiskWrite方法写到内存,不同的是commit方法参数为null,而apply方法参数为postWriteRunnable
先看写入内存的方法:
private MemoryCommitResult commitToMemory() {long memoryStateGeneration;boolean keysCleared = false;List<String> keysModified = null;Set<OnSharedPreferenceChangeListener> listeners = null;Map<String, Object> mapToWriteToDisk;synchronized (SharedPreferencesImpl.this.mLock) {if (mDiskWritesInFlight > 0) {mMap = new HashMap<String, Object>(mMap);}mapToWriteToDisk = mMap; //先复制旧数据mDiskWritesInFlight++;boolean hasListeners = mListeners.size() > 0;if (hasListeners) {keysModified = new ArrayList<String>();listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());}synchronized (mEditorLock) {boolean changesMade = false;if (mClear) { //调用过clear方法,先清除旧数据if (!mapToWriteToDisk.isEmpty()) {changesMade = true;mapToWriteToDisk.clear();}keysCleared = true;mClear = false;}for (Map.Entry<String, Object> e : Set()) {String k = e.getKey();Object v = e.getValue();if (v == this || v == null) {if (!ainsKey(k)) {continue;}ve(k);} else {//相同不修改,不同更新if (ainsKey(k)) {Object existingValue = (k);if (existingValue != null && existingValue.equals(v)) {continue;}}mapToWriteToDisk.put(k, v);}changesMade = true;if (hasListeners) {keysModified.add(k);}}//清楚mModifiedmModified.clear();if (changesMade) {mCurrentMemoryStateGeneration++;}memoryStateGeneration = mCurrentMemoryStateGeneration;}}return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,listeners, mapToWriteToDisk);}
写入的时候先将mMap赋值给局部变量Map<String, Object> mapToWriteToDisk,然后将新写入的数据add到mapToWriteToDisk中,最后封装到MemoryCommitResult中返回。
private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared,@Nullable List<String> keysModified,@Nullable Set<OnSharedPreferenceChangeListener> listeners,Map<String, Object> mapToWriteToDisk) {StateGeneration = memoryStateGeneration;this.keysCleared = keysCleared;this.keysModified = keysModified;this.listeners = listeners;this.mapToWriteToDisk = mapToWriteToDisk;}void setDiskWriteResult(boolean wasWritten, boolean result) {this.wasWritten = wasWritten;writeToDiskResult = untDown();}
写入磁盘的方法:
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {//根据是否为null判断是同步的commit方式还是异步的apply方式final boolean isFromSyncCommit = (postWriteRunnable == null);final Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run();}}};// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) { //commit方法,直接在当前线程runboolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}
我们上面说到commit和apply在调用这个方法的时候区别在于postWriteRunnable是否为null.这里先根据是否为null判断是同步的commit方式还是异步的apply方式.如果是comity方法,直接在当前线程调用writeToDiskRunnable.run();写入文件writeToFile(mcr, isFromSyncCommit);,如果是apply则将写文件的Runnable任务加到QueuedWork队列中。
@UnsupportedAppUsagepublic static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler();synchronized (sLock) {sWork.add(work); //将任务加入到队列中等待执行//通过handler发送消息if (shouldDelay && sCanDelay) {handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);} else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);}}}@UnsupportedAppUsageprivate static Handler getHandler() {synchronized (sLock) {if (sHandler == null) {HandlerThread handlerThread = new HandlerThread("queued-work-looper",Process.THREAD_PRIORITY_FOREGROUND);handlerThread.start();sHandler = new Looper());}return sHandler;}}
可以看到,内部就是通过handler发送消息执行任务的,任务还是一样的writeFile()方法. sHandler是一个全局的Handler对象,运行在HandlerThread的工作线程中,所以apply()方法,会通过一个全局唯一的异步线程进行写文件的操作。
写到文件的过程
@GuardedBy("mWritingToDiskLock")private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {//......boolean fileExists = ists();if (fileExists) {boolean needsWrite = false;// 判断数据是否真正发生了变化if (mDiskStateGeneration < StateGeneration) {if (isFromSyncCommit) {needsWrite = true;} else {synchronized (mLock) {if (mCurrentMemoryStateGeneration == StateGeneration) {needsWrite = true;}}}}if (!needsWrite) { //没有发生变化,不需要写入,直接return,避免无畏IO操作mcr.setDiskWriteResult(false, true);return;}boolean backupFileExists = ists();if (!backupFileExists) {//备份文件if (!ameTo(mBackupFile)) {Log.e(TAG, "Couldn't rename file " + mFile+ " to backup file " + mBackupFile);mcr.setDiskWriteResult(false, false);return;}} else {mFile.delete();}}// Attempt to write the file, delete the backup and return true as atomically as// possible. If any exception occurs, delete the new file; next time we will restore// from {FileOutputStream str = createFileOutputStream(mFile);if (str == null) {mcr.setDiskWriteResult(false, false);return;}//写入xml文件XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);writeTime = System.currentTimeMillis();
//强制落盘机制.默认系统采用的是延迟写机制,应用程序只需要将数据写到页缓冲中去就可以了,这里强制写入磁盘FileUtils.sync(str);fsyncTime = System.currentTimeMillis();str.close();ContextImpl.Path(), mMode, 0);try {final StructStat stat = Os.Path());synchronized (mLock) {mStatTimestamp = stat.st_mtim;mStatSize = stat.st_size;}} catch (ErrnoException e) {// Do nothing}if (DEBUG) {fstatTime = System.currentTimeMillis();}//写入成功,删除备份文件mBackupFile.delete();mDiskStateGeneration = StateGeneration;mcr.setDiskWriteResult(true, true);long fsyncDuration = fsyncTime - writeTime;mSyncTimes.add((int) fsyncDuration);mNumSync++;return;} catch (XmlPullParserException e) {Log.w(TAG, "writeToFile: Got exception:", e);} catch (IOException e) {Log.w(TAG, "writeToFile: Got exception:", e);}// 写入失败删除临时文件if (ists()) {if (!mFile.delete()) {Log.e(TAG, "Couldn't clean up partially-written file " + mFile);}}mcr.setDiskWriteResult(false, false);}
写入过程简单说就是备份 → 写入 → 检查 → 善后,这样保证了数据的安全性和稳定性。
这里呼应了开头初始化时startLoadFromDisk判断是否存在备份文件,存在说明上次写入失败了,需要从备份文件读取。
SharedPreferences 的写入操作,首先是将源文件备份ameTo(mBackupFile) 再写入所有数据,只有写入成功,并且通过 sync 完成落盘后,才会将 Backup(.bak) 文件删除。如果写入过程中进程被杀,或者关机等非正常情况发生。进程再次启动后如果发现该 SharedPreferences 存在 Backup 文件,就将 Backup 文件重名为源文件,原本未完成写入的文件就直接丢弃,这样最多也就是未完成写入的数据丢失,它能保证最后一次落盘(真正落盘)成功后的数据。
作者:godliness
链接:
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在讨论SP的缺陷之前,我们先思考一下SP设计中需要考虑的部分:
final class SharedPreferencesImpl implements SharedPreferences {// 1、使用注释标记锁的顺序// Lock ordering rules:// - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock// - acquire mWritingToDiskLock before EditorImpl.mLock// 2、通过注解标记持有的是哪把锁@GuardedBy("mLock")private Map<String, Object> mMap;@GuardedBy("mWritingToDiskLock")private long mDiskStateGeneration;public final class EditorImpl implements Editor {@GuardedBy("mEditorLock")private final Map<String, Object> mModified = new HashMap<>();}
}
对于简单的 读操作 而言,我们知道其原理是读取内存中mMap的值并返回,那么为了保证线程安全,只需要加一把锁mLock保证mMap的线程安全即可:
public String getString(String key, @Nullable String defValue) {synchronized (mLock) {String v = ((key);return v != null ? v : defValue;}
}
对于写操作而言,每次putXXX()并不能立即更新在mMap中,这是理所当然的,如果开发者没有调用apply()方法,那么这些数据的更新理所当然应该被抛弃掉,但是如果直接更新在mMap中,那么数据就难以恢复。
因此,Editor本身也应该持有一个mEditorMap对象,用于存储数据的更新;只有当调用apply()时,才尝试将mEditorMap与mMap进行合并,以达到数据更新的目的。
因此,这里我们还需要另外一把锁保证mEditorMap的线程安全,不和mMap公用同一把锁的原因是,在apply()被调用之前,getXXX和putXXX理应是没有冲突的。
public final class EditorImpl implements Editor {@Overridepublic Editor putString(String key, String value) {synchronized (mEditorLock) {mEditorMap.put(key, value);return this;}}
}
文件的更新理所当然也需要加一把锁mWritingToDiskLock:
synchronized (mWritingToDiskLock) {writeToFile(mcr, isFromSyncCommit);
}
——答案是用文件备份机制。SharedPreferences的写入操作正式执行之前,首先会对文件进行备份,将初始文件重命名为增加了一个.bak后缀的备份文件。SharedPreferences的写入操作正式执行之前,首先会对文件进行备份,将初始文件重命名为增加了一个.bak后缀的备份文件。这之后,尝试对文件进行写入操作,写入成功时,则将备份文件删除;反之,若因异常情况(比如进程被杀)导致写入失败,进程再次启动后,若发现存在备份文件,则将备份文件重名为源文件,原本未完成写入的文件就直接丢弃。
apply产生ANR的原因:
apply 方法,首先创建了一个 awaitCommit 的 Runnable,然后加入到 QueuedWork 中,awaitCommit 中包含了一个等待锁,当文件更新完毕后才会释放锁。 writeToFile 执行完成会释放等待锁,之后会回调传递进来的第二个参数 Runnable 的 run 方法,并将 QueuedWork 中的这个等待任务移除。
但当Stop()以及Service处理onStop等相关方法时,则会执行 QueuedWork.waitToFinish()等待所有的等待锁释放,因此如果SharedPreferences一直没有完成更新任务,有可能会导致卡在主线程,最终超时导致ANR。
什么情况下SharedPreferences会一直没有完成任务呢?比如太频繁无节制的apply(),导致任务过多,这也侧面说明了SPUtils.putXXX()这种粗暴的设计的弊端。
总结来看,SP 调用 apply 方法,会创建一个等待锁放到 QueuedWork 中,并将真正数据持久化封装成一个任务放到异步队列中执行,任务执行结束会释放锁。Activity onStop 以及 Service 处理 onStop,onStartCommand 时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放。
public static void waitToFinish() {Handler handler = getHandler();//.......try {processPendingWork(); //处理队列任务} finally {StrictMode.setThreadPolicy(oldPolicy);}try {while (true) {Runnable finisher;synchronized (sLock) {finisher = sFinishers.poll();}if (finisher == null) {break;}finisher.run(); //运行finisher任务,awaitCommit运行获得等待锁}} finally {sCanDelay = true;}
//.......}
如何解决?
—— 清空等待队列
Activity 的 onStop,以及 Service 的 onStop 和 onStartCommand 都是通过 ActivityThread 触发的,ActivityThread 中有一个 Handler 变量,我们通过 Hook 拿到此变量,给此 Handler 设置一个 callback,Handler 的 dispatchMessage 中会先处理 callback。
反射调用QueuedWork清除队列。
参考头条方案
根据前面的源码分析总结SP存在以下的坑:
put("key", "v");
Integer("key", 11); //报错
edit.putString("name", "vinson").clear().putString("blog", "")
edit.apply()
因为clear()只是把mClear标记设为true,在写文件的时候把之前的数据清除,本次修改的提交都会写入。
@Overridepublic Editor clear() {synchronized (mEditorLock) {mClear = true;return this;}}
参考文章
本文发布于:2024-02-05 00:02:41,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170719416360967.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |