赞
踩
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 = context.getSharedPreferences("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 = editor.commit()
//异步提交到SharedPreferences文件
editor.apply()
当我们第一次访问一个名为"setting"的SharedPreferences文件,系统会在应用数据目录下(/data/data/packageName/)的shared_prefs文件夹下,创建一个同名的setting.xml文件。
存储的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之前会先通过context.getSharedPreferences()获取SP的实例对象, context的实现类是ContextImpl, 看下ContextImpl的getSharedPreferences实现:
public SharedPreferences getSharedPreferences(String name, int mode) { if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; //name为null,则文件命名为null.xml } } File file; synchronized (ContextImpl.class) { //加锁同步 if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths.get(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对象:
@Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { //先从缓存获取 final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); sp = cache.get(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 = sSharedPrefsCache.get(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 File(prefsFile.getPath() + ".bak");
}
makeBackupFile 用来定义备份文件,命名为 “xml同名.bak”, 该文件在写入磁盘时会用到,用来备份文件,在写入失败异常的情况下,下次使用从备份文件恢复,这样就只需丢弃写入失败的数据,而之前的数据还能恢复。
@UnsupportedAppUsage private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } //开启异步线程从磁盘读取文件,加锁防止多线程并发操作 new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); } private void loadFromDisk() { synchronized (mLock) { //加锁 if (mLoaded) { //已经加载过 return; } //备份文件存在,说明上次写入失败,直接从备份文件恢复到mFile if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } } // Debugging if (mFile.exists() && !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.stat(mFile.getPath()); if (mFile.canRead()) { BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16 * 1024); // 从 XML 里面读取数据返回一个 Map,内部使用了 XmlPullParser map = (Map<String, Object>) XmlUtils.readMapXml(str); } catch (Exception e) { Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), 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 than sorry. try { 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 { // 唤醒等待的线程,到这文件读取完毕 mLock.notifyAll(); } } }
这里有一个mLoaded标记来标记是否加载完xml文件并转为map,xml解析出来的数据会存到mMap内存。
SP的初始化分析完成,可以知道应用首次使用 SP 的时候会从磁盘读取,之后缓存在内存中。
已获取String数据为例,其他数据类型一样。
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(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 StrictMode. BlockGuard.getThreadPolicy().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 对象。
@Override
public 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; @Override public 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()方法
@Override public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } finally { if (DEBUG) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " committed after " + (System.currentTimeMillis() - startTime) + " ms"); } } notifyListeners(mcr); return mcr.writeToDiskResult; }
apply()方法
@Override public void apply() { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { //等待锁, 文件写入完成后才释放锁 mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } if (DEBUG && mcr.wasWritten) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " applied after " + (System.currentTimeMillis() - startTime) + " ms"); } } }; //加入QueuedWork等待执行 QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(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 : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); if (v == this || v == null) { if (!mapToWriteToDisk.containsKey(k)) { continue; } mapToWriteToDisk.remove(k); } else { //相同不修改,不同更新 if (mapToWriteToDisk.containsKey(k)) { Object existingValue = mapToWriteToDisk.get(k); if (existingValue != null && existingValue.equals(v)) { continue; } } mapToWriteToDisk.put(k, v); } changesMade = true; if (hasListeners) { keysModified.add(k); } } //清楚mModified mModified.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) { this.memoryStateGeneration = memoryStateGeneration; this.keysCleared = keysCleared; this.keysModified = keysModified; this.listeners = listeners; this.mapToWriteToDisk = mapToWriteToDisk; } void setDiskWriteResult(boolean wasWritten, boolean result) { this.wasWritten = wasWritten; writeToDiskResult = result; writtenToDiskLatch.countDown(); }
写入磁盘的方法:
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { //根据是否为null判断是同步的commit方式还是异步的apply方式 final boolean isFromSyncCommit = (postWriteRunnable == null); final Runnable writeToDiskRunnable = new Runnable() { @Override public 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方法,直接在当前线程run boolean 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队列中。
@UnsupportedAppUsage public 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); } } } @UnsupportedAppUsage private static Handler getHandler() { synchronized (sLock) { if (sHandler == null) { HandlerThread handlerThread = new HandlerThread("queued-work-looper", Process.THREAD_PRIORITY_FOREGROUND); handlerThread.start(); sHandler = new QueuedWorkHandler(handlerThread.getLooper()); } return sHandler; } }
可以看到,内部就是通过handler发送消息执行任务的,任务还是一样的writeFile()方法. sHandler是一个全局的Handler对象,运行在HandlerThread的工作线程中,所以apply()方法,会通过一个全局唯一的异步线程进行写文件的操作。
写到文件的过程
@GuardedBy("mWritingToDiskLock") private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) { //...... boolean fileExists = mFile.exists(); if (fileExists) { boolean needsWrite = false; // 判断数据是否真正发生了变化 if (mDiskStateGeneration < mcr.memoryStateGeneration) { if (isFromSyncCommit) { needsWrite = true; } else { synchronized (mLock) { if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { needsWrite = true; } } } } if (!needsWrite) { //没有发生变化,不需要写入,直接return,避免无畏IO操作 mcr.setDiskWriteResult(false, true); return; } boolean backupFileExists = mBackupFile.exists(); if (!backupFileExists) { //备份文件 if (!mFile.renameTo(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 the backup. try { 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.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); try { final StructStat stat = Os.stat(mFile.getPath()); synchronized (mLock) { mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } } catch (ErrnoException e) { // Do nothing } if (DEBUG) { fstatTime = System.currentTimeMillis(); } //写入成功,删除备份文件 mBackupFile.delete(); mDiskStateGeneration = mcr.memoryStateGeneration; 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 (mFile.exists()) { if (!mFile.delete()) { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } mcr.setDiskWriteResult(false, false); }
写入过程简单说就是备份 → 写入 → 检查 → 善后,这样保证了数据的安全性和稳定性。
这里呼应了开头初始化时startLoadFromDisk判断是否存在备份文件,存在说明上次写入失败了,需要从备份文件读取。
SharedPreferences 的写入操作,首先是将源文件备份:mFile.renameTo(mBackupFile) 再写入所有数据,只有写入成功,并且通过 sync 完成落盘后,才会将 Backup(.bak) 文件删除。如果写入过程中进程被杀,或者关机等非正常情况发生。进程再次启动后如果发现该 SharedPreferences 存在 Backup 文件,就将 Backup 文件重名为源文件,原本未完成写入的文件就直接丢弃,这样最多也就是未完成写入的数据丢失,它能保证最后一次落盘(真正落盘)成功后的数据。
作者:godliness
链接:https://www.jianshu.com/p/f5a29bce2e6f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在讨论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 = (String)mMap.get(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 {
@Override
public 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 中的这个等待任务移除。
但当Activity.onStop()以及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");
sharedPreferences.getInteger("key", 11); //报错
edit.putString("name", "vinson").clear().putString("blog", "https://juejin.cn/user/923245496518439")
edit.apply()
因为clear()只是把mClear标记设为true,在写文件的时候把之前的数据清除,本次修改的提交都会写入。
@Override
public Editor clear() {
synchronized (mEditorLock) {
mClear = true;
return this;
}
}
参考文章
https://jishuin.proginn.com/p/763bfbd308ae
https://www.jianshu.com/p/5fcef7f68341
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。