An overview,

SharedPreferences SharedPreferences is an Android storage method used to store relatively small key-value pairs. It is suitable for storing small data such as user preferences. SharedPreferences using XML file format save the data, the file is located in the data/data/packagename shared_prefs directory. SharedPreferences is suitable for light use, but heavy use can cause your application to crash. Android has a problem, too, and has developed a new solution DataStore under Jetpack to store data in an asynchronous, consistent transactional manner. DataStore is designed to replace SharedPreferences, currently at version 1.0.0-Alpha04. The source for this article comes from API 29

Question:

  • SharedPreferencestheapply()andcommit()The difference between them?
  • Can causeANR?
  • Can it be used in multiple processes?

Two, simple use

   // Save data
    fun save(view: View) {
        val sp = getSharedPreferences("test", Context.MODE_PRIVATE)
        val editor = sp.edit()
        editor.putString("key"."value")
        editor.putInt("num".1)
        editor.apply()
    }
    // Read data
    fun read(view: View) {
        val sp = getSharedPreferences("test", Context.MODE_PRIVATE)
        val str = sp.getString("key"."")
        val num = sp.getInt("num".0)
        println(str)
        println(num)
    }
Copy the code

SP creation and cache, ContextImpl analysis

SharedPreferences is an interface whose implementation class is SharedPreferencesImpl, which can be traced through the Activity’s getSharedPreferences() method. The XML file created here is saved to avoid multiple creation. Mode is also detected when creating SharedPreferencesImpl instances. API 24 no longer supports MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE modes. The created instance will be stored in a static map. Therefore, during the lifetime of the app, all instantiated data will not be recycled, so do not save too much data to prevent OOM. In multi-process mode, obtaining an instance of SP will be loaded for the second time, which is a time-consuming IO operation according to the later analysis. Therefore, multi-process mode reduces the efficiency of SP and is unreliable.

    ContextWrapper.java
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // mBase is contextimpl.java
        return mBase.getSharedPreferences(name, mode);
    }
    ContextImpl.java
    private ArrayMap<String, File> mSharedPrefsPaths;
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // Resolve previous problems
        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                name = "null"; }}// The file name is key and the file name is value
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                Create the XML file in the shared_prefs directoryfile = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); }}return getSharedPreferences(file, mode);
    }
    @Override
    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }
    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            // Read from the cache first
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                //API 24 no longer supports MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE modes
                checkMode(mode);
                // Encrypted file detection
                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 and cache
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                returnsp; }}// Multi-process mode or API is less than 11
        if((mode & Context.MODE_MULTI_PROCESS) ! =0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            // reload once
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
    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<>();
            // Save by package name
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

Copy the code

4. SP data loading and reading

1. The load

When SharedPreferencesImpl is initialized, a new thread is started to load the file and then read the data from the XML file into the map.

   / / initialization
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        startLoadFromDisk();
    }
   // Start asynchronous loading
    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 it has been loaded
                return;
            }
            if (mBackupFile.exists()) {// If there are backup files, directly change the backup file to this file
                mFile.delete();
                mBackupFile.renameTo(mFile);
            }
        }
        Map<String, Object> map = null;
        StructStat stat = null;
        Throwable thrown = null;
        try {
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                   // Read the file into the map through XmlUtils
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16 * 1024);
                    map = (Map<String, Object>) XmlUtils.readMapXml(str);
                } catch (Exception e) {
                } finally{ IoUtils.closeQuietly(str); }}}catch (ErrnoException e) {
        } catch (Throwable t) {
            thrown = t;
        }

        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;
            try {
               // Make sure no exception is reported, assign to the loaded map, record the recorded time and file size
                if (thrown == null) {
                    if(map ! =null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;// Record the time recorded
                        mStatSize = stat.st_size;// Record the size of the file
                    } else {
                        mMap = newHashMap<>(); }}}catch (Throwable t) {
                mThrowable = t;// Record an exception
            } finally {// Notify other threadsmLock.notifyAll(); }}}Copy the code

GetXXX ();

If the file is not loaded, the user interface (UI) thread will wait. If the file is too large and takes too long to load, the user interface thread will wait.

    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            returnv ! =null? v : defValue; }}private void awaitLoadedLocked(a) {
        if(! mLoaded) {//StrictMode check
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while(! mLoaded) {try {
                mLock.wait(); // Wait until the load is finished
            } catch (InterruptedException unused) {
            }
        }
        if(mThrowable ! =null) {// If there is an exception, throw it inside
            throw newIllegalStateException(mThrowable); }}Copy the code

5. Save data apply and commit

1. To be submitted

Each call to Edit returns a new EditorImpl object, and a call to putXXX() puts the data to be committed into a map of mModified.

    public Editor commit(a) {
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }

    EditorImpl.java
    private final Object mEditorLock = new Object();
    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();
    private boolean mClear = false;

    public Editor putString(String key, @Nullable String value) {
         synchronized (mEditorLock) {
              mModified.put(key, value);
              return this; }}Copy the code

2.commit

The COMMIT method first commits the modified data to memory and puts it into the SP map. It also records in the commitToMemory method whether real changes have occurred (key or value changes). If any changes have occurred, the file will be written and IO operations will be reduced. Commit waits for the file to finish writing on the main thread and returns the result.

Commit commits data on the main thread and waits for execution results. If the data is too large, writing to the file takes too long and blocks the main thread, resulting in a lag.

public boolean commit(a) {
    // Update data to SP
    MemoryCommitResult mcr = commitToMemory();
    // Synchronize data to file
    SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null);
    try {
        // Wait for the file to be written
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally{}// Notify the listener
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

private MemoryCommitResult commitToMemory(a) {
    long memoryStateGeneration;
    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) {// Listen for key changes
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            // Whether the change has really happened
            boolean changesMade = false;
            if (mClear) {// If the map is marked empty, empty the map directly
                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();
                // The value of the editor object itself or null means 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);
                        // Check to see if there really is a change
                        if(existingValue ! =null && existingValue.equals(v)) {
                            continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }
                / / really changed
                changesMade = true;
                if (hasListeners) {// This key is changed, recordkeysModified.add(k); }}// Delete the data submitted in the editor (already submitted to SP map)
            mModified.clear();

            if(changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; }}return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    // Whether the commit is synchronous
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run(a) {
                synchronized (mWritingToDiskLock) {
                    // Write to the file
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if(postWriteRunnable ! =null) {//commit does not exist, apply doespostWriteRunnable.run(); }}};if (isFromSyncCommit) {//commit performs the write file directly here
        boolean wasEmpty = false;
        synchronized (mLock) {
            // In the commitToMemory method this value is incremented by 1, so this is true
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            WriteToDiskRunnable is executed directly in this thread and then returned
            writeToDiskRunnable.run();
            return; }}/ / the apply itQueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit); }Copy the code

3.apply

Unlike commit, apply has no return value. After the data is written to memory, the file writing task is queued to another thread for execution.

public void apply(a) {
    // Update data to SP
    final MemoryCommitResult mcr = commitToMemory();
    QueuedWork: QueuedWork: QueuedWork: QueuedWork: QueuedWork: QueuedWork Block the main thread if necessary and wait.
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run(a) {
                try {
                    mcr.writtenToDiskLatch.await();
                } 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);
}

Copy the code

4. writeToFile

Write data to a file, which checks to see if it really needs to be written to disk, commit directly writes to the file, and Apply checks to see if this is the last commit, which only needs to be written to disk for the last time. The fileutils.sync (STR) operation ensures that data falls onto disks rather than being written to buffers, ensuring that data is not lost due to, for example, device power outages.

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();

    // Rename the current file so that it can be used as a backup for the next read
    if (fileExists) {
        boolean needsWrite = false;// Whether the file needs to be written

        // Make sure that changes are made
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {// Commit writes files
                needsWrite = true;
            } else {// Apply only writes to the last commit
                synchronized (mLock) {
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true; }}}}if(! needsWrite) {// Returns without writing the disk
            mcr.setDiskWriteResult(false.true);
            return;
        }

        boolean backupFileExists = mBackupFile.exists();

        if(! backupFileExists) {if(! mFile.renameTo(mBackupFile)) {// Backup files
                Log.e(TAG, "Couldn't rename file " + mFile
                      + " to backup file " + mBackupFile);
                mcr.setDiskWriteResult(false.false);
                return; }}else{ mFile.delete(); }}// Try to write files, delete backups, and return true as atomically as possible. If any exceptions occur, delete the new file; Next time we will restore from a backup
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        if (str == null) {
            mcr.setDiskWriteResult(false.false);
            return;
        }
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        writeTime = System.currentTimeMillis();
       // Make sure the file falls down
        FileUtils.sync(str);

        fsyncTime = System.currentTimeMillis();

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {// Record the time and size of the filemStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; }}catch (ErrnoException e) {
            // Do nothing
        }
        // Write succeeded, delete the backup file
        mBackupFile.delete();
        mDiskStateGeneration = mcr.memoryStateGeneration;
        // Write success, release lock, return result
        mcr.setDiskWriteResult(true.true);

        long fsyncDuration = fsyncTime - writeTime;
        mSyncTimes.add((int) fsyncDuration);
        mNumSync++;
        return;
    } catch (XmlPullParserException e) {
       
    } catch (IOException e) {
    }
    // Write failed, delete file
    if (mFile.exists()) {
        if(! mFile.delete()) { Log.e(TAG,"Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false.false);
}

Copy the code

5.startReloadIfChangedUnexpectedly

If mode is set to MODE_MULTI_PROCESS, the system determines whether the file has changed and reloads the file to SP once. Therefore, using SP in multi-process will reduce efficiency, because synchronization in SP is thread synchronization, and inter-process synchronization cannot guarantee the correctness of data, so it cannot be used in multi-process. MODE_MULTI_PROCESS has also been officially marked as Deprecated.

void startReloadIfChangedUnexpectedly(a) {
    synchronized (mLock) {
        if(! hasFileChangedUnexpectedly()) {return; } startLoadFromDisk(); }}private boolean hasFileChangedUnexpectedly(a) {
    synchronized (mLock) {
        if (mDiskWritesInFlight > 0) {
            if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
            return false; }}final StructStat stat;
    try {
        BlockGuard.getThreadPolicy().onReadFromDisk();
        stat = Os.stat(mFile.getPath());
    } catch (ErrnoException e) {
        return true;
    }
    synchronized (mLock) {// Determine whether the file has changed by the update time and size of the file
        return! stat.st_mtim.equals(mStatTimestamp) || mStatSize ! = stat.st_size; }}Copy the code

Six, QueuedWork. Java

As a normal process, Apply adds tasks to the sWork of a LinkedList via the queue method, and then executes the write tasks in a single-threaded order via the HandlerThread.

public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();

    synchronized (sLock) {
        // Add it to the list
        sWork.add(work);
        // Send a message to the task thread to perform the write file task
        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else{ handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); }}}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; }}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) { processPendingWork(); }}}private static void processPendingWork(a) {
    long startTime = 0;
    synchronized (sProcessingWork) {
        LinkedList<Runnable> work;

        synchronized (sLock) {
            work = (LinkedList<Runnable>) sWork.clone();
            sWork.clear();
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }
        // Execute tasks in a loop
        if (work.size() > 0) {
            for(Runnable w : work) { w.run(); }}}}Copy the code

Alternatively, QueuedWork has a waitToFinish method, which is called in the ActivityThread handleStopService and handleStopActivity methods, with the onPause of the Activity called. BroadcastReceiver onReceive, stop Service, etc. As can be seen from the code, in these cases, if the file write task is not completed, directly put the write task on the main thread to execute, cancel other threads to execute the task. In this way, the file writing task can be completed and data loss can be prevented. The waitToFinish approach was implemented differently prior to API 8.0, where the main thread was blocked to wait for the task to complete.

public static void waitToFinish(a) {
    long startTime = System.currentTimeMillis();
    boolean hadMessages = false;

    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // The following is to be executed directly, so it is deleted here
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
        }
        // There can be no delay
        sCanDelay = false;
    }

    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        // Execute the task directly
        processPendingWork();
    } finally {
        StrictMode.setThreadPolicy(oldPolicy);
    }

    try {
        while (true) {// The finisher added by apply is executed
            Runnable finisher;
            synchronized (sLock) {
                finisher = sFinishers.poll();
            }
            if (finisher == null) {
                break; } finisher.run(); }}finally {
        sCanDelay = true;
    }
    synchronized (sLock) {
        long waitTime = System.currentTimeMillis() - startTime;
        if (waitTime > 0 || hadMessages) {
            mWaitTimes.add(Long.valueOf(waitTime).intValue());
            mNumWaits++;
            if (DEBUG || mNumWaits % 1024= =0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                mWaitTimes.log(LOG_TAG, "waited: "); }}}}Copy the code

Seven,

In conclusion,

  • commitandapplyOne has a return value, one doesn’t, one commits synchronously, one commits asynchronously.
  • Too many keys in a file will cause a large file, slow loading, and slow writing. It blocks the main thread while loading data, causing it to stall, or worseANRThat can bekeySplit files by function.SPMemory will not be recycled, so many files will not do, itself positioning is small data storage.
  • Cannot be used in multiple processes
  • Use results when you don’t need themapply.commitIt writes files to the main thread. butapplyIt’s not necessarily reliablewaitToFinishDescribed in.
  • Each version may be implemented differently, perform differently, and increasebugRepair difficulty, so similar to putJetpackIndividually upgradable feel the need.

Reference for this article:

A comprehensive profile of SharedPreferences

Don’t abuse SharedPreferences

SharedPreferences ANR Problem Analysis & Optimization for Android 8.0

Analyze the ANR problem caused by SharedPreference apply