SharedPreference is used to access and modify the Context. GetSharedPreferences return data interface. It is Google’s official implementation of lightweight data storage solution, can be used to store user information and other data. Sp provides a high level of consistency, but at a high cost, which can lead to ANR problems.

The execution process of SharedPreferences

To acquire the SharedPreferences

We normally call the Context’s getSharedPreferences method, which is actually implemented by the ContextImpl class. Calling it returns a SharedPreferencesImpl corresponding to the filename in the argument. For each particular XML file, all clients correspond to the same SP instance. This correspondence is stored in an ArrayMap. If sp is already present in ArrayMap, it is returned directly. If sp is not present in ArrayMap, a new sp instance is created and stored in ArrayMap.

// getSharedPreferences(String name,int mode)
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            // ...
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            return sp;
        }
        // ...
    }
    return sp;
}
/ / packagePrefs key: File val: SharedPreferencesImpl files and sp
// sSharedPrefsCache key:packageName val:packagePrefs packageName corresponds to the arraymap above
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

Read the data

SharedPreferencesImpl is the default implementation class for SharedPreferences. When it is new, it calls startLoadFromDisk to read the k-V pair into memory from disk. In order to ensure safe load, mlock is used as the lock. Data is read by an asynchronous loadFromDisk process and mloaded indicates whether the load is complete. The logic of loadFromDisk is to replace the incoming file with the backup file if there is one, then read the KV pair and store it in a temporary map, and finally assign it to the global mMap if there are no exceptions.

private void startLoadFromDisk(a) {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run(a) {
            loadFromDisk();
        }
    }.start();
}

private void loadFromDisk(a) {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if(mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); }}// Debugging
    if(mFile.exists() && ! mFile.canRead()) { Log.w(TAG,"Attempt to read preferences file " + mFile + " without permission");
    }
    Map<String, Object> map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally{ IoUtils.closeQuietly(str); }}}catch (ErrnoException e) {
    } catch (Throwable t) {
        thrown = t;
    }
    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;
        try {
            if (thrown == null) {
                if(map ! =null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = newHashMap<>(); }}}catch (Throwable t) {
            mThrowable = t;
        } finally{ mLock.notifyAll(); }}}Copy the code

MMap stores k-V key-value pairs, and upon completion of loading, mLock.notifyAll() is called to wake up all waiting reader processes and Editor objects regardless of success or failure.

// The getXXX method of SharedPreferences calls this method to ensure that the asynchronous load has completed
private void awaitLoadedLocked(a) {
    if(! mLoaded) { BlockGuard.getThreadPolicy().onReadFromDisk(); }while(! mLoaded) {try {
            mLock.wait();  // Wait for notifyAll to wake up
        } catch (InterruptedException unused) {
        }
    }
    / /...
}

publicMap<String, ? > getAll() {synchronized (mLock) {
        awaitLoadedLocked();
        return newHashMap<String, Object>(mMap); }}public Editor edit(a) { 
    synchronized (mLock) { 
        awaitLoadedLocked(); 
    } 
    return new EditorImpl(); 
}
Copy the code

Modify the data

To keep the data consistent, SharedPreferences has an Editor class built in to handle all changes. Changes are first stored in a map and only performed on files during commit or apply.

private final Object mEditorLock = new Object(); / / edit lock
private final Map<String, Object> mModified = new HashMap<>(); 
private boolean mClear = false;
// mModified Saves whether the modified K-V mClear flag is cleared
// Delete key mmodified. put(key,this)
public Editor putXXX(String key, XXX value) {
    synchronized (mEditorLock) {
        mModified.put(key, value);
        return this; }}Copy the code

The main difference between commit and apply is that commit is performed synchronously, while apply is asynchronous and delayable. As you can see, the main process for both Apply and COMMIT is to get a MemoryCommitResult object and then call enqueueDiskWrite to queue it. You can see that whether postWriteRunnable is empty is the key for enqueueDiskWrite to determine whether the calling method is commit or apply. Apply’s awaitCommit is put into QueueWork to ensure that it is executed when onStop is called like Activity, ensuring that data is not lost.

public void apply(a) {
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        @Override
        public void run(a) {
            try {
                mcr.writtenToDiskLatch.await(); // Wait until the countdown is 0 before the thread can execute
            } catch (InterruptedException ignored) {
            }
        }
    };
    QueuedWork.addFinisher(awaitCommit);
    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run(a) { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); }}; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

public boolean commit(a) {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(
 mcr, null);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}
Copy the code

CommitToMemory is mainly used to generate MemoryCommitResult, including several important variables mapToWriteToDisk, memoryStateGeneration, keysCleared, KeysModified is used for processing when saving to disk. MDiskWritesInFlight indicates that several threads are committing, so commitToMemory will ++, and writeToFile will ++. MemoryStateGeneration is used to mark memory versions. By comparing it to the disk SP version, the global memory SP version can determine whether to commit or apply.

WrittenToDiskLatch is a countdown timer, and its await() method in apply’s awaitCommit is to wait for the setDiskWriteResult method to decrement the counter by one, Enable the main thread on which writtenToDiskLatch resides to execute.

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;

    private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared,
            @Nullable List<String> keysModified,
            @Nullable Set<OnSharedPreferenceChangeListener> listeners,
            Map<String, Object> mapToWriteToDisk) {
            // ...
    }
    // writeToFile will be executed
    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten; writeToDiskResult = result; writtenToDiskLatch.countDown(); }}private MemoryCommitResult commitToMemory(a) {
    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++;     // Add another thread to modify the commit
        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }
        synchronized (mEditorLock) {
            // Setup logic of mapToWriteToDisk
            if (changesMade) {
                mCurrentMemoryStateGeneration++; / / global
            }
            MemoryStateGeneration and memoryStateGeneration when writeToFile is executed
            / / mCurrentMemoryStateGeneration is not necessarily the samememoryStateGeneration = mCurrentMemoryStateGeneration; }}return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
            listeners, mapToWriteToDisk);
}
Copy the code

In enqueueDiskWrite, mDiskWritesInFlight is determined by the number of commit threads. If commit and mDiskWritesInFlight=1, the thread can execute directly. Otherwise, the thread is queued for execution. The function that is actually executed is writeToFile, and then, if applied, postWriteRunnable blocks the thread and waits for the result.

 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);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;  // After submitting diskWrite, the process is subtracted by 1
                }
                if(postWriteRunnable ! =null) {
                    postWriteRunnable.run();  // awaitCommit Waits for the result to return}}};if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1; // Only one commit can be executed directly in this process
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return; }}// Otherwise, execute in queueQueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit); }Copy the code

WriteToFile logic can be broken down into the following steps:

  • Set the Boolean value for needsWrite. Write only when the disk SP version is low, and then check whether the disk SP version is commit or update only when the MCR SP version is consistent with the latest global version when applying. Otherwise, frequent operations are not required.
  • If needsWrite is true, check to see if there is a backup file. The backup file is mainly used to restore to the previous data consistency state if the modification fails, so if there is no backup file, create one with mFile. If there is a backup file, the mFile can be deleted.
  • Modify the file, if successful delete the backup file, and update various values such as disk SP version and so on. The call to setDiskWriteResult returns the result. The countdown operation to the countdown timer is performed within this function so that postWriteRunnable will not block.
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    long startTime = 0;
    long existsTime = 0;
    long backupExistsTime = 0;
    long outputStreamCreateTime = 0;
    long writeTime = 0;
    long fsyncTime = 0;
    long setPermTime = 0;
    long fstatTime = 0;
    long deleteTime = 0;
    
    boolean fileExists = mFile.exists();

    if (fileExists) {
        boolean needsWrite = false;
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true; }}}}if(! needsWrite) { mcr.setDiskWriteResult(false.true);
            return;
        }
        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(); }}try {
        FileOutputStream str = createFileOutputStream(mFile);
        if (str == null) {
            mcr.setDiskWriteResult(false.false);
            return;
        }
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        writeTime = System.currentTimeMillis();
        FileUtils.sync(str);
        fsyncTime = System.currentTimeMillis();
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        if (DEBUG) {
            setPermTime = System.currentTimeMillis();
        }
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized(mLock) { mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; }}catch (ErrnoException e) {
        }
        mBackupFile.delete();
        mDiskStateGeneration = mcr.memoryStateGeneration;
        mcr.setDiskWriteResult(true.true);
        long fsyncDuration = fsyncTime - writeTime;
        mSyncTimes.add((int) fsyncDuration);
        mNumSync++;
        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    if (mFile.exists()) {
        if(! mFile.delete()) { Log.e(TAG,"Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false.false);
}
Copy the code

The problem of SharedPreferences

In the actual use of the case, did find that after using sharedPreferences will feel a little bit stuck, my simulator is more obvious. After reading sp source code and other people’s blogs, IT is found that SP loads and writeToFile either read or rewrite the entire file at one time. If XML is large, performance will be affected.

And since the Apply method calls QueuedWork.addFinisher(awaitCommit), awaitCommit blocks the thread waiting for writeToFile to return, In the activity. The onstop, broadcastReceiver. OnReceive, service. HandleCommend, invoked QueuedWork. WaitToFinish () to perform queuework inside all the work And the finisher. In this case, it will take a long time to wait for activity.onstop, which will affect the change of activtiy life cycle and cause ANR problems.

It is not clear why waitToFinsh is called at all, and I have seen on other blogs that the anR is reduced by emptying queuework’s wait queue directly.

Ideas for improvement

  • Implementation classes for SharedPreferences can be customized
  • Empty the wait queue before calling the waitToFinsh method
  • Learn to use MMKV

feeling

After learning the source code of SharedPreferences, I really feel that I have a clearer understanding of the whole process and use, and probably know what to start from if I want to optimize in the future. In the whole process of reading, I feel that I have not learned enough about synchronization, asynchrony, lock and so on, which needs to be strengthened in the future.

Reference documentation

Android re-learning series SharedPreferences source code analysis

Analyze the ANR problem caused by SharedPreference apply

SharedPreferences ANR