Count the slots in Shared Preferences!

Posted by raytri on Wed, 28 Aug 2019 17:41:53 +0200

Preface

Recently, it has suffered a lot in dealing with a historical legacy project, mainly as the occasional Shared Preferences configuration file data confusion, or even loss. After investigation, it was found that it was a multi-process problem. There are two different processes in the project, and Shared Preferences files are frequently read and written, which results in data confusion and loss. Take this opportunity to read the Shared Preferences source code carefully. Now let's talk about what slots Shared Preferences have.

Source code parsing

Shared Preferences is easy to use, so I won't show it here. Next, we will read the source code from three aspects: acquiring SharedPreference, getXXX() acquiring data and putXXX() storing data.

1. Get Shared Preferences

1.1 getDefaultSharedPreferences()

Generally, we get the default Shared Preferences object by using the getDefaultShared Preferences () method of PreferenceManager. The code is as follows:

> PreferenceManager.java 

/**
 * Get the default Shared Preferences object, file name packageName_preferences, model MODE_PRIVATE
 */
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());  // See 1.2
}

The default sp file full path is / data/data/shared_prefs/[packageName]_preferences.xml. The default mode is MODE_PRIVATE, which is used only now. It will also be mentioned in the source code parsing later. Finally, the getSharedPreferences() method of ContextImpl is called.

1.2 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<>();
        }
        // Find whether the sp file exists from the cache mSharedPrefsPaths
        file = mSharedPrefsPaths.get(name);
        if (file == null) { // If not, create a new sp file named "name.xml"
            file = getSharedPreferencesPath(name);
            mSharedPrefsPaths.put(name, file);
        }
    }
    return getSharedPreferences(file, mode); // See 1.3
}

First, there is a variable mSharedPrefsPaths. Find its definition:

/**
 * The file is named key and the specific file is value. Store all sp files
 * Protected by ContextImpl.class lock
 */
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;

MShared PrefsPaths is an Array Map that caches the corresponding relationship between file names and sp files. Firstly, the corresponding sp file is found in the cache according to the name of the file in the parameter. If it does not exist, a new file named [name].xml is created and stored in the cache mSharedPrefsPaths. Finally, another overloaded getSharedPreferences() method is called with the parameter 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); // First try to get sp from the cache
        if (sp == null) { // If the acquisition cache fails
            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 Shared Preferences Impl, see 1.4
            cache.put(file, sp);
            return sp;
        }
    }

    // When mode is MODE_MULTI_PROCESS, the file may be modified by other processes, and then reloaded.
    // Obviously this is not enough 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;
}

Shared Preferences is just an interface. What we want to get is actually its implementation class Shared Preferences Impl. The cached Shared Preferences Impl object and its sp file can be retrieved by the getShared Preferences CacheLocked () method.

1.3.1 getSharedPreferencesCacheLocked()
> ContextImpl.java

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;
}

SShared PrefsCache is a nested Array Map, which is defined as follows:

private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

The package is named key, and the value is an ArrayMap that stores the sp file and its SharedPreferencesImp object. If there is a direct return, 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 with N, if you use MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE, 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");
        }
    }
}

Starting with Android N, it is clear that MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE are no longer supported, and MODE_MULTI_PROCESS can not guarantee thread safety, so MODE_PRIVATE is generally used.

1.4 SharedPreferencesImpl

If there is no SharedPreferencesImpl object in the cache, you have to create it yourself. Look at its constructor:

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

Note the mMap here, which is a Map < String, Object > that stores all key-value pairs in the sp file. So all the data of Shared Preferences file exists in memory, since it exists in memory, it is doomed that it is not suitable for storing large amounts of data.

1.4.1 startLoadFromDisk()
> SharedPreferencesImpl.java

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk(); // Asynchronous loading. See 1.4.2
        }
    }.start();
}
1.4.2 loadFromDisk()
> SharedPreferencesImpl.java

private void loadFromDisk() {
    synchronized (mLock) { // Getting mLock locks
        if (mLoaded) { // Loaded into memory, returned directly, no longer read files
            return;
        }
        if (mBackupFile.exists()) { // If there is a backup file, rename the backup file to sp file directly
            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 { // 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 modification time
                    mStatSize = stat.st_size; // Update file 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(); // Wake up a waiting thread
        }
    }
}

Simply stroke the process:

  1. Determine if it has been loaded into memory
  2. Determine whether a legacy backup file exists and rename it sp file if it exists
  3. Read the sp file and store it in memory
  4. Update file information
  5. Release the lock and wake up the waiting thread

loadFromDisk() is asynchronously executed and thread-safe. It seems reasonable to have a lock mLock in the process of reading, but there will be problems in unreasonable use.

After looking at this long source code, don't forget that we're still stuck in the getSharedPreferences() method, which is the process of getting SharedPreferences. If we call getSharedPreferences() in the process of using, and then call getXXX() directly to get data, it happens that the amount of sp file data is large and the reading process is time-consuming, and the getXXX() method will be blocked. When you see the source code of the getXXX() method later, you will see that it needs to wait for the sp file to load, otherwise it will block. So in the process of using, SharedPreferences objects can be initialized asynchronously in advance, sp files can be loaded into memory to avoid potential Karton. This is a slot in Shared Preferences, which we need to pay attention to in the process of using.

2. Read sp data

The seven getXXX functions in Shared Preferences Impl are used to get the data in the sp file. These seven functions all have the same logic. Take getInt() as an example to see the source code:

> SharedPreferencesImpl.java

@Override
public int getInt(String key, int defValue) {
    synchronized (mLock) {
        awaitLoadedLocked(); // When the sp file has not been loaded, it will be blocked here, as shown in 2.1
        Integer v = (Integer)mMap.get(key); // Read directly from memory after loading
        return v != null ? v : defValue;
    }
}

Once the sp file is loaded, all data acquisition operations are read from memory. This improves efficiency, but obviously it is not appropriate to store a large amount of data directly in memory, so Shared Preferences is doomed not to be suitable for storing a large amount of data.

2.1 awaitLoadedLocked()

> SharedPreferencesImpl.java

@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) { // When the sp file has not been loaded, wait
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

The initial value of mLoaded is false, which is set to true after reading the sp file in the loadFromDisk() method and calling mLock.notifyAll() to notify the waiting thread.

3. Storing sp data

Shared Preferences stores data in the following basic ways:

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

The edit() method returns an Editor() object. Like Shared Preferences, Editor is only an interface, and their implementation classes are Editor Impl and Shared Preferences Impl, respectively.

3.1 edit()

> SharedPreferencesImpl.java

@Override
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked(); // Waiting for the sp file to load
    }

    return new EditorImpl(); // See 3.2
}

The edit() method also waits for the sp file to be loaded, and then initializes EditImpl(). Each call to the edit() method instantiates a new EditorImpl object. So we should be careful not to call the edit() method every time we put(), which may be a mistake when encapsulating the SharedPreferences tool 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 data to be modified

    @GuardedBy("mEditorLock")
    private boolean mClear = false; // Clearance markers

    @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() {
        synchronized (mEditorLock) {
            mClear = true;
            return this;
        }
    }
    
    @Override
    public boolean commit() { } // See 3.2.1
    
    @Override
    public boolean apply() { } // See 3.2.2

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 a clear tag, which is set to true in the clear() method.

All putXXX() methods only change the mModified collection and modify the sp file when commit() or apply() is called. Let's look at these two methods separately.

3.2.1 commit()
> SharedPreferencesImpl.java

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

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

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

        // Synchronize memory data to files, see 3.2.3
        SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
        try {
            mcr.writtenToDiskLatch.await(); // Waiting 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); // Notify the listener and call back OnShared Preference ChangeListener
        return mcr.writeToDiskResult; // Returns the result of the write operation
    }

The general process of commit() is:

  • First, synchronize mModified into memory, commitToMemory()
  • Then synchronize the memory data to the sp file, enqueueDiskWrite()
  • Wait for the write operation to complete and notify the listener

Memory synchronization is the commitToMemory() method, and writing to a file 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() {
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // During commit() writing to a local file, mDiskWritesInFlight is set to 1.
        // When the writing process is not complete, commitToMemory() is called again. Modifying mMap directly may affect the writing result.
        // So here's 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 deletion of this 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, keysModified, listeners,
            mapToWriteToDisk);
}

Simply put, the commitToMemory() method combines all the data mModified that needs to be changed with the original sp file data mMap to generate a new data set mapToWriteToDisk, which can also be seen from the name, which is the data set to be written to the file later. Yes, Shared Preferences writes in full. Even if you change only one of the configuration items, all the data will be rewritten. To address this, we can optimize that configuration items that need frequent changes are stored in separate sp files to avoid full writing 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() {
            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 executes here, handled by QueuedWork.QueuedWorkHandler
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

Look back at how the enqueueDiskWrite() method is called in the commit() method:

 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);

The second parameter, postWriteRunnable, is null, so isFromSyncCommit is true, which executes the if block above, rather than QueuedWork.queue(). Thus, the last file writing operation of the commit() method is executed directly in the current calling thread. If you call this method in the main thread, the IO operation will be performed directly in the main thread. Obviously, this is not recommended, which may cause Carton or ANR. In practice, we should try our best to use the apply() method to submit data. Of course, apply() is not perfect, as we will mention 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
            // Write files only if the disk state is older than the current submission
            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) { // No need to write, return directly
                mcr.setDiskWriteResult(false, true);
                return;
            }

            boolean backupFileExists = mBackupFile.exists(); // Does the backup file exist?

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

            // If the backup file does not exist, rename mFile to the backup file for later use when an exception occurs
            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 file time
                    mStatSize = stat.st_size; // Update file size
                }
            } catch (ErrnoException e) {
                // Do nothing
            }

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

            // Writing was successful, delete the backup file if there is one.
            // Write successfully, delete backup files
            mBackupFile.delete();

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

            mDiskStateGeneration = mcr.memoryStateGeneration;

            // Return to Write Successfully, Wake Up Waiting Threads
            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 files that were not successfully written
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false, false); // Return Write Failure
    }

The process is clear and the code is simple.

3.2.4 apply()
> SharedPreferencesImpl.java

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

    // Synchronize mModified to memory first
    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);
}

Also, call commitToMemory() to synchronize to memory first, and then call enqueueDiskWrite() to synchronize to files. Unlike commit(), the Runnable parameter of the enqueueDiskWrite() method is no longer null, and a postWriteRunnable is passed in. So its internal execution logic and commit() method are completely different. Let's go back to section 3.2.3 and see that the commit() method executes writeToDiskRunnable() directly on the current thread, while apply() is handled by QueuedWork:

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); // See 3.2.5
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);
        }
    }
}

The handler here is the thread that executes Runnable. Take a look at the getHandler source code:

> QueuedWork.java

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

            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

Writing sp files executes asynchronously on a separate thread.

In addition to performing asynchronous operations, QueuedWork has a role to play. It ensures that asynchronous tasks can be performed after Activity on Pause ()/onStop (), or BroadCast on Receive (). 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.
    // Carton and even ANR may be caused by waiting to write
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }

    stopInfo.setActivity(r);
    stopInfo.setState(r.state);
    stopInfo.setPersistentState(r.persistentState);
    pendingActions.setStopInfo(stopInfo);
    mSomeActivitiesChanged = true;
}

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

summary

After Shared Preferences source code, there are many slots!

  1. Cross-process is not supported and MODE_MULTI_PROCESS is useless. Frequent reading and writing across processes can lead to data corruption or loss.
  2. The sp file is read during initialization, which may cause subsequent getXXX() methods to block. It is recommended that Shared Preferences be initialized asynchronously in advance.
  3. The data of sp file will be stored in memory, so it is not suitable to store large data.
  4. The edit() method creates an EditorImpl object each time. It is recommended to edit() once and put XXX () many times.
  5. Whether commit() or apply(), any modification is written in full. It is suggested that there should be separate sp files for high frequency modification of configuration items.
  6. commit() is saved synchronously with a return value. apply() is saved asynchronously with no return value. On demand.
  7. The onPause(), onReceive() wait for the asynchronous write operation to complete, which may cause Carton or ANR.

With so many questions, shouldn't we use Shared Preferences? The answer is definitely not. Shared Preferences is still a good choice if you don't need to cross-process and just store a small number of configuration items.

If Shared Preferences can't meet your needs, recommend Tencent Open Source MMKV !

Writing the first Wechat Public Number: Dedicated to Java, Android original knowledge sharing, LeetCode problem solving.

More latest original articles, pay attention to me!

Topics: Android Java xml