Chapter 1: SharedPreferences source code learning
Defects in Android SharedPreferences
- Cause ANR
- Cause Caton
- All read and write to memory
- Data loss
- Cannot cross process
Comparison of MMKV, Jetpack DataStore and SharedPreferences
function | MMKV | Jetpack DataStore | SharedPreferences |
---|---|---|---|
Whether to block the main thread | no | no | yes |
Thread safe | yes | yes | yes |
Is cross process supported | yes | no | no |
Do you support protocol buffers | yes | yes | no |
Is it type safe | no | yes | no |
Can you monitor data changes | no | yes | yes |
DataStore: stability
MMKV: efficiency
SharedPreferences source code reading
Question:
1. What classes are involved in SP and the relationship between classes?
android.content.SharedPreferences android.content.SharedPreferences.Editor android.content.SharedPreferences.OnSharedPreferenceChangeListener android.app.SharedPreferencesImpl android.content.Context android.content.ContextWrapper android.app.ContextImpl
[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-hoteale7-1644676177858) (E: \ advanced Android Development: 80 sets of framework source code analysis, in-depth analysis of Android underlying principles \ Chapter 1: K-V data persistence \ SharedPreferences class diagram. png)]
SharedPreferences function
- Read the value of key value pair
getAll getString getStringSet getInt getLong getFloat getBoolean contains
- Get SharedPreferences Editor object, SharedPreferences Editor to write key values
Editor edit();
- Register and unregister listeners for SP content changes
registerOnSharedPreferenceChangeListener unregisterOnSharedPreferenceChangeListener
SharedPreferences.Editor function
- It provides interfaces for writing, repairing and deleting the values of key value pairs
putString putStringSet putInt putLong putFloat putBoolean remove clear
- Provides an interface to write the modified value to the file
commit Synchronous write apply Asynchronous write
OnSharedPreferenceChangeListener function
- Listener for SP content change
onSharedPreferenceChanged
SharedPreferencesImpl
Implementation class of SharedPreferences
Get SP Context
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);
Proxy class for ContextWrapper Context
Implementation class of ContextImpl Context
Implement the getSharedPreferences method.
@Override 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); }
2. How do I create an SP?
Simple use of SP
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putString("test", "test111"); editor.putString("All", "All"); editor.commit();
View the SP's xml file.
![img](file:///C:\Users\navy\AppData\Roaming\Tencent\Users\897304074\TIM\WinTemp\RichOle\ASLCEXEKaTeX parse error: Expected 'EOF', got '}' at position 7: F41WPZ}̲CIZ~U]X.png)
Get the SharedPreferences object through the ContextWrapper.
Context mBase; public ContextWrapper(Context base) { mBase = base; } @Override public SharedPreferences getSharedPreferences(String name, int mode) { return mBase.getSharedPreferences(name, mode); }
Activity, ContextWrapper, ContextImpl, Context association
ContextImpl create getSharedPreferences
@Override 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); }
ContextImpl create SharedPreferencesImpl
@Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { // Get cache final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); sp = cache.get(file); if (sp == null) { // Check mode 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"); } } // Create SharedPreferencesImpl 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; }
SharedPreferencesImpl initialization
SharedPreferencesImpl(File file, int mode) { mFile = file; // Create rollback file mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; startLoadFromDisk(); }
3. How does the SP load files?
SharedPreferencesImpl starts the thread to load the file
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void startLoadFromDisk() { synchronized (mLock) { // Object lock added, setting not loaded mLoaded = false; } // Open thread load file new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); }
SharedPreferencesImpl load file
private void loadFromDisk() { // synchronized (mLock) { if (mLoaded) { return; } // File rollback 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()) { // Use BufferedInputStream and FileInputStream to read files BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16 * 1024); // Parsing XML 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(); } } }
4. How does the SP save data to a file?
Get Editor object
@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. // Wait for the file to load synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl(); }
put operation
@Override public Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { // Map < string, Object > Mmodified value to be modified mModified.put(key, value); return this; } }
Perform a commit operation and synchronously write to memory
@Override public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } // Modify SharedPreferencesImpl mMap object 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"); } } // Notification value changed notifyListeners(mcr); return mcr.writeToDiskResult; }
Write to memory commitToMemory
Get the K-V mapToWriteToDisk to write to.
- Initialize mapToWriteToDisk with mMap
- Fill the K-V of mModified into mapToWriteToDisk
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. // There is a write conflict if (mDiskWritesInFlight > 0) { // We can't modify our mMap as a currently // in-flight write owns it. Clone it before // modifying it. // noinspection unchecked // Copy the current unmodified mMap 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) { if (!mapToWriteToDisk.isEmpty()) { changesMade = true; mapToWriteToDisk.clear(); } keysCleared = true; mClear = false; } // Facilitate the mModified Map to be modified, 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. // When the key is removed, v is set to null 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, keysCleared, keysModified, listeners, mapToWriteToDisk); }
private static class MemoryCommitResult { final long memoryStateGeneration; final boolean keysCleared; @Nullable final List<String> keysModified; @Nullable final Set<OnSharedPreferenceChangeListener> listeners; final Map<String, Object> mapToWriteToDisk; final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); @GuardedBy("mWritingToDiskLock") volatile boolean writeToDiskResult = false; boolean wasWritten = false;
Put into disk write queue sharedpreferencesimpl this. enqueueDiskWrite
SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */);
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { // Is it a synchronous commit final boolean isFromSyncCommit = (postWriteRunnable == null); final Runnable writeToDiskRunnable = new Runnable() { @Override public void run() { synchronized (mWritingToDiskLock) { // write file 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; } } // Put it into the QueuedWork queue and wait for execution QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }
QueuedWork
/** {@link #getHandler() Lazily} created handler */ @GuardedBy("sLock") private static Handler sHandler = null; // handle of HandlerThread /** Work queued via {@link #queue} */ @GuardedBy("sLock") private static LinkedList<Runnable> sWork = new LinkedList<>(); // Add to work 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); } } } private static class QueuedWorkHandler extends Handler { static final int MSG_RUN = 1; QueuedWorkHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { if (msg.what == MSG_RUN) { // Execution message processPendingWork(); } } }
Write content to file
@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(); } // fileExists, backup file does not exist, rename fileExists to mBackupFile 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 { // Create FileOutputStream FileOutputStream str = createFileOutputStream(mFile); if (DEBUG) { outputStreamCreateTime = System.currentTimeMillis(); } if (str == null) { mcr.setDiskWriteResult(false, false); return; } // Write mapToWriteToDisk to xml file 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; // Set wasWritten to true and result to true 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); }
Supplement: apply process
@Override public void apply() { final long startTime = System.currentTimeMillis(); // Write memory 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); }
5. How does SP complete data serialization?
Traditional IO read / write + XML parsing
IO Technology
BufferedInputStream,FileInputStream read FileOutputStream write
XML parsing
XmlUtils.readMapXml(str) mlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
6. How does sp read?
SharedPreferencesImpl
@Override @Nullable public String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
7. How does SP ensure thread safety?
private final Object mLock = new Object();
Error loading file
Obtain the mLock lock of SharedPreferencesImpl first, and the setting is not loaded
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void startLoadFromDisk() { synchronized (mLock) { // Object lock added, setting not loaded mLoaded = false; } // Open thread load file new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); }
Non main thread load file
private void loadFromDisk() { // First obtain the mLock lock of SharedPreferencesImpl and rename the backup file to mFile synchronized (mLock) { if (mLoaded) { return; } // File rollback if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } } ``` // File contents loaded ``` // Then obtain the mLock lock of SharedPreferencesImpl and set mLoaded to true, indicating that the load is successful. And notify others 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 { // Wake up all threads waiting on the object. mLock.notifyAll(); } } }
When reading content
Obtain the mLock lock of SharedPreferencesImpl first, and wait for the file to load before reading.
@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 { // If the load is not successful, the current thread will enter the waiting state mLock.wait(); } catch (InterruptedException unused) { } } if (mThrowable != null) { throw new IllegalStateException(mThrowable); } }
When writing content
When getting Editor objects
Obtain the mLock lock of SharedPreferencesImpl and wait for the file to load before creating the EditorImpl object
@Override public Editor edit() { // synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl(); }
When EditorImpl performs a put operation.
Do not modify the mMap of SharedPreferencesImpl. Store the value to be modified in mModified without affecting the reading of SP. Use mEditorLock to ensure thread safety of put.
public final class EditorImpl implements Editor { private final Object mEditorLock = new Object(); @Override public Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { // Map < string, Object > Mmodified value to be modified mModified.put(key, value); return this; } }
commit/apply, when writing to memory committomory.
Get sharedpreferencesimpl first this. Mlock lock ensures that get and put operations are not affected. Get the value of mMap.
Obtain the EditorImpl mEditorLock lock to ensure that the value of mModified is correct.
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) { 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. 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, keysCleared, keysModified, listeners, mapToWriteToDisk); }
During commit
MemoryCommitResult uses CountDownLatch to ensure that the setDiskWriteResult method is called only once.
private static class MemoryCommitResult { final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); void setDiskWriteResult(boolean wasWritten, boolean result) { this.wasWritten = wasWritten; writeToDiskResult = result; writtenToDiskLatch.countDown(); }
@Override public boolean commit() { 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; }
Put into disk write queue sharedpreferencesimpl this. Enqueuediskwrite error
Get mLock and modify the mDiskWritesInFlight value.
- mDiskWritesInFlight + + indicates the number of times memory
- mDiskWritesInFlight -- indicates write to disk
isFromSyncCommi
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) { // When the commit is called only once, directly execute writeToDiskRunnable writeToDiskRunnable.run(); return; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }
When writeToFile, call setDiskWriteResult to modify the result
@GuardedBy("mWritingToDiskLock") private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
summary
- Use technologies synchronized, [notifyAll, wait], CountDownLatch (countDown, await) and distributed related technologies
- Use mLock to ensure the value security of SP objects, such as mLoaded and mMap
@GuardedBy("mLock") private Map<String, Object> mMap; @GuardedBy("mLock") private Throwable mThrowable; @GuardedBy("mLock") private int mDiskWritesInFlight = 0; @GuardedBy("mLock") private boolean mLoaded = false; @GuardedBy("mLock") private StructTimespec mStatTimestamp; @GuardedBy("mLock") private long mStatSize; @GuardedBy("mLock") private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
- Use mWritingToDiskLock to ensure the correctness of SP disk related data
/** Latest memory state that was committed to disk */ @GuardedBy("mWritingToDiskLock") private long mDiskStateGeneration; /** Time (and number of instances) of file-system sync requests */ @GuardedBy("mWritingToDiskLock") private final ExponentiallyBucketedHistogram mSyncTimes = new ExponentiallyBucketedHistogram(16);
- The correctness of the editor is guaranteed
@GuardedBy("mEditorLock") private final Map<String, Object> mModified = new HashMap<>(); @GuardedBy("mEditorLock") private boolean mClear = false;
-
mLock.wait() waits for the file to load, mlock Notifyall() notifies file loading
-
Using mWritingToDiskLock, you can only write to the disk once you save.
-
Use CountDownLatch to ensure that the setDiskWriteResult method is called only once.
8. How does SP monitor data changes?
Register and add listeners
@GuardedBy("mLock") private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>(); @Override public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized(mLock) { mListeners.put(listener, CONTENT); } } @Override public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized(mLock) { mListeners.remove(listener); } }
Error writing to memory. Get listeners collection
// Returns true if any changes were made private MemoryCommitResult commitToMemory() { Set<OnSharedPreferenceChangeListener> listeners = null; synchronized (SharedPreferencesImpl.this.mLock) { boolean hasListeners = mListeners.size() > 0; if (hasListeners) { keysModified = new ArrayList<String>(); listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } } return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified, listeners, mapToWriteToDisk); }
When commit/apply, notify the listener of the change
@Override public boolean commit() { ... notifyListeners(mcr); return mcr.writeToDiskResult; }
Notify listeners of changes
private void notifyListeners(final MemoryCommitResult mcr) { if (mcr.listeners == null || (mcr.keysModified == null && !mcr.keysCleared)) { return; } if (Looper.myLooper() == Looper.getMainLooper()) { if (mcr.keysCleared && Compatibility.isChangeEnabled(CALLBACK_ON_CLEAR_CHANGE)) { for (OnSharedPreferenceChangeListener listener : mcr.listeners) { if (listener != null) { listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, null); } } } for (int i = mcr.keysModified.size() - 1; i >= 0; i--) { final String key = mcr.keysModified.get(i); for (OnSharedPreferenceChangeListener listener : mcr.listeners) { if (listener != null) { listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key); } } } } else { // Run this function on the main thread. ActivityThread.sMainThreadHandler.post(() -> notifyListeners(mcr)); } } }
9. Why does SP write on a large scale, inefficient and time-consuming?
- SP must load all file contents into memory before reading and writing. If the file content is very large, it is very time-consuming.
- SP adopts traditional IO mode to read and write files. Low efficiency
- SP uses XMl for serialization, which is inefficient.
10. Why does SP cause data loss?
- SP apply adopts asynchronous operation. When the asynchronous operation is not completed, the program crashes, resulting in data loss.
- When SP writeToFile, if an exception occurs, the current file will be deleted.
@GuardedBy("mWritingToDiskLock") private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) { ..... } 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); }
11. Precautions for SP use.
-
When obtaining the SP object, the file may not be loaded completely. If the read-write operation is performed immediately, the main thread will be blocked.
-
When the edit() method is called, a new EditorImpl is created. Do not create too many EditorImpl objects, which may cause memory jitter.
-
Do not call commit and apply frequently to enter the disk operation. Commit blocks the main thread. During commit and apply, enqueueDiskWrite method and QueuedWork will be called Queue (writetodiskrunnable,! Isfromsynccommit), put the write to disk task into QueuedWork. The ActivityThread will call the waitto finish method.
Waitto finish calls processPendingWork. processPendingWork traverses and executes the sWork (LinkedList).
When there are too many writetodiskrunnables or writetodiskrunnables consume too much time, it will cause the main line to jam, affect the startup speed, and even cause ANR.
[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-refulmmq-1644676177863) (C: \ users \ Navy \ appdata \ roaming \ typora \ typora user images \ image-20220212201214044. PNG)]
public static void waitToFinish() { try { processPendingWork(); } finally { StrictMode.setThreadPolicy(oldPolicy); } }
private static void processPendingWork() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } synchronized (sProcessingWork) { LinkedList<Runnable> work; synchronized (sLock) { work = sWork; sWork = new LinkedList<>(); // Remove all msg-s as all work will be processed now getHandler().removeMessages(QueuedWorkHandler.MSG_RUN); } if (work.size() > 0) { for (Runnable w : work) { w.run(); } if (DEBUG) { Log.d(LOG_TAG, "processing " + work.size() + " items took " + +(System.currentTimeMillis() - startTime) + " ms"); } } } }
12. How can sp cause ANR?
Refer to the precautions for SP use.
13. Will an exception be caused when the SP name is empty?
can't.
-
When the version number of the transmitted name is less than 19, the name will become a "null" string.
@Override
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);
}
-
When getting SharedPreferencesPath path path, if name is null, name + ".xml" will become null xml
ContextImpl
@Override public File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); }
14. Mode is not supported when the SP version is greater than or equal to 24 N_ WORLD_ READABLE,MODE_WORLD_WRITEABLE mode
MODE_WORLD_READABLE cross application read
MODE_WORLD_WRITEABLE cross application write
private void checkMode(int mode) { if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) { if ((mode & MODE_WORLD_READABLE) != 0) { throw new SecurityException("MODE_WORLD_READABLE no longer supported"); } if ((mode & MODE_WORLD_WRITEABLE) != 0) { throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported"); } } }
15. How does contextimpl cache?
ContextImpl obtains the SP and caches the SP according to the package name and File. If present, you do not need to create an sp.
class ContextImpl extends Context { // packageName [file, SharedPreferencesImpl] private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache; @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; }
16. How does SP support file backup?
Initialize backup file
@UnsupportedAppUsage SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file);
static File makeBackupFile(File prefsFile) { return new File(prefsFile.getPath() + ".bak"); }
When loading a file, if the backup file exists. Delete the current file and rename the backup file to the current file
private void loadFromDisk() { synchronized (mLock) { if (mLoaded) { return; } if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } }
When writing to disk
If the backup file does not exist, rename the current file to the backup file. When the write is successful, delete the backup file.
@GuardedBy("mWritingToDiskLock") private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) { // Rename the current file so it may be used as a backup during the next read if (fileExists) { boolean needsWrite = false; 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(); } } // Writing was successful, delete the backup file if there is one. mBackupFile.delete();
17. How does SP solve memory leakage?
@GuardedBy("mLock") private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
***
SP optimization point
- Traditional IO operations using NIO and mmap codes
- Use protocol buffer instead of xml
SP architecture summary
[the external chain image transfer fails, and the source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-roxzrz3l-1644676177864) (E: \ advanced Android Development: 80 sets of framework source code analysis, in-depth analysis of Android underlying principles \ Chapter 1: K-V data persistence \ SharedPreferences class diagram. png)]
- In the principle of single responsibility, read and write are separated, and memory write and disk write are separated.
SharedPreferencesImpl assisted read. Editor is responsible for temporary memory write.
- ContextWrapper, ContextImp, Context decorator mode