preface

I recently suffered from this problem while working on a legacy project, which was mainly caused by the occasional data corruption or even loss of the SharedPreferences profile. After investigation, it was found to be a multi-process problem. There are two different processes in the project that frequently read and write to the SharedPreferences file, resulting in data corruption and loss. Take this opportunity to read a review of SharedPreferences source code, the following to say SharedPreferences are what slots.

The source code parsing

Using SharedPreferences is simple and won’t be demonstrated here. Below according to get SharedPreference, getXXX() to get data and putXXX() to store data these three aspects to read the source code.

1. Access to the SharedPreferences

1.1 getDefaultSharedPreferences ()

Normally we will pass the PreferenceManager getDefaultSharedPreferences () method to get the default SharedPreferences object, its code is shown below:

> PreferenceManager.java 

/** * Get the default SharedPreferences object named packageName_preferences and mode MODE_PRIVATE */
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());  / / see 1.2
}
Copy the code

The default full path of the SP file is /data/data/shared_prefs/[packageName]_preferences. XML. The default mode is MODE_PRIVATE. This mode is only used now. The getSharedPreferences() method of ContextImpl is called.

GetSharedPreferences (String name, int Mode)

> ContextImpl.java

@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<>();
        }
        // First check the existence of the sp file from cache mSharedPrefsPaths
        file = mSharedPrefsPaths.get(name);
        if (file == null) { // If it does not exist, create a new sp file named "name.xml"file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); }}return getSharedPreferences(file, mode); / / see 1.3
}
Copy the code

First, there is a variable called mSharedPrefsPaths. Look for its definition:

/** * The file name is key and the specific file name is value. Store all SP files * protected by contextimpl.class lock */
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;
Copy the code

MSharedPrefsPaths is an ArrayMap that caches the mapping between file names and sp files. First, the system checks whether the sp file exists in the cache according to the file name in the parameter. If not, a new file named [name].xml is created and stored in cache mSharedPrefsPaths. Finally, another overloaded getSharedPreferences() method is called with File.

1.3 getSharedPreferences(File file, int mode)

> ContextImpl.java

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); / / see 1.3.1
        sp = cache.get(file); // Try to get sp from cache first
        if (sp == null) { // If the cache fails to be retrieved
            checkMode(mode); // Check mode, see 1.3.2
            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); // Create SharedPreferencesImpl, see 1.4
            cache.put(file, sp);
            returnsp; }}// If mode is MODE_MULTI_PROCESS, the file may be modified by other processes
    // Obviously this is not sufficient to ensure cross-process security
    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;
}
Copy the code

SharedPreferences is just the interface, and what we want to get is actually its implementation class, SharedPreferences. Through getSharedPreferencesCacheLocked () method can obtain has SharedPreferencesImpl object and its sp file cache.

1.3.1 getSharedPreferencesCacheLocked ()
> ContextImpl.java

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked(a) {
    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;
}
Copy the code

SSharedPrefsCache is a nested ArrayMap, defined as follows:

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
Copy the code

Take the package name key and an ArrayMap that stores the SP file and its SharedPreferencesImp object as value. If there is a direct return, instead create a new ArrayMap as the value and store it in the cache.

1.3.2 checkMode ()
> ContextImpl.java

private void checkMode(int mode) {
    // Starting from N, if MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE are used, throw an exception directly
    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"); }}}Copy the code

Starting with Android N, MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE are explicitly no longer supported, plus MODE_MULTI_PROCESS is not thread-safe. MODE_PRIVATE usually works.

1.4 SharedPreferencesImpl

If there is no corresponding SharedPreferencesImpl object in the cache, you have to create your own. Take a look at its constructor:

SharedPreferencesImpl(File file, int mode) {
    mFile = file; / / sp file
    mBackupFile = makeBackupFile(file); // Create a backup file
    mMode = mode; 
    mLoaded = false; // Indicates whether the sp file has been loaded into memory
    mMap = null; // Store key-value pairs in sp files
    mThrowable = null;
    startLoadFromDisk(); // Load data, see 1.4.1
}
Copy the code

Note the mMap here, which is a Map

that stores all the key-value pairs in the SP file. So all the data in the SharedPreferences file is in memory, and since it is in memory, it is not suitable for storing large amounts of data.
,>

1.4.1 startLoadFromDisk ()
> SharedPreferencesImpl.java

private void startLoadFromDisk(a) {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run(a) {
            loadFromDisk(); // Load asynchronously. See 1.4.2
        }
    }.start();
}
Copy the code
1.4.2 loadFromDisk ()
> SharedPreferencesImpl.java

private void loadFromDisk(a) {
    synchronized (mLock) { // Get the mLock lock
        if (mLoaded) { // The file has been loaded into memory
            return;
        }
        if (mBackupFile.exists()) { // If there is a backup file, rename the backup file to sp filemFile.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 { // Read the sp file
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally{ IoUtils.closeQuietly(str); }}}catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if(map ! =null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim; // Update the change time
                    mStatSize = stat.st_size; // Update the file size
                } else {
                    mMap = newHashMap<>(); }}// 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(); // Wake up the waiting thread}}}Copy the code

A quick overview of the process:

  1. Check whether it has been loaded into memory
  2. Check whether any remaining backup file exists. If yes, rename it as sp file
  3. Read sp file and store it in memory
  4. Updating File Information
  5. Release the lock and wake up the thread in the waiting state

LoadFromDisk () is asynchronous and thread-safe, holding the lock mLock during reads, which seems reasonable in design, but can cause problems when used improperly.

After reading this long source code, don’t forget that we are still stuck in the getSharedPreferences() method, which is the process of getting SharedPreferences. If we use getSharedPreferences() and then call getXXX() to get data, the method will be blocked if the sp file has a large amount of data and the reading process is time-consuming. When you look at the source code for the getXXX() method later, you’ll see that it waits for the SP file to load, or it blocks. Therefore, in the process of using, you can asynchronously initialize the SharedPreferences object in advance and load the SP file into the memory to avoid potential delays. This is a slot in SharedPreferences and something to be aware of when using it.

2. Read SP data

The seven getXXX functions in the SharedPreferencesImpl are used to get the data in the SP file. GetInt () = getInt(); getInt() = getInt();

> SharedPreferencesImpl.java

@Override
public int getInt(String key, int defValue) {
    synchronized (mLock) {
        awaitLoadedLocked(); // the sp file will block here before loading is complete, as shown in 2.1
        Integer v = (Integer)mMap.get(key); // Read directly from memory after loading
        returnv ! =null? v : defValue; }}Copy the code

Once the SP file is loaded, all operations to get data are read from memory. This does increase efficiency, but it is obvious that storing large amounts of data directly in memory is not appropriate, so SharedPreferences are not suitable for storing large amounts of data.

2.1 awaitLoadedLocked ()

> SharedPreferencesImpl.java

@GuardedBy("mLock")
private void awaitLoadedLocked(a) {
    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) {// wait until the sp file is loaded
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if(mThrowable ! =null) {
        throw newIllegalStateException(mThrowable); }}Copy the code

MLoaded has an initial value of false and is set to true after sp files are read in the loadFromDisk() method and mlock. notifyAll() is called to notify the waiting thread.

3. Store SP data

The basic method for storing data in SharedPreferences is as follows:

val editor = PreferenceManager.getDefaultSharedPreferences(this).edit()
editor.putInt("key".1)
editor.commit()/editor.apply()
Copy the code

The edit() method returns an Editor() object. Editor, like SharedPreferences, is just an interface, and their implementation classes are EditorImpl and SharedPreferences.

3.1 edit ()

> SharedPreferencesImpl.java

@Override
public Editor edit(a) {
    synchronized (mLock) {
        awaitLoadedLocked(); // Wait for the sp file to finish loading
    }

    return new EditorImpl(); / / see 3.2
}
Copy the code

The edit() method also waits for the sp file to load before initializing EditImpl(). Each call to the edit() method instantiates a new EditorImpl object. Be careful not to call the Edit () method every time you put(), which is a mistake you might make when encapsulating the SharedPreferences utility class.

3.2 EditorImpl

> SharedPreferencesImpl.java

public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();

    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>(); // Store the data to be modified

    @GuardedBy("mEditorLock")
    private boolean mClear = false; // Clear the flag

    @Override
    public Editor putString(String key, @Nullable String value) {
        synchronized (mEditorLock) {
            mModified.put(key, value);
            return this; }}@Override
    public Editor remove(String key) {
        synchronized (mEditorLock) {
            mModified.put(key, this);
            return this; }}@Override
    public Editor clear(a) {
        synchronized (mEditorLock) {
            mClear = true;
            return this; }}@Override
    public boolean commit(a) {}/ / see 3.2.1
    
    @Override
    public boolean apply(a) {}/ / see 3.2.2
Copy the code

There are two member variables, mModified and mClear. MModified is a HashMap that stores all key-value pairs that need to be added or modified through the putXXX() method. MClear is the clear flag, which is set to true in the clear() method.

All putXXX() methods simply change the mModified set and only modify the sp file when commit() or apply() is called. Let’s look at each of these methods.

3.2.1 the commit ()
> SharedPreferencesImpl.java

@Override
    public boolean commit(a) {
        long startTime = 0;

        if (DEBUG) {
            startTime = System.currentTimeMillis();
        }

        // Synchronize mModified to memory
        MemoryCommitResult mcr = commitToMemory(); / / see 3.2.2

        // Then synchronize the memory data to a file, see 3.2.3
        SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
        try {
            mcr.writtenToDiskLatch.await(); // Wait for the write operation to complete
        } catch (InterruptedException e) {
            return false;
        } finally {
            if (DEBUG) {
                Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                        + " committed after " + (System.currentTimeMillis() - startTime)
                        + " ms");
            }
        }
        notifyListeners(mcr); / / notification listener, callback OnSharedPreferenceChangeListener
        return mcr.writeToDiskResult; // Returns the result of the write operation
    }
Copy the code

Commit () looks like this:

  • First of all, the synchronousmModifiedCommitToMemory ()
  • EnqueueDiskWrite () then synchronizes memory data to sp file.
  • Wait for the write operation to complete and notify the listener

Memory synchronization is the commitToMemory() method and file writing is the enqueueDiskWrite() method. Let’s look at these two methods in detail.

3.2.2 commitToMemory ()
> SharedPreferencesImpl.java

// Returns true if any changes were made
private MemoryCommitResult commitToMemory(a) {
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // During commit() writing to local files, mDiskWritesInFlight is set to 1.
        CommitToMemory () is called before the write process is complete. Modifying mMap directly may affect the write result
        // Make a deep copy of mMap
        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();
                }
                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.
                // v == this and v == null both indicate that the key is deleted
                if (v == this || v == null) {
                    if(! mapToWriteToDisk.containsKey(k)) {continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if(existingValue ! =null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }

            mModified.clear();

            if(changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; }}return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}
Copy the code

Simply put, commitToMemory() combines all mModified data with the original SP file mMap to generate a new data set, mapToWriteToDisk. As the name suggests, this is the data set to be written to the file. Yes, writes to SharedPreferences are full writes. Even if you change only one of the configuration items, all data will be rewritten. One optimization we can make is to store the configuration items that need to be changed frequently in a separate SP file, avoiding the need to write all the configuration items each time.

3.2.3 enqueueDiskWrite ()
> SharedPreferencesImpl.java

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run(a) {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr, isFromSyncCommit); / / see 3.2.3.1
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if(postWriteRunnable ! =null) { postWriteRunnable.run(); }}};// Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    // commit() writes directly to the current thread
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return; }}/ / the apply () method to perform here, by QueuedWork. QueuedWorkHandler processingQueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit); }Copy the code

Let’s look back at how enqueueDiskWrite() is called in the commit() method:

 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
Copy the code

The second argument, postWriteRunnable, is null, so isFromSyncCommit is true and the above if block is executed instead of queuedWork.queue (). If you call commit() on the main thread, you will perform IO operations directly on the main thread. Obviously, this is not recommended and may cause caton or ANR. In practice, we should use the apply() method whenever possible to submit data. Of course, apply() isn’t perfect, as we’ll see later.

3.2.3.1 writeToFile ()

The last step of the commit() method is to write mapToWriteToDisk to the sp file.

> SharedPreferencesImpl.java

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
            // The file needs to be written only if the disk state is older than the current 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) {// Return directly without writing
                mcr.setDiskWriteResult(false.true);
                return;
            }

            boolean backupFileExists = mBackupFile.exists(); // Check whether the backup file exists

            if (DEBUG) {
                backupExistsTime = System.currentTimeMillis();
            }

            // If the backup file does not exist, rename mFile to the backup file for future exceptions
            if(! backupFileExists) {if(! mFile.renameTo(mBackupFile)) { Log.e(TAG,"Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(false.false);
                    return; }}else{ mFile.delete(); }}// Attempt to write the file, delete the backup and return true as atomically as
        // possible. If any exception occurs, delete the new file; next time we will restore
        // from the backup.
        try {
            FileOutputStream str = createFileOutputStream(mFile);

            if (DEBUG) {
                outputStreamCreateTime = System.currentTimeMillis();
            }

            if (str == null) {
                mcr.setDiskWriteResult(false.false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); // Full write

            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; // Update the file time
                    mStatSize = stat.st_size; // Update the file size}}catch (ErrnoException e) {
                // Do nothing
            }

            if (DEBUG) {
                fstatTime = System.currentTimeMillis();
            }

            // Writing was successful, delete the backup file if there is one.
            // The backup file is deleted
            mBackupFile.delete();

            if (DEBUG) {
                deleteTime = System.currentTimeMillis();
            }

            mDiskStateGeneration = mcr.memoryStateGeneration;

            // Returns write success and wakes up the waiting thread
            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
        // Clear the file that failed to write
        if (mFile.exists()) {
            if(! mFile.delete()) { Log.e(TAG,"Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false.false); // Returns write failure
    }
Copy the code

The process is clear, the code is simple,

3.2.4 the apply ()
> SharedPreferencesImpl.java

@Override
public void apply(a) {
    final long startTime = System.currentTimeMillis();

    // Synchronize mModified to memory
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run(a) {
                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(a) { 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);
}
Copy the code

Also, call commitToMemory() to synchronize to memory and enqueueDiskWrite() to synchronize to files. Unlike commit(), the enqueueDiskWrite() method’s Runnable argument is no longer null and a postWriteRunnable is passed in. So the internal execution logic is completely different from the commit() method. Back in Section 3.2.3, commit() executes writeToDiskRunnable() directly on the current thread, while apply() is handled by QueuedWork:

QueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit);/ / see 3.2.5
Copy the code
3.2.5 queue ()
> QueuedWork.java

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); }}}Copy the code

GetHandler is a Runnable thread that executes the Runnable.

> QueuedWork.java

private static Handler getHandler(a) {
    synchronized (sLock) {
        if (sHandler == null) {
            HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        returnsHandler; }}Copy the code

Writing sp files is performed asynchronously on a separate thread.

QueuedWork has a purpose other than performing asynchronous operations. It ensures that asynchronous tasks can be executed after Activity onPause()/onStop() or BroadCast onReceive(). Take the handlePauseActivity() method in ActivityThread.java as an example:

@Override
public void handleStopActivity(IBinder token, boolean show, int configChanges,
        PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
    final ActivityClientRecord r = mActivities.get(token);
    r.activity.mConfigChangeFlags |= configChanges;

    final StopInfo stopInfo = new StopInfo();
    performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
            reason);

    if (localLOGV) Slog.v(
        TAG, "Finishing stop of " + r + ": show=" + show
        + " win=" + r.window);

    updateVisibility(r, show);

    // Make sure any pending writes are now committed.
    // There may be a lag or even an ANR due to waiting for writes
    if(! r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } stopInfo.setActivity(r); stopInfo.setState(r.state); stopInfo.setPersistentState(r.persistentState); pendingActions.setStopInfo(stopInfo); mSomeActivitiesChanged =true;
}
Copy the code

The intention may be good, but we all know that Activity() onPause()/onStop() should not perform time-consuming tasks. If there is a large amount of SP data, there will undoubtedly be performance problems, which may cause stuttering or even ANR.

conclusion

Finished the SharedPreferences source code, slot can really many!

  1. Cross-process is not supported,MODE_MULTI_PROCESSIt is no use. Frequent reads and writes across processes may cause data corruption or loss.
  2. The sp file will be read during initialization, which may lead to subsequentgetXXX()Method blocking. It is recommended to initialize SharedPreferences asynchronously in advance.
  3. Sp file data will be stored in the memory, so it is not suitable to store big data.
  4. edit()Method is created one at a timeEditorImplObject. One edit() and multiple putXXX() are recommended.
  5. Whether it iscommit()orapply()Is written to full for any modifications. You are advised to save a separate SP file for frequently modified configuration items.
  6. commit()Synchronous save, return value.apply()Asynchronously saved, no return value. Take as needed.
  7. onPause()onReceive()Isochron waits for asynchronous write operations to complete, which may cause stalling or ANR.

With all these questions, shouldn’t we use SharedPreferences? The answer is definitely not. If you don’t need to cross processes and only store a few configuration items, SharedPreferences is still a good choice.

If SharedPreferences can no longer meet your needs, I recommend Tencent open source MMKV!

This article first published wechat official account: BingxinshuaiTM, focusing on Java, Android original knowledge sharing, LeetCode problem solving.

More latest original articles, scan code to pay attention to me!