[Android source code learning] SharedPreferences source code learning

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

functionMMKVJetpack DataStoreSharedPreferences
Whether to block the main threadnonoyes
Thread safeyesyesyes
Is cross process supportedyesnono
Do you support protocol buffersyesyesno
Is it type safenoyesno
Can you monitor data changesnoyesyes

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

Keywords: Java Android Android Studio

Added by supratwinturbo on Sat, 12 Feb 2022 16:48:35 +0200