Introduction to the

SharedPreferences(SP) is a popular data storage method in Android. SP adopts key-value (key-value pair). However, it is not recommended to use SP to store large-scale data, which may degrade performance.

The problem

  1. Why not store large keys and values that cause interface cards?
  2. What is the difference between Apply and commit?
  3. Is the write to disk incremental or full?

use

private void putValue() {
    SharedPreferences sharedPreferences = getSharedPreferences("gityuan", Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.putString("blog"."www.gityuan.com");
    editor.putInt("years".3);
    editor.commit();
}

private String getValue() {
    SharedPreferences sharedPreferences = getSharedPreferences("gityuan", Context.MODE_PRIVATE);
    return sharedPreferences.getString("blog"."null");
}
Copy the code

Source code analysis

Get SharedPreferences

ContextImpl.java


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<>();
        }
        (1) get the File File of the face name = mSharedPrefsPaths.if (file == null) {(2File = getSharedPreferencesPath(name); (3) toMapMSharedPrefsPaths. Put (name, file); }} (4) Look at this method.return getSharedPreferences(file, mode);
}

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
Copy the code
@Override
public SharedPreferences getSharedPreferences(File file, int mode){ SharedPreferencesImpl sp; Synchronized (contextimpl.class) {final ArrayMap<File, SharedPreferencesImpl> cache = (contextimpl.class)1) look at this method getSharedPreferencesCacheLocked (); sp = cache.get(file);if (sp == null) {
            checkMode(mode);
            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"); }} (2Create a SharedPreferencesImpl. Let's see what's going on in the constructor. sp =newSharedPreferencesImpl(file, mode); (3) on theMapIn the cache. The put (file, sp);returnsp; }} (4If you specify multi-process mode, the file will be reloaded when it is changed by another process.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;
}

@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final StringpackageName = getPackageName(); (1To get the current package nameMap<File, SharedPreferencesImpl>
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = 
sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}
Copy the code

Each App corresponds to multiple (name -> File -> SharedPreferencesImpl).

SharedPreferencesImpl initialization

@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode){ mFile = file; (1MBackupFile = makeBackupFile(file); mMode = mode; mLoaded =false;
    mMap = null;
    mThrowable = null; (2StartLoadFromDisk (); }Copy the code
private void startLoadFromDisk(){synchronized (mLock) {(1) flag bit loaded into memory mLoaded =false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}
Copy the code
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        (1If the backup File exists, write the memory of the backup File to Fileif(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) {
        // 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; (1MStatTimestamp = stat.st_mtim; (2MStatSize = stat.st_size; (3Update 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(); (4Wake up the waiting thread}}}Copy the code

To get the data

@Nullable
public String getString(String key, @Nullable String defValue){synchronized (mLock) {(1Wait for the disk to load into memory successfully awaitLoadedLocked();String v = (String)mMap.get(key);
        returnv ! =null ? v : defValue;
    }
}

@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) {try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if(mThrowable ! =null) {
        throw newIllegalStateException(mThrowable); }}Copy the code

Fetching data will wait until the main thread finishes loading disk data into memory.

Editor

SharedPreferences.Editor editor = sharedPreferences.edit();

@Override
public Editor edit(){(1Synchronized (mLock) {awaitLoadedLocked(); }return new EditorImpl();
}
Copy the code
public final class EditorImpl implements Editor {
    private final Map<String.Object> mModified = Maps.newHashMap();
    private boolean mClear = false;

    // Insert data
    public Editor putString(String key, @Nullable String value) {
        synchronized (this) {
            // Insert data into the mModified object
            mModified.put(key, value);
            return this; }}// Remove data
    public Editor remove(String key) {
        synchronized (this) {
            mModified.put(key, this);
            return this; }}// Clear all data
    public Editor clear() {
        synchronized (this) {
            mClear = true;
            return this; }}}Copy the code

Both Commit and Apply simply store data in memory first.

The data submitted

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

    if(DEBUG) { startTime = System.currentTimeMillis(); } (1Update data to memory MemoryCommitResult MCR = commitToMemory(); (2) memory data synchronization to file SharedPreferencesImpl. Enclosing enqueueDiskWrite (MCR,null /* sync write on this thread okay */);
    try{(3) has reached the awaited state, until the completion of written to the file of MCR. WrittenToDiskLatch. Await (); }catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms"); }} (4) to inform the listener, and in the main thread callback onSharedPreferenceChanged () method. notifyListeners(mcr); (5) returns the result of writing to diskreturn mcr.writeToDiskResult;
}
Copy the code
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    long memoryStateGeneration;
    boolean keysCleared = false;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String.Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        // We optimistically don't make a deep copy until
        // a memory commit comes in when we're already
        // writing to disk.
        if (mDiskWritesInFlight > 0) {
            // We can't modify our mMap as a currently
            // in-flight write owns it. Clone it before
            // modifying it.
            // noinspection unchecked
            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; (1) when mCleartrue, the mMap is clearedif (mClear) {
                if(! mapToWriteToDisk.isEmpty()) { changesMade =true;
                    mapToWriteToDisk.clear();
                }
                keysCleared = true;
                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.2) Notice herethisIs a special value used to remove the corresponding key operation. If the value isnullRemove the corresponding 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); } (3ChangesMade = Indicates that the data has changedtrue;
                if(hasListeners) {(4) Records the changed Key. keysModified.add(k); }} (5Clear mModified data in EditorImpl mModified.clear();if(changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; }}return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
            listeners, mapToWriteToDisk);
}
Copy the code
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable){(1Final Boolean isFromSyncCommit = (postWriteRunnable ==null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run(){synchronized (mWritingToDiskLock) {(2WriteToFile (MCR, isFromSyncCommit); } synchronized (mLock) { mDiskWritesInFlight--; }if(postWriteRunnable ! =null) { postWriteRunnable.run(); }}};// Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false; Synchronized (mLock) {(3) write memory plus1, write to disk minus1
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return; } } QueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit); }Copy the code
@GuardedBy("mWritingToDiskLock")
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
        if(mDiskStateGeneration < MCR. MemoryStateGeneration) {(1Commit will need to writeif (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; }}}} (2No write is required and success is returned immediatelyif(! needsWrite) { mcr.setDiskWriteResult(false.true);
            return;
        }

        boolean backupFileExists = mBackupFile.exists();

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

        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; } (5) write xmLutils.writemapxml (McR.maptowritetodisk, STR) to disk; 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) {
            // Do nothing
        }

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

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

        if(DEBUG) { deleteTime = System.currentTimeMillis(); } mDiskStateGeneration = mcr.memoryStateGeneration; (3Write success 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
    if (mFile.exists()) {
        if(! mFile.delete()) { Log.e(TAG,"Couldn't clean up partially-written file "+ mFile); }} (4Write failure McR.setdiskwriteresult (false.false);
}
Copy the code

Data is written to the disk only when it changes.

Write to disk.

public static final void writeMapXml(Map val, XmlSerializer out,
        WriteMapCallback callback) throws XmlPullParserException, java.io.IOException {
    if (val == null) {
        return;
    }

    Set s = val.entrySet();
    Iterator i = s.iterator();

    while (i.hasNext()) {
        Map.Entry e = (Map.Entry)i.next();
        writeValueXml(e.getValue(), (String)e.getKey(), out, callback); }}Copy the code

apply

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

    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"); }}}; (1Queuedwork.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);
}
Copy the code
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);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if(postWriteRunnable ! =null) { postWriteRunnable.run(); }}};// Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return; }} (1Queuedwork. queue(writeToDiskRunnable,! isFromSyncCommit); }Copy the code
public static void queue(Runnable work, boolean shouldDelay){(1Handler Handler = getHandler(); synchronized (sLock) { sWork.add(work); (2) apply Normally sends a delayed message. (Remove Activity to close)if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

@UnsupportedAppUsage
private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {(1A child thread is created to process the message HandlerThread HandlerThread =new HandlerThread("queued-work-looper",
                    Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

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

Conclusion:

  1. Why not store large keys and values that cause interface cards?
  2. What is the difference between Apply and commit?
  3. Is the write to disk incremental or full?

First question:

The first time getSharedPreferences is read the disk file in the main thread. If the file is too large, the main thread will stall.

Second question:

  1. Apply () writes to disk asynchronously, while commit() writes to disk synchronously. However, when the Activity Service BroadCastReceiver (BroadCastReceiver) is BroadCastReceiver, all pending disk writes are put on the main thread, and apply() is used for synchronous writes.

  2. Apply has no return value, and commit has a return value to know if the change was committed successfully.

Third question:

It’s written in full. Take a look at the source code analysis for the write to disk part of the code.

Conclusion:

  1. There are a lot of locks inside, so it must be inefficient.

Reference:

  • SharedPreferences source

  • A comprehensive profile of SharedPreferences