SharedPreferences源码解析(sharedpreferences使用方法)
简介
SharedPreferences(简称SP)是Android中很常用的数据存储方式,SP采用key-value(键值对)形式, 但不建议使用SP 来存储大规模的数据
, 可能会降低性能.
问题
为什么不要存放大的key和value,会引起界面卡?
apply和commit的区别是啥?
写入磁盘是增量还是全量?
使用
private void putValue() { SharedPreferences sharedPreferences = getSharedPreferences("gityuan", Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString("blog", "www.gityuan.com"); editor.putInt("years", 3); editor.commit(); } private String getValue() { SharedPreferences sharedPreferences = getSharedPreferences("gityuan", Context.MODE_PRIVATE); return sharedPreferences.getString("blog", "null"); } 复制代码
源码分析
获取SharedPreferences
ContextImpl.java
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<>(); } (1)获取当面名字的File file = mSharedPrefsPaths.get(name); if (file == null) { (2)如果没找到就创建一个文件 file = getSharedPreferencesPath(name); (3)存到Map内存中 mSharedPrefsPaths.put(name, file); } } (4)看下这个方法。 return getSharedPreferences(file, mode); } @Override public File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); } 复制代码
@Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { final ArrayMap<File, SharedPreferencesImpl> cache = (1)看下这个方法 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"); } } (2)创建SharedPreferencesImpl。看下构造方法里面做了啥。 sp = new SharedPreferencesImpl(file, mode); (3)放到Map里面 cache.put(file, sp); return sp; } } (4)指定多进程模式, 则当文件被其他进程改变时,则会重新加载,多进程不做重点。 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; } @GuardedBy("ContextImpl.class") private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() { if (sSharedPrefsCache == null) { sSharedPrefsCache = new ArrayMap<>(); } final String packageName = getPackageName(); (1)获取当前包名的Map<File, SharedPreferencesImpl> ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName); if (packagePrefs == null) { packagePrefs = new ArrayMap<>(); sSharedPrefsCache.put(packageName, packagePrefs); } return packagePrefs; } 复制代码
每一个App对应多个(name -> File -> SharedPreferencesImpl)。
SharedPreferencesImpl初始化
@UnsupportedAppUsage SharedPreferencesImpl(File file, int mode) { mFile = file; (1)创建备份文件 mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; (2)加载磁盘 startLoadFromDisk(); } 复制代码
private void startLoadFromDisk() { synchronized (mLock) { (1)标记位是否加载到内存 mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); } 复制代码
private void loadFromDisk() { synchronized (mLock) { if (mLoaded) { return; } (1)备份文件存在就把备份文件的内存写到File中 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); 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;(1)读取到内容放到内存中 mStatTimestamp = stat.st_mtim;(2)跟新修改时间 mStatSize = stat.st_size;(3)更新文件大小 } 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();(4)唤醒处于等待状态的线程 } } } 复制代码
获取数据
@Nullable public String getString(String key, @Nullable String defValue) { synchronized (mLock) { (1)等待磁盘加载到内存成功 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); } } 复制代码
获取数据会一直等主线程完成磁盘数据加载到内存中。
Editor
SharedPreferences.Editor editor = sharedPreferences.edit();
@Override public Editor edit() { (1)等待读取到内存 synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl(); } 复制代码
public final class EditorImpl implements Editor { private final Map<String, Object> mModified = Maps.newHashMap(); private boolean mClear = false; //插入数据 public Editor putString(String key, @Nullable String value) { synchronized (this) { //插入数据, 先暂存到mModified对象 mModified.put(key, value); return this; } } //移除数据 public Editor remove(String key) { synchronized (this) { mModified.put(key, this); return this; } } //清空全部数据 public Editor clear() { synchronized (this) { mClear = true; return this; } } } 复制代码
去了Commit和Apply都只是把数据先存入内存中。
数据提交
@Override public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } (1)将数据更新到内存 MemoryCommitResult mcr = commitToMemory(); (2)将内存数据同步到文件 SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { (3)进入等待状态,直到写入文件的完成 mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } finally { if (DEBUG) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " committed after " + (System.currentTimeMillis() - startTime) + " ms"); } } (4)通知监听者,并在主线程回调onSharedPreferenceChanged()方法。 notifyListeners(mcr); (5)返回写入磁盘的结果 return mcr.writeToDiskResult; } 复制代码
// Returns true if any changes were made private MemoryCommitResult commitToMemory() { long memoryStateGeneration; boolean keysCleared = false; List<String> keysModified = null; Set<OnSharedPreferenceChangeListener> listeners = null; Map<String, Object> 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<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; (1)当mClear为true, 则直接清空mMap if (mClear) { 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(); // "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. (2)注意此处的this是个特殊值, 用于移除相应的key操作. 如果value为null就移除相应的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); } (3)数据有改变的标志位 changesMade = true; if (hasListeners) { (4)记录发生改变的Key。 keysModified.add(k); } } (5)清空EditorImpl中的mModified数据 mModified.clear(); if (changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; } } return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified, listeners, mapToWriteToDisk); } 复制代码
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { (1)同步写入还是异步写入 final boolean isFromSyncCommit = (postWriteRunnable == null); final Runnable writeToDiskRunnable = new Runnable() { @Override public void run() { synchronized (mWritingToDiskLock) { (2)写入文件 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) { (3)写入内存加1,写入磁盘减1 wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); } 复制代码
@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) { (1)commit会需要写入 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; } } } } (2)不需要写入会立刻返回写入成功 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; } (5)写入磁盘 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; (3)写入成功 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); } } (4)写入失败 mcr.setDiskWriteResult(false, false); } 复制代码
数据有改变才会写入磁盘。
写入磁盘。
public static final void writeMapXml(Map val, XmlSerializer out, WriteMapCallback callback) throws XmlPullParserException, java.io.IOException { if (val == null) { return; } Set s = val.entrySet(); Iterator i = s.iterator(); while (i.hasNext()) { Map.Entry e = (Map.Entry)i.next(); writeValueXml(e.getValue(), (String)e.getKey(), out, callback); } } 复制代码
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"); } } }; (1)将awaitCommit添加到QueuedWork 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); } 复制代码
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; } } (1)apply会走到这里 QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); } 复制代码
public static void queue(Runnable work, boolean shouldDelay) { (1)看下Handler Handler handler = getHandler(); synchronized (sLock) { sWork.add(work); (2)apply正常情况下发送了一个延迟消息。(去除Activity关闭) 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) { (1)起了一个子线程去处理消息 HandlerThread handlerThread = new HandlerThread("queued-work-looper", Process.THREAD_PRIORITY_FOREGROUND); handlerThread.start(); sHandler = new QueuedWorkHandler(handlerThread.getLooper()); } return sHandler; } } 复制代码
总结:
为什么不要存放大的key和value,会引起界面卡?
apply和commit的区别是啥?
写入磁盘是增量还是全量?
第一个问题:
第一次getSharedPreferences
的时候会在主线程读取磁盘文件。文件太大,会导致主线程卡顿。
第二个问题:
apply()以异步的方式写入磁盘,而commit()是以同步的方式写入磁盘。但是当组件(Activity Service BroadCastReceiver)这些系统组件特定状态转换的时候,会把QueuedWork中未完成的那些磁盘写入操作放在主线程执行,apply()这个时候就是同步写入了。
apply没有返回值, commit有返回值能知道修改是否提交成功。
第三个问题:
是全量写入的。可以看下源码分析的写入磁盘
部分代码。
总结:
里面用了大量的锁,效率肯定是低的。
作者:中玉
链接:https://juejin.cn/post/7062720610991865893