[TOC]

The profile

SharedPreferences(SP) is a common data storage method in Android. SP adopts key-value (key-value pair) and is mainly used for lightweight data storage, especially for saving application configuration parameters

use

SharedPreferences sharedPreferences = getSharedPreferences("setting", Context.MODE_PRIVATE); Editor editor = sharedPreferences.edit(); Editor. putString("current_city", "Beijing "); editor.commit();Copy the code

/data/data/<package_name>/shared_prefs/setting.xml

<? XML version='1.0' Encoding =' UTF-8 'standalone='yes'? > <map> <string name="current_city"> Beijing </string> </map>Copy the code

The overall structure

The key point

  • Get SP, no files will be created
  • Getting the SP actually creates the SharedPreferencesImpl, which loads the file contents into memory

Access to SP

getSharedPreferences

Key points:

  • Files are created when XML is not available
  • The returned SP is actually SharedPreferencesImpl
  • Cache location after SP file is opened
    • ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache
    • The key is package_name
class ContextImpl extends Context { private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache; private ArrayMap<String, File> mSharedPrefsPaths; public SharedPreferences getSharedPreferences(String name, int mode) { File file; synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths file = mSharedPrefsPaths. Get (name); If (file == null) {// If the file does not exist, create a new file, The file path is /data/data/package name/shared_prefs/ file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode); } } public File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); } private File getPreferencesDir() {synchronized (mSync) {if (mPreferencesDir == null) {mPreferencesDir = new File(getDataDir(), "shared_prefs"); } return ensurePrivateDirExists(mPreferencesDir); } } public SharedPreferences getSharedPreferences(File file, int mode) { checkMode(mode); SharedPreferencesImpl sp; synchronized (ContextImpl.class) { final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); sp = cache.get(file); If (sp == null) {// Create SharedPreferencesImpl sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; }} // If ((mode & context.mode_multi_process)! = 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { sp.startReloadIfChangedUnexpectedly(); } return sp; } 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; }Copy the code

SharedPreferencesImpl must be created

Load and initialize

  • The SharedPreference file is loaded asynchronously to take advantage of this preloading
  • Read THE KV of the XML into a map, so if the XML is large, it will take up a lot of memory
    • Each initialization will load all kv into memory, and commit will update all data to the file, so the entire file should not be too large, otherwise it will affect performance.
    • Split: The kv that is used frequently and infrequently, read frequently and write frequently is stored separately
SharedPreferencesImpl(File file, int mode) { mFile = file; // Create a backup file with the suffix. Bak mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; startLoadFromDisk(); } private void startLoadFromDisk() { synchronized (this) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); } private void loadFromDisk() { synchronized (SharedPreferencesImpl.this) { if (mLoaded) { return; } if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } } Map map = null; StructStat stat = null; try { stat = Os.stat(mFile.getPath()); if (mFile.canRead()) { BufferedInputStream str = null; try { str = new BufferedInputStream(new FileInputStream(mFile), 16*1024); map = XmlUtils.readMapXml(str); } catch (XmlPullParserException | IOException e) { ... } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { ... } synchronized (SharedPreferencesImpl.this) { mLoaded = true; if (map ! = null) { mMap = map; // Save the information read from the file to mMap mStatTimestamp = stat.st_mtime; // Update time mStatSize = stat.st_size; } else {mMap = new HashMap<>(); } notifyAll(); // Wake up the waiting thread}}Copy the code

read

If the load is not complete, it waits for a lock and the calling thread blocks

Public String getString(String key, @nullable String defValue) {synchronized (this) {// Check that awaitLoadedLocked(); String v = (String)mMap.get(key); return v ! = null ? v : defValue; } } 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 new IllegalStateException(mThrowable); }}Copy the code

write

You can see that an mModified map is actually created to record what is being modified

public Editor edit() { synchronized (this) { awaitLoadedLocked(); } return new EditorImpl(); } public final class EditorImpl implements Editor { private final Map<String, Object> mModified = Maps.newHashMap(); private boolean mClear = false; Public Editor putString(String key, @nullable String value) {synchronized (this) { Mmodified. put(key, value); return this; }} public Editor remove(String key) {synchronized (this) {mmodified.put (key, this); return this; Public Editor clear() {synchronized (this) {mClear = true; return this; }}}Copy the code

Trading commit the apply

commit

Key points:

  • Merge KV map and mModified memory

  • The commit execution is isFromSyncCommit, which runs the runnable directly from the falling disk. It’s a synchronous process.

  • Commit has a return value

  • Do not commit multiple times in a row. Instead, get edit() once and execute putxxx() several times before committing in batches, especially when encapsulating

Public Boolean commit() {// Update data to memory MemoryCommitResult MCR = commitToMemory(); / / memory data synchronization to file SharedPreferencesImpl. The enclosing enqueueDiskWrite (MCR, null); Try {/ / has reached the awaited state, until written to the file of operation to complete MCR. WrittenToDiskLatch. Await (); } catch (InterruptedException e) { return false; } / / notification, monitoring, and in the main thread callback onSharedPreferenceChanged notifyListeners () method (MCR); // Return MCR. WriteToDiskResult; } private MemoryCommitResult commitToMemory() { MemoryCommitResult mcr = new MemoryCommitResult(); synchronized (SharedPreferencesImpl.this) { if (mDiskWritesInFlight > 0) { mMap = new HashMap<String, Object>(mMap); } // this is the map to write to disk McR.maptowritetodisk = mMap; mDiskWritesInFlight++; Boolean hasListeners = mListeners. Size () > 0; if (hasListeners) { mcr.keysModified = new ArrayList<String>(); mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (this) {mMap if (mClear) {if (! mMap.isEmpty()) { mcr.changesMade = true; mMap.clear(); } mClear = false; } for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); / / note here, this is a special value, is used to remove the corresponding key operations. If (v = = this | | v = = null) {if (! mMap.containsKey(k)) { continue; } mMap.remove(k); } else { if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); if (existingValue ! = null && existingValue.equals(v)) { continue; } } mMap.put(k, v); } mcr.changesMade = true; // changesMade indicates whether changes have been made on the record if (hasListeners) {McR.keysmodified.add (k); // Record the changed key}} mmodified.clear (); // Delete mModified data in EditorImpl}} return MCR; } private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { final Runnable writeToDiskRunnable = new Runnable() { public void run() { Synchronized (mWritingToDiskLock) {// writeToFile(MCR); } synchronized (SharedPreferencesImpl.this) { mDiskWritesInFlight--; } // If (postWriteRunnable! = null) { postWriteRunnable.run(); }}}; final boolean isFromSyncCommit = (postWriteRunnable == null); If (isFromSyncCommit) {// Commit will enter the branch Boolean wasEmpty = false; Synchronized (SharedPreferencesImpl. This) {/ / commitToMemory process will add 1, wasEmpty = true wasEmpty = mDiskWritesInFlight = = 1; } if (wasEmpty) {writeToDiskRunnable.run(); return; }} / / does not perform the method QueuedWork singleThreadExecutor (). The execute (writeToDiskRunnable); }Copy the code

apply

The actual execution is done using HandlerThread

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"); }}}; 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); }Copy the code
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
Copy the code
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);
        }
    }
}

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;
    }
}
Copy the code

Which one to use

  • Apply () returns no value and commit() returns Boolean indicating whether the change was committed successfully
  • Commit () commits content synchronously to hard disk, whereas Apply () commits changes immediately to memory, then starts an asynchronous thread to commit to hard disk, and you are not notified if the commit fails.
  • Get operations are thread-safe, and GET only retrieves data from memory (mMap)

The problem

Is SharedPreferences thread-safe

  • SharePreferences are thread-safe methods that are guaranteed by lots of synchronized (inefficient, lots of synchronized), but not process-safe

ANR

  • All GET/SET requests are stuck until the first read, so there is ANR risk on the first read.

  • Even apply, which ends without a drop, may ANR

conclusion

  • The SharedPreference file is loaded asynchronously to take advantage of this preloading
  • Each initialization will load all kv into memory, and commit will update all data to the file, so the entire file should not be too large, otherwise it will affect performance. Splitting principle: The kv that is frequently used and infrequently used shall be stored separately, and the KV that is frequently read and written shall be stored separately
  • Do not commit multiple times in a row. Instead, get edit() once and execute putxxx() several times before committing in batches, especially when encapsulating
  • Apply commits asynchronously, but causes caton /ANR

Alternative to SP: MMKV

  • Github – MMKV principle

Memory to prepare

The MMAP memory mapping file provides a memory block that can be written at any time. App only writes data into it, and the operating system is responsible for writing the memory back to the file. There is no need to worry about data loss caused by crash.

Time when Mmap writes back to disk

  • Out of memory
  • Process exits
  • Call msync and munmap
  • After a certain delay time

Data organization

With respect to data serialization, we use protobuf protocol, and PB has a good performance in performance and space occupancy. Considering that we are providing a general KV component, key can be limited to string and value can be varied (int/bool/double, etc.). To be generic, consider serializing values into a unified buffer using the Protobuf protocol, which can then serialize these KV objects into memory.