preface
The day before, my friend asked me the following questions:
1. What is the difference between Apply and commit? Does it block the main thread?
2. Is the process safe? Why?
3, to achieve process security, how to design?
Source code environment: API 28 Andorid 9.0
directory
Load and initialize
First look at the loading of SharedPreferences
ContextImpl.java
@Override public SharedPreferences getSharedPreferences(String name, int mode) { 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); // Generate name. XML file mSharedPrefsPaths. Put (name, file) under shared_prefs; } } return getSharedPreferences(file, mode); / / 1}Copy the code
Here you map file names and files to mSharedPreferences, and look at the comment method at the next step
@Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) { final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); //1 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) { sp.startReloadIfChangedUnexpectedly(); } return sp; }Copy the code
The above code comments 1 according to the package name to obtain from sSharedPrefsCache ArrayMap < File, SharedPreferencesImpl >, then according to the documents to obtain the corresponding SharedPreferencesImpl instance, if not, The mode is checked (starting with Android N, MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE is no longer supported) and a SharedPreferencesImpl instance is created based on the File and mode.
SharedPreferencesImpl.java
SharedPreferencesImpl(File File, int mode) {mFile = File; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; startLoadFromDisk(); } //2. private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); } //3 private void loadFromDisk() { synchronized (mLock) { if (mLoaded) { return; If (mbackupfile.exists ()) {mfile.delete ();} // 4\. 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); . } finally { mLock.notifyAll(); }}}Copy the code
As you can see, the SharedPreferencesImpl constructor starts a thread to parse the XML file. Look at the method in note 3 and store the parse XML text contents in map memory with key-value pairs.
The process is as follows: Start a thread to read disk files and parse them into a map.
Commit and apply methods
1. Commit method
Commit and apply are implemented using the EditorImpl class of SharedPreferences
@Override public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } MemoryCommitResult mcr = commitToMemory(); / / comment 1 SharedPreferencesImpl. Enclosing enqueueDiskWrite (MCR, null / * sync write on this thread okay * /); 2 try {/ / comment / / comment here 2 internal call CountDownLatch awiat wait for method, only after the written documents will release MCR. WrittenToDiskLatch. Await (); } catch (InterruptedException e) { return false; } finally { ...... } notifyListeners(mcr); return mcr.writeToDiskResult; }Copy the code
Follow the code in comments 1 and 2, see the comments after the code for detailed explanation
commitToMemory()
private MemoryCommitResult commitToMemory() { long memoryStateGeneration; 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; // 1. Reference the loaded map to mapToWriteToDisk mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { keysModified = new ArrayList<String>(); listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (mEditorLock) { //2. Boolean changesMade = false; if (mClear) { if (! mapToWriteToDisk.isEmpty()) { changesMade = true; mapToWriteToDisk.clear(); } mClear = false; } for (Map.Entry<String, Object> e : mModified.entrySet()) { // 3. Every put value is placed in the mModified Map. String K = LLDB etKey(); Object v = e.getValue(); If (n = = this | | v = = null) {/ / 4 \. Value = = this (Editor) or value = = null current data does not conform to the requirements the if (! mapToWriteToDisk.containsKey(k)) { continue; } mapToWriteToDisk.remove(k); } else {//6. If you have this key, if the value is the same, then it's not added to mapToWriteToDisk, If not to join the if (mapToWriteToDisk. Either containsKey (k)) {Object existingValue = mapToWriteToDisk. Get (k); if (existingValue ! = null && existingValue.equals(v)) { continue; } } mapToWriteToDisk.put(k, v); } //7\ for loop is gone, mapToWriteToDisk has been modified, adding or deleting the value changesMade = true; if (hasListeners) { keysModified.add(k); }} //8. Delete mModified.clear(); If (changesMade) {/ / 9. The current memory state value + 1 mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; Return new MemoryCommitResult(memoryStateGeneration, keysModified, Listeners, mapToWriteToDisk); }Copy the code
EnqueueDiskWrite (MCR, NULL) Note that the second argument is passed null
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { //1\. PostWriteRunnable is null, so isFromSysnCommit is true. Final Boolean isFromSyncCommit = indicates synchronous operation (postWriteRunnable == null); Final Runnable writeToDiskRunnable = new Runnable() {@override public void run() {// 2\. Synchronized (mWritingToDiskLock) {// ==== later analysis ===== writeToFile(MCR, isFromSyncCommit); } synchronized (mLock) { mDiskWritesInFlight--; } if (postWriteRunnable ! = null) { postWriteRunnable.run(); }}}; If (isFromSyncCommit) {Boolean wasEmpty = false; Synchronized (mLock) {//3\. Number of write operations being performed == 1 wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; Queuedwork. queue(writeToDiskRunnable,! isFromSyncCommit); }Copy the code
QueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit);
public static void queue(Runnable work, boolean shouldDelay) { Handler handler = getHandler(); Synchronized (sLock) {//1. Join LinkedList sWork; If (shouldDelay &&scandelay) {if (shouldDelay &&scandelay) { handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY); } else { handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); }}}Copy the code
The handler above is of type QueuedWorkHandler (the handler created by the HandlerThread), which handles the logic in its handleMessage and has only one method, processPendingWork()
private static void processPendingWork() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } synchronized (sProcessingWork) { LinkedList<Runnable> work; Work = (LinkedList<Runnable>) swork.clone (); sWork.clear(); // Remove all msg-s as all work will be processed now getHandler().removeMessages(QueuedWorkHandler.MSG_RUN); } if (work.size() > 0) {//2\. } if (DEBUG) { Log.d(LOG_TAG, "processing " + work.size() + " items took " + +(System.currentTimeMillis() - startTime) + " ms"); }}}}Copy the code
At this point, the commit analysis is almost complete, leaving only one write file operation, the writeToFile(MCR, isFromSyncCommit) method called in the run method when Runnable is built
@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; boolean fileExists = mFile.exists(); . If (fileExists) {Boolean needsWrite = false; / / 2. This is written to the file to be submitted to judge the 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) {//3. Return McR.setdiskwriteresult (false, true) without writing; return; } //4\. At the beginning of SP loading, a backup file was created from the original file, and then the original file was deleted. Boolean backupFileExists = mBackupfile.exists (); . //5\. Backup file does not exist if (! backupFileExists) { if (! mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); mcr.setDiskWriteResult(false, false); return; }} else {//6\. Delete mfile.delete (); }} //7\. Re-write the content to the original file, set the Mode permission, 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); . 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); }Copy the code
To summarize, the COMMIT operation first builds the MemoryCommitResult object, synchronizes the edited results to memory, and then writes the results to a disk file. In the process of file writing, CountDownLatch will be used to block and wait until the file is written successfully. The backup file will be deleted after the file is written successfully. When the same SP instance submits data again, the backup file name will be renamed. If the write fails, the original file will be deleted. As you can see, data commit rewrites the entire file data. (The backup file is used for the next recovery of data, see SP constructor example)
2, apply method
@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) { ....... }}; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); }}; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); }Copy the code
Just like commit, apply does not. Return value We look directly at the enqueueDiskWrite method, this time the second argument is postWriteRunnable, which is not null. QueuedWorkHandler (default: 100ms) is used to prevent frequent writes, and QueuedWorkHandler is created by HandlerThread. Other differences are ignored.
Here’s another look at the QueuedWork.addFinisher(awaitCommit) method in the code above
public static void addFinisher(Runnable finisher) { synchronized (sLock) { sFinishers.add(finisher); }}Copy the code
It places the awaitCommit in sFinishers and removes it after execution. Let’s look at one more piece of code
/** * 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() { ...... try { while (true) { Runnable finisher; synchronized (sLock) { finisher = sFinishers.poll(); } if (finisher == null) { break; } finisher.run(); } } finally { sCanDelay = true; }... }Copy the code
If you look at the comments on this source code, The frame rack makes sure that it is done before switching states and that disk writes that are being performed using apply () are Activiy’s As you can see from calling waitToFinish before onPause(), BroadcastReceiver’s onReceive(), and Service’s onStartCommand() methods.
3. Compare apply and commit
In my opinion, the main difference is that for commit with Apply, a message will be sent 100ms later to avoid frequent disk writes, while for commit, a message will be sent directly with Handler. Notice that this is the same Handler, and that it’s all writing to a file in a child thread of a different calling thread.
4, About get, PUT data
Let’s go straight to the getString and putString methods, everything else is similar
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
Copy the code
The lock will be acquired by mLock and will wait until the XML has been parsed, or writing-related operations will queue up to acquire or release the lock, ensuring data correctness. A putString is similar, except it’s not a lock on the same object.
@Override public Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this; }}Copy the code
Third, summary
1. What is the difference between Apply and commit? Does it block the main thread?
For commit applications, a message is sent 100ms late to avoid frequent disk writes. For commit applications, a message is sent directly by a Handler. Notice that this is the same Handler, and that it’s all writing to a file in a child thread of a different calling thread.
However, both methods actually block the thread. Submitting data involves calling the await of CountDownLatch. The downLatch method is called only after the file has been successfully written, so this is blocking off the shelf. And there’s one in the Apply method
It is recommended to use Apply, which designs to re-write data to a file every time it is written. Apply has a latency of 100ms to avoid frequent writes.
2. Is the process safe? Why?
SharedPreferences are thread-safe, of course, as you can see from the amount of synchronized used to ensure data correctness.
Process but it is not safe, the same file, A process that is being read, is writing B process, process load SP B will be the backup file rename the file name, A process that read, read before data can be written to the original file in memory, B write the content of the other processes in the operating process cannot obtain.
3, to achieve process security, how to design?
For SP that requires process security, MMKV is preferred
Comparison of MMKV and SharedPreferences and MMKV summary of Android storage optimization
MMKV principle
Of course, there are other solutions, which are arguably not as good as MMKV (easy access), for example
-
The SP is wrapped with the ContentProvider, and the process accesses the SP through the ContentProvider.
-
Using broadcast to achieve state synchronization, but the immediacy is not good
-
Socket: Each process needs to maintain a Socket, which ensures data security and is difficult to use.
-
File + file lock format