SharedPreferences, which we often use, actually has many defects, mainly in the following aspects

  • memory
  • GetValue may cause ANR
  • Multiple processes are not supported
  • Partial updates are not supported
  • Either COMMIT or apply can result in ANR

The following reference android source code based on the use of plain English and some code snippets and we discuss sharing.

memory

final class SharedPreferencesImpl implements SharedPreferences { ...... SharedPreferencesImpl(File File, int mode) {mFile = File; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; StartLoadFromDisk (); }... }Copy the code

We all know that the Context implementation relies on the ContextImpl class, and our SharedPreferences implementation relies on the SharedPreferences class.

ContextImpl.java
    /**
     * Map from package name, to preference name, to cached preferences.
     */
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
Copy the code

In our ContextImpl class there is a static ArrayMap that caches all sp file objects under different packageName.

But in this class we can see the sounding initialization and assignment of the cache array, but there is no operation to remove or release the data in the array object.

Therefore, we can also know that sp files in the corresponding package directory of the APP will be cached in the method area during the operation of the APP, and this mechanism will cause a lot of memory, and would rather OOM than actively release the memory space.

GetValue can cause thread blocking or ANR

In our SharedPreferencesImpl constructor, a child thread is started to load the disk file, convert the XML file into a map, and the load takes a while to complete if the file is large or the thread schedule doesn’t start the thread right away.

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

And if we go to getValue right after we initialize it, then getValue will check to see if we’re blocking the external thread with awaitLoadedLocked,

private void awaitLoadedLocked() { if (! mLoaded) { BlockGuard.getThreadPolicy().onReadFromDisk(); } while (! // Mlock. wait(); // mlock. wait(); } catch (InterruptedException unused) { } } if (mThrowable ! = null) { throw new IllegalStateException(mThrowable); }}Copy the code

File must be loaded and converted successfully before the value operation. Finally, the notify operation will wake up the external thread that reads value when the disk load is complete.

The above operation scenarios are often seen in our APP. At the same time, if our SP data storage is large, it will take a long time for the disk to load and block the external thread, which directly leads to the main thread acquiring sp value directly.

Multiple processes are not supported

The mode parameter is also used to synchronize data from multiple processes.

static void setFilePermissionsFromMode(String name, int mode, int extraPermissions) { int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR |FileUtils.S_IRGRP|FileUtils.S_IWGRP |extraPermissions; if ((mode&MODE_WORLD_READABLE) ! = 0) { perms |= FileUtils.S_IROTH; } if ((mode&MODE_WORLD_WRITEABLE) ! = 0) { perms |= FileUtils.S_IWOTH; } FileUtils.setPermissions(name, perms, -1, -1); }Copy the code

The synchronization here means that when accessing this SP instance, it will determine whether the current disk file has been changed compared to the last memory modification. If so, it will reload the disk file and then synchronize to the cache.

  public static int setPermissions(String path, int mode, int uid, int gid) {
        try {
            Os.chmod(path, mode);
        } catch (ErrnoException e) {
            Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
            return e.errno;
        }

        if (uid >= 0 || gid >= 0) {
            try {
                Os.chown(path, uid, gid);
            } catch (ErrnoException e) {
                Slog.w(TAG, "Failed to chown(" + path + "): " + e);
                return e.errno;
            }
        }
        return 0;
    }

Copy the code

However, this synchronization has little effect, because when multiple processes change the SP value at the same time, the memory data in different processes will not be synchronized in real time, and the modification of SP data at the same time may lead to data loss and overwriting.

Partial updates are not supported

apply

public void apply() { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); // The task finally executes final Runnable awaitCommit = new in the handleStopService handlePauseActivity handleStopActivity method of the ActivityThread  Runnable() { @Override public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); }}; Queuedwork. queue(writeToDiskRunnable,! isFromSyncCommit); / / add the task to ActivityThread QueueWork list of SharedPreferencesImpl. Enclosing enqueueDiskWrite (MCR, postWriteRunnable); // changes reflected in memory. notifyListeners(mcr); }Copy the code

Both our synchronous commit and asynchronous Apply methods are full updates, meaning that even if we change a key-value pair, it overwrites the data to a disk file, causing unnecessary memory overhead.

Either COMMIT or apply can result in ANR

A more deadly problem with commit and apply is that they can also lead to ANR. This is because both commit and Apply perform an enqueueDiskWrite operation, which adds the current task of synchronizing SP memory data to Disk to a set of ActivityThread linked tasks. So we have to wonder when this disk synchronization task will finally be completed,

In fact, it will wait until the service stops, or the activity pauses or stops, before the for loop sets the tasks mentioned above, and finally completes the memory data to disk data. The activity or service switch life cycle is blocked due to a large number of read/write synchronous tasks to disk, resulting in ANR.

HandleStopActivity method (ActivityThread)

Queuedwork.waittofinish () — processPendingWork(); Finally, the disk write back task is performed

for (Runnable w : work) {
                    w.run();
                }
Copy the code

To sum up, we must have a better understanding of SharedPreferences after these analyses.

Android officially recommends that we use the DataStore in Jetpack, or we can use the MMKV framework developed by Tencent team.