SharedPreferences
应该是任何一名 Android 初学者都知道的存储类了,它轻量,适合用于保存软件配置等参数。以键值对的 XML 文件形式存储在本地,程序卸载后也会一并清除,不会残留信息。
使用起来也非常简单。
// 读取val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)val string = sharedPreferences.getString("123","")// 写入val editor = sharedPreferences.edit()editor.putString("123","123")editor.commit()
当我们写下这样的代码的时候,IDE 极易出现一个警告,提示我们用 apply()
来替换 commit()
。原因也很简单,因为 commit()
是同步的,而 apply()
采用异步的方式通常来说效率会更高一些。但是,当我们把 editor.commit()
的返回值赋给一个变量的时候,这时候就会发现 IDE 没有了警告。这是因为 IDE 认为我们想要使用 editor.commit()
的返回值了,所以,通常来说,在我们不关心操作结果的时候,我们更倾向于使用 apply()
进行写入的操作。
获取 SharedPreferences 实例
我们可以通过 3 种方式来获取 SharedPreferences
的实例。
getSharedPreferences("123", Context.MODE_PRIVATE)
Context
的任意子类都可以直接通过 getSharedPreferences()
方法获取到 SharedPreferences
的实例,接受两个参数,分别对应 XML 文件的名字和操作模式。其中 MODE_WORLD_READABLE
和 MODE_WORLD_WRITEABLE
这两种模式已在 Android 4.2 版本中被废弃。
- Context.MODE_PRIVATE: 指定该
SharedPreferences
数据只能被本应用程序读、写; - Context.MODE_WORLD_READABLE: 指定该
SharedPreferences
数据能被其他应用程序读,但不能写; - Context.MODE_WORLD_WRITEABLE: 指定该
SharedPreferences
数据能被其他应用程序读; - Context.MODE_APPEND:该模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件;
另外在 Activity
的实现中,还可以直接通过 getPreferences()
获取,实际上也就把当前 Activity 的类名作为文件名参数。
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) { return getSharedPreferences(getLocalClassName(), mode);}
此外,我们也可以通过 PreferenceManager
的 getDefaultSharedPreferences()
获取到。
public static SharedPreferences getDefaultSharedPreferences(Context context) { return context.getSharedPreferences(getDefaultSharedPreferencesName(context), getDefaultSharedPreferencesMode());}public static String getDefaultSharedPreferencesName(Context context) { return context.getPackageName() + "_preferences";}private static int getDefaultSharedPreferencesMode() { return Context.MODE_PRIVATE;}
可以很明显的看到,这个方式就是在直接把当前应用的包名作为前缀来进行命名的。
注意:如果在 Fragment 中使用
SharedPreferences
时,SharedPreferences
的初始化尽量放在onAttach(Activity activity)
里面进行 ,否则可能会报空指针,即getActivity()
会可能返回为空。
SharedPreferences 源码(基于 API 28)
有较多 SharedPreferences
使用经验的人,就会发现 SharedPreferences
其实具备挺多的坑,但这些坑主要都是因为不熟悉其中真正的原理所导致的,所以,笔者在这里,带大家一起揭开 SharedPreferences
的神秘面纱。
SharedPreferences 实例获取
前面讲了 SharedPreferences
有三种获取实例的方法,但归根结底都是调用的 Context
的 getSharedPreferences()
方法。由于 Android 的 Context
类采用的是装饰者模式,而装饰者对象其实就是 ContextImpl
,所以我们来看看源码是怎么实现的。
// 存放的是名称和文件夹的映射,实际上这个名称就是我们外面传进来的 nameprivate ArrayMapmSharedPrefsPaths;public SharedPreferences getSharedPreferences(String name, int mode) { // 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"; } } File file; synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths.get(name); if (file == null) { file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode);}@Overridepublic File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml");}private File makeFilename(File base, String name) { if (name.indexOf(File.separatorChar) < 0) { return new File(base, name); } throw new IllegalArgumentException( "File " + name + " contains a path separator");}
可以很明显的看到,内部是采用 ArrayMap
来做的处理,而这个 mSharedPrefsPaths
主要是用于存放名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name,这时候我们通过 name 拿到我们的 File,如果当前池子中没有的话,则直接新建一个 File,并放入到 mSharedPrefsPaths
中。最后还是调用的重载方法 getSharedPreferences(File,mode)
// 存放包名与ArrayMap键值对,初始化时会默认以包名作为键值对中的 Key,注意这是个 static 变量private static ArrayMap> sSharedPrefsCache;@Overridepublic SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { final ArrayMap cache = getSharedPreferencesCacheLocked(); sp = cache.get(file); if (sp == null) { 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"); } } sp = new SharedPreferencesImpl(file, mode); cache.put(file, 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;} private ArrayMap getSharedPreferencesCacheLocked() { if (sSharedPrefsCache == null) { sSharedPrefsCache = new ArrayMap<>(); } final String packageName = getPackageName(); ArrayMap packagePrefs = sSharedPrefsCache.get(packageName); if (packagePrefs == null) { packagePrefs = new ArrayMap<>(); sSharedPrefsCache.put(packageName, packagePrefs); } return packagePrefs;}
可以看到,又采用了一个 ArrayMap
来存放文件和 SharedPreferencesImpl
组成的键值对,然后通过通过单例的方式返回一个 SharedPreferences
对象,实际上是 SharedPreferences
的实现类 SharedPreferencesImpl
,而且在其中还建立了一个内部缓存机制。
所以,从上面的分析中,我们能知道 对于一个相同的 name,我们获取到的都是同一个 SharedPreferencesImpl 对象。
SharedPreferencesImpl
在上面的操作中,我们可以看到在第一次调用 getSharedPreferences
的时候,我们会去构造一个 SharedPreferencesImpl
对象,我们来看看都做了什么。
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; startLoadFromDisk();}private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start();}private void loadFromDisk() { synchronized (mLock) { if (mLoaded) { return; } 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"); } Mapmap = 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); map = (Map ) 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(); } }}
注意看我们的 startLoadFromDisk
方法,我们会去新开一个子线程,然后去通过 XmlUtils.readMapXml()
方法把指定的 SharedPreferences
文件的所有的键值对都读出来,然后存放到一个 map 中。
而众所周知,文件的读写操作都是耗时的,可想而知,在我们第一次去读取一个 SharedPreferences
文件的时候花上了太多的时间会怎样。
SharedPreferences 的读取操作
上面讲了初次获取一个文件的 SharedPreferences
实例的时候,会先去把所有键值对读取到缓存中,这明显是一个耗时操作,而我们正常的去读取数据的时候,都是类似这样的代码。
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)val string = sharedPreferences.getString("123","")
SharedPreferences
的getXXX()
方法可能会报ClassCastException
异常,所以我们在同一个 name 的时候,对不一样的类型,必须使用不同的 key。但是putXXX
是可以用不同的类型值覆盖相同的 key 的。
那势必可能会导致这个操作需要等待一定的时间,我们姑且可以这么猜想,在 getXXX()
方法执行的时候应该是会等待前面的操作完成才能执行的。
因为 SharedPreferences
是一个接口,所以我们主要来看看它的实现类 SharedPreferencesImpl
,这里以 getString()
为例。
@Override@Nullablepublic String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; }}
awaitLoadedLocked()
方法应该就是我们所想的等待执行操作了,我们看看里面做了什么。
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
方法里面我们使用了 mLock.wait()
来等待初始化的读取操作,而我们前面看到的 loadFromDiskLocked()
方法的最后也可以看到它调用了 mLock.notifyAll()
方法来唤醒后面这个阻塞的 getXXX()
。那么这里就会明显出现一个问题,我们的 getXXX()
方法是写在 UI 线程的,如果这个方法被阻塞的太久,势必会出现 ANR 的情况。所以我们一定在平时需要根据具体情况考虑是否需要把 SharedPreferences
的读写操作放在子线程中。
SharedPreferences 的内部类 Editor
我们在写入数据之前,总是要先通过类似这样的代码获取 SharedPreferences
的内部类 Editor
。
val editor = sharedPreferences.edit()
我们当然要看看这个到底是什么东西。
@Override public Editor edit() { // TODO: remove the need to call awaitLoadedLocked() when // requesting an editor. will require some work on the // Editor, but then we should be able to do: // // context.getSharedPreferences(..).edit().putString(..).apply() // // ... all without blocking. synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl(); }
我们在
可以看到,我们在读取解析完 XML 文件的时候,直接返回了一个 Editor
的实现类 EditorImpl
。我们随便查看一个 putXXX 的方法一看。
private final Object mEditorLock = new Object();@GuardedBy("mEditorLock")private final MapmModified = new HashMap<>();@GuardedBy("mEditorLock")private boolean mClear = false;@Overridepublic Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this; }}
可以看到,我们在 EditorImpl
里面使用了一个 HashMap
来存放我们的键值对数据,每次 put 的时候都会直接往这个键值对变量 mModified
中进行数据的 put 操作。
commit() 和 apply()
我们总是在更新数据后需要加上 commit()
或者 apply()
来进行输入的写入操作,我们不妨来看看他们的实现到底有什么区别。
先看 commit() 和 apply() 的源码。
@Overridepublic 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;}@Overridepublic 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.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; 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);}
可以看到,apply()
和 commit()
的区别是在 commit()
把内容同步提交到了硬盘,而 apply()
是先立即把修改提交给了内存,然后开启了一个异步的线程提交到硬盘。commit()
会接收 MemoryCommitResult
里面的一个 boolean
参数作为结果,而 apply()
没有对结果做任何关心。
我们可以看到,文件写入更新的操作都是交给 commitToMemory()
做的,这个方法返回了一个 MemoryCommitResult
对象,我们来看看到底做了什么。
// Returns true if any changes were madeprivate MemoryCommitResult commitToMemory() { long memoryStateGeneration; ListkeysModified = null; Set listeners = null; Map mapToWriteToDisk; synchronized (SharedPreferencesImpl.this.mLock) { // 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 unchecked mMap = new HashMap (mMap); } mapToWriteToDisk = mMap; mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { keysModified = new ArrayList (); listeners = new HashSet (mListeners.keySet()); } synchronized (mEditorLock) { boolean changesMade = false; if (mClear) { if (!mapToWriteToDisk.isEmpty()) { changesMade = true; mapToWriteToDisk.clear(); } mClear = false; } for (Map.Entry 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 (!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.clear(); if (changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; } } return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk);}
可以看到,我们这里的 mMap
即存放当前 SharedPreferences
文件中的键值对,而 mModified
则存放的是当时 edit()
时 put 进去的键值对,这个我们前面有所介绍。这里有个 mDiskWritesInFlight
看起来应该是表示正在等待写的操作数量。
接下来我们首先处理了 edit().clear()
操作的 mClear
标志,当我们在外面调用 clear()
方法的时候,我们会把 mClear
设置为 true,这时候我们会直接通过 mMap.clear()
清空此时文件中的键值对,然后再遍历 mModified
中新 put 进来的键值对数据放到 mMap
中。也就是说:在一次提交中,如果我们又有 put 又有 clear()
操作的话,我们只能 clear()
掉之前的键值对,这次 put()
进去的键值对还是会被写入到 XML 文件中。
// 读取val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)// 写入val editor = sharedPreferences.edit()editor.putInt("1", 123)editor.clear()editor.apply()Log.e("nanchen2251", "${sharedPreferences.getInt("1", 0)}")
也就是说,当我们编写下面的代码的时候,得到的打印还是 123。
然后我们接着往下看,又发现了另外一个 commit()
和 apply()
都做了调用的方法是 enqueueDiskWrite()
。
/** * Enqueue an already-committed-to-memory result to be written * to disk. * * They will be written to disk one-at-a-time in the order * that they're enqueued. * * @param postWriteRunnable if non-null, we're being called * from apply() and this is the runnable to run after * the write proceeds. if null (from a regular commit()), * then we're allowed to do this disk write on the main * thread (which in addition to reducing allocations and * creating a background thread, this has the advantage that * we catch them in userdebug StrictMode reports to convert * them where possible to apply() ...) */private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { 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) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}
在这个方法中,首先通过判断 postWriteRunnable
是否为 null 来判断是 apply()
还是 commit()
。然后定义了一个 Runnable
任务,在 Runnable
中先调用了 writeToFile()
进行了写入和计数器更新的操作。
然后我们再来看看这个 writeToFile()
方法做了些什么。
@GuardedBy("mWritingToDiskLock")private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) { long startTime = 0; long existsTime = 0; long backupExistsTime = 0; long outputStreamCreateTime = 0; long writeTime = 0; long fsyncTime = 0; long setPermTime = 0; long fstatTime = 0; long deleteTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } boolean fileExists = mFile.exists(); if (DEBUG) { existsTime = System.currentTimeMillis(); // Might not be set, hence init them to a default value backupExistsTime = existsTime; } // Rename the current file so it may be used as a backup during the next read if (fileExists) { boolean needsWrite = false; // Only need to write if the disk state is older than this commit if (mDiskStateGeneration < mcr.memoryStateGeneration) { if (isFromSyncCommit) { needsWrite = true; } else { synchronized (mLock) { // No need to persist intermediate states. Just wait for the latest state to // be persisted. if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { needsWrite = true; } } } } if (!needsWrite) { mcr.setDiskWriteResult(false, true); return; } boolean backupFileExists = mBackupFile.exists(); if (DEBUG) { backupExistsTime = System.currentTimeMillis(); } // 此处需要注意一下 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 (DEBUG) { outputStreamCreateTime = System.currentTimeMillis(); } if (str == null) { mcr.setDiskWriteResult(false, false); return; } XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); writeTime = System.currentTimeMillis(); FileUtils.sync(str); fsyncTime = System.currentTimeMillis(); str.close(); ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); if (DEBUG) { setPermTime = System.currentTimeMillis(); } 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(); } // Writing was successful, delete the backup file if there is one. mBackupFile.delete(); if (DEBUG) { deleteTime = System.currentTimeMillis(); } mDiskStateGeneration = mcr.memoryStateGeneration; mcr.setDiskWriteResult(true, true); if (DEBUG) { Log.d(TAG, "write: " + (existsTime - startTime) + "/" + (backupExistsTime - startTime) + "/" + (outputStreamCreateTime - startTime) + "/" + (writeTime - startTime) + "/" + (fsyncTime - startTime) + "/" + (setPermTime - startTime) + "/" + (fstatTime - startTime) + "/" + (deleteTime - startTime)); } long fsyncDuration = fsyncTime - writeTime; mSyncTimes.add((int) fsyncDuration); mNumSync++; if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) { mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": "); } return; } catch (XmlPullParserException e) { Log.w(TAG, "writeToFile: Got exception:", e); } catch (IOException e) { Log.w(TAG, "writeToFile: Got exception:", e); } // Clean up an unsuccessfully written file if (mFile.exists()) { if (!mFile.delete()) { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } mcr.setDiskWriteResult(false, false);}
代码比较长,做了一些时间的记录和 XML 的相关处理,但最值得我们关注的还是其中打了标注的对于 mBackupFile
的处理。我们可以明显地看到,在我们写入文件的时候,我们会把此前的 XML 文件改名为一个备份文件,然后再将要写入的数据写入到一个新的文件中。如果这个过程执行成功的话,就会把备份文件删除。由此可见:即使我们每次只是添加一个键值对,也会重新写入整个文件的数据,这也说明了 SharedPreferences 只适合保存少量数据,文件太大会有性能问题。
看完了这个 writeToFile()
,我们再来看看下面做了啥。
// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; }}QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
可以看到,当且仅当是 commit()
并且只有一个待写入操作的时候才能直接执行到 writeToDiskRunnable.run()
,否则都会执行到 QueuedWork
的 queue()
方法,这个 QueuedWork
又是什么东西?
/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */@GuardedBy("sLock")private static final LinkedListsFinishers = new LinkedList<>();/** Work queued via {@link #queue} */@GuardedBy("sLock")private static final LinkedList sWork = new LinkedList<>();/** * Internal utility class to keep track of process-global work that's outstanding and hasn't been * finished yet. * * New work will be {@link #queue queued}. * * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}. * This is used to make sure the work has been finished. * * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for * other things in the future. * * The queued asynchronous work is performed on a separate, dedicated thread. * * @hide */public class QueuedWork { /** * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}. * * Used by SharedPreferences$Editor#startCommit(). * * Note that this doesn't actually start it running. This is just a scratch set for callers * doing async work to keep updated with what's in-flight. In the common case, caller code * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time * these Runnables are run is from {@link #waitToFinish}. * * @param finisher The runnable to add as finisher */ public static void addFinisher(Runnable finisher) { synchronized (sLock) { sFinishers.add(finisher); } } /** * Remove a previously {@link #addFinisher added} finisher-runnable. * * @param finisher The runnable to remove. */ public static void removeFinisher(Runnable finisher) { synchronized (sLock) { sFinishers.remove(finisher); } } /** * Trigger queued work to be processed immediately. The queued work is processed on a separate * thread asynchronous. While doing that run and process all finishers on this thread. The * finishers can be implemented in a way to check weather the queued work is finished. * * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive, * after Service command handling, etc. (so async work is never lost) */ public static void waitToFinish() { long startTime = System.currentTimeMillis(); boolean hadMessages = false; Handler handler = getHandler(); synchronized (sLock) { if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) { // Delayed work will be processed at processPendingWork() below handler.removeMessages(QueuedWorkHandler.MSG_RUN); if (DEBUG) { hadMessages = true; Log.d(LOG_TAG, "waiting"); } } // We should not delay any work as this might delay the finishers sCanDelay = false; } StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); try { processPendingWork(); } finally { StrictMode.setThreadPolicy(oldPolicy); } try { while (true) { Runnable finisher; synchronized (sLock) { finisher = sFinishers.poll(); } if (finisher == null) { break; } finisher.run(); } } finally { sCanDelay = true; } synchronized (sLock) { long waitTime = System.currentTimeMillis() - startTime; if (waitTime > 0 || hadMessages) { mWaitTimes.add(Long.valueOf(waitTime).intValue()); mNumWaits++; if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) { mWaitTimes.log(LOG_TAG, "waited: "); } } } } /** * Queue a work-runnable for processing asynchronously. * * @param work The new runnable to process * @param shouldDelay If the message should be delayed */ public static void queue(Runnable work, boolean shouldDelay) { Handler handler = getHandler(); synchronized (sLock) { sWork.add(work); if (shouldDelay && sCanDelay) { handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY); } else { handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); } } }}
简单地说,这个 QueuedWork
类里面有一个专门存放 Runnable
的两个 LinkedList
对象,他们分别对应未完成的操作 sFinishers
和正在工作的 sWork
。
waitToFinish()
方法中,会不断地去遍历执行未完成的 Runnable
。我们根据注释也知道了这个方法会在 Activity
的 onPause()
和 BroadcastReceiver
的 onReceive()
方法后调用。假设我们频繁的调用了 apply()
方法,并紧接着调用了 onPause()
,那么就可能会发生 onPause()
一直等待 QueuedWork.waitToFinish
执行完成而产生 ANR。也就是说,即使是调用了 apply()
方法去异步提交,也不是完全安全的。如果 apply()
方法使用不当,也是可能出现 ANR 的。 总结
说了这么多,我们当然还是需要做一个总结。
apply()
没有返回值而commit()
返回boolean
表明修改是否提交成功 ;commit()
是把内容同步提交到硬盘的,而apply()
先立即把修改提交到内存,然后开启一个异步的线程提交到硬盘,并且如果提交失败,你不会收到任何通知。- 所有
commit()
提交是同步过程,效率会比apply()
异步提交的速度慢,在不关心提交结果是否成功的情况下,优先考虑apply()
方法。 apply()
是使用异步线程写入磁盘,commit()
是同步写入磁盘。所以我们在主线程使用的commit()
的时候,需要考虑是否会出现 ANR 问题。- 我们每次添加键值对的时候,都会重新写入整个文件的数据,所以它不适合大量数据存储。
- 多线程场景下效率比较低,因为 get 操作的时候,会锁定
SharedPreferencesImpl
里面的对象,互斥其他操作,而当put
、commit()
和apply()
操作的时候都会锁住Editor
的对象,在这样的情况下,效率会降低。 - 由于每次都会把整个文件加载到内存中,因此,如果 SharedPreferences 文件过大,或者在其中的键值对是大对象的 JSON 数据则会占用大量内存,读取较慢是一方面,同时也会引发程序频繁 GC,导致的界面卡顿。
基于以上缺点:
- 建议不要存储较大数据到
SharedPreferences
,也不要把较多数据存储到同一个 name 对应的SharedPreferences
中,最好根据规则拆分为多个SharedPreferences
文件。 - 频繁修改的数据修改后统一提交,而不是修改过后马上提交。
- 在跨进程通讯中不去使用
SharedPreferences
。 - 获取
SharedPreferences
对象的时候会读取SharedPreferences
文件,如果文件没有读取完,就执行了 get 和 put 操作,可能会出现需要等待的情况,因此最好提前获取SharedPreferences
对象。 - 每次调用
edit()
方法都会创建一个新的EditorImpl
对象,不要频繁调用edit()
方法。 参考链接: