SharedPreferense原理和缺陷分析

阅读: 评论:0

SharedPreferense原理和缺陷分析

SharedPreferense原理和缺陷分析

SharedPreferense 实现原理

简介

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
链接:
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结
  1. 通过 getSharedPreferences 可以获取 SP 实例,从首次初始化到读到数据会存在延迟,因为读文件的操作阻塞调用的线程直到文件读取完毕,如果在主线程调用,可能会对 UI 流畅度造成影响。
  2. SharedPreferences的File创建和内容解析会缓存在内存中。
  3. commit 会在调用者线程同步执行写文件,返回写入结果;apply 将写文件的操作异步执行,没有返回值。可以根据具体情况选择性使用,推荐使用 apply。
  4. 所有的文件和内存读写操作,都通过锁对象进行加锁,保证了多线程同步,是线程安全的,但不是进程安全的。
  5. 文件的更新是全量更新的,修改一个值都会对整个xml文件进行覆盖操作。

SharedPreferense存在的问题

在讨论SP的缺陷之前,我们先思考一下SP设计中需要考虑的部分:

  1. 读操作的优化:初始化时,对xml文件进行一次读取,并将文件内所有内容(即所有的键值对)缓存到内存的一个Map中,这样,接下来所有的读操作,只需要从这个Map中取就可以了。这是一种空间换时间的权衡,规避了短时间内频繁的I/O操作对性能产生的影响。为避免高内存占用,不要用SP存大数据!!
  2. 写操作的优化:写操作,设计者抽象出了一个Editor类,不管某次操作通过若干次调用putXXX()方法,更新了几个xml中的键值对,只有调用了commit()或apply()方法,最终才会真正写入文件.可以将多个键值对的更新合并到一次写操作。
  3. 数据更新和文件数量权衡:我们知道数据更新的时候是全量更新xml文件的,如果数据量大对写操作对成本会很高。所以我们需要根据业务分文件存储,getSharedPreferences(String name, int mode)传入不同文件名区分。
  4. 线程安全问题:SP是线程安全的。那么他是如何保证线程安全的呢?用了哪些锁?
    —— 为了保证SharedPreferences是线程安全的,主要使用了3把锁:
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);
}
  1. 文件损坏和备份机制:由于不可预知情况导致写xml文件的时候异常出错,如何避免对整个xml文件造成损坏?

——答案是用文件备份机制。SharedPreferences的写入操作正式执行之前,首先会对文件进行备份,将初始文件重命名为增加了一个.bak后缀的备份文件。SharedPreferences的写入操作正式执行之前,首先会对文件进行备份,将初始文件重命名为增加了一个.bak后缀的备份文件。这之后,尝试对文件进行写入操作,写入成功时,则将备份文件删除;反之,若因异常情况(比如进程被杀)导致写入失败,进程再次启动后,若发现存在备份文件,则将备份文件重名为源文件,原本未完成写入的文件就直接丢弃。

  1. 摆脱不掉的ANR问题:我们知道commit是同步的方法,如果在主线程使用可能导致ANR。而apply方法是异步处理的,那么它就不会导致ANR吗?答案是否定的。apply()的内部实现的确将I/O操作交给了子线程,可以说其本身是没有问题的,而其原因归根到底则是Android的另外一个机制。

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清除队列。

参考头条方案

  1. 不支持跨进程:sp有一个貌似可以提供「跨进程」功能的FLAG——MODE_MULTI_PROCESS,这个flag保证了啥?保证了在API 11以前的系统上,如果sp已经被读取进内存,再次获取这个sp的时候,如果有这个flag,会重新读一遍文件,仅此而已!而且现在已经被废弃了。所以要支持跨进程最好用ContentProvider或者加文件锁,后面介绍。

根据前面的源码分析总结SP存在以下的坑

  1. SP数据会一直缓存在内存,占用内存空间。(SharedPreferencesImpl 在初始化后就会自动去加载 xmlFile 中的所有键值对数据。ContextImpl 的逻辑是先根据 fileName 拿到 xmlFile,再根据 xmlFile 拿到 SharedPreferencesImpl,最终应用内所有的 SharedPreferencesImpl 都会被缓存在 sSharedPrefsCache 这个静态变量中。)
  2. 可能堵塞导致卡顿或ANR。get操作需要等待xml文件加载完成,如果文件大加载时间长可能导致卡顿或ANR;commit如果在主线程调用也会,apply也可能导致activity跳转阻塞。
  3. getValue 不保证数据类型安全。put和get数据类型不对应会出错。
put("key", "v");
Integer("key", 11); //报错
  1. 不支持增量更新。
  2. clear()反直觉用法,下面的代码我们可能以为只会留下“blog”的key-value,而实际上是两个都会保存。
edit.putString("name", "vinson").clear().putString("blog", "")
edit.apply()

因为clear()只是把mClear标记设为true,在写文件的时候把之前的数据清除,本次修改的提交都会写入。

@Overridepublic Editor clear() {synchronized (mEditorLock) {mClear = true;return this;}}
  1. SP是进程不安全的,不支持跨进程使用。
    要实现跨进程支持,需要用ContentProvide对SP进行包装,提供跨进程能力。参考SP跨进程实现

参考文章

本文发布于:2024-02-05 00:02:41,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170719416360967.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23