Android SharedPreferences

Usage

SharedPreferences preferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE);
preference.edit().putInt(strKey, nValue).apply();
//or
preference.edit().putInt(strKey, nValue).commit();

Context

  • getSharedPreferences

abstract method and return SharedPreferences object

ContextImpl extends Context

  • getSharedPreferences

override method in Context

  • getSharedPreferencesPath

create new directory for xml file

  • getPreferencesDir
mPreferencesDir = new File(getDataDir(), "shared_prefs");
  • getDataDir

get data directory,refer to /data/data/packagename/.And entire directory is /data/data/packagename/shared_prefs/

  • makeFilename

entire arguments is:

makeFilename(getPreferencesDir(), name + ".xml");

the file and name will put into map:

ArrayMap<String, File> mSharedPrefsPaths.put(name, file);
  • getSharedPreferences

the arguments is file and mode.

  • getSharedPreferencesCacheLocked

init ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache and ArrayMap<File, SharedPreferencesImpl> packagePrefs,and put packagePrefs in sSharedPrefsCache with packageName as key.

  • checkMode
    codes:
private void checkMode(int mode) {
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
        if ((mode & MODE_WORLD_READABLE) != 0) {
            throw new SecurityException("MODE_WORLD_READABLE no longer supported");
        }
        if ((mode & MODE_WORLD_WRITEABLE) != 0) {
            throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
        }
    }
}
  • SharedPreferencesImpl
    constructor method invoke
    and ArrayMap<File, SharedPreferencesImpl> cache put SharedPreferencesImpl object with file as key.

SharedPreferencesImpl

implements SharedPreferences interface and accomplish its methods.

  • SharedPreferencesImpl()
  • makeBackupFile

create temp file with bak with trail.

static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}
  • startLoadFromDisk
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}
  • loadFromDisk

load file from disk to memory in a map,in the method,we can find:

Map<String, Object> map = (Map<String, Object>) XmlUtils.readMapXml(str);
Map<String, Object> mMap = map;

at the end of getSharedPreferences,we find that MODE_MULTI_PROCESS property is making effect to version which less than 11,or useless if you set it,and below 11,it just loads data from disk again.

Editor

After getSharedPreferences method,we will invoke edit() to get a object of Editor class to commit or apply.
Editor is a inner interface in SharedPreferences class and will be implemented in SharedPreferencesImpl.

  • edit

return Editor object,and code is blocked by a lock object mLock.

  • awaitLoadedLocked
while (!mLoaded) {
    try {
        mLock.wait();
    } catch (InterruptedException unused) {
    }
}

mLoaded property is initialized in loadFromDisk method,and this means that before data is loaded from disk it will not be got Editor operator,means process is blocked.

  • EditorImpl

invoke EditorImpl constructor to get its object.

EditorImpl

implements methods in Editor interface.

  • putString

put data through put method,for instance:putInt,putLong,putFloat,putBoolean and so on.

synchronized (mEditorLock) {
    mModified.put(key, value);
    return this;
}
  • commit

invoke commitToMemory first and enqueueDiskWrite in SharedPreferencesImpl class and notifyListeners with MemoryCommitResult argument.

  • commitToMemory
    foreach Map object of mModified and put key and value in mapToWriteToDisk object which key is String and value type is Object.
Map<String, Object> mapToWriteToDisk = mMap;
for (Map.Entry<String, Object> e : mModified.entrySet()) {
    String k = e.getKey();
    Object v = e.getValue();
    mapToWriteToDisk.put(k, v);
}

and then init a new object:new MemoryCommitResult.

MemoryCommitResult

this class blocked some arguments and returned to commitToMemory method,and changed data are set to mapToWriteToDisk in MemoryCommitResult class.

Back to commit method,the next method is enqueueDiskWrit in commit.

SharedPreferencesImpl

  • enqueueDiskWrite
    this method is defined in SharedPreferencesImpl class and do a favour to write data in xml file.
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;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

the postWriteRunnable argument will be null and isFromSyncCommit is true,what’s more,mDiskWritesInFlight argument is self-added,wasEmpty is true.Naturelly,the thread writeToDiskRunnable will be run.
The thread of writeToDiskRunnable does work for saving data with writeToFile method and self-decreased mDiskWritesInFlight argument.

  • writeToFile
    The vital codes are:
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
  • writeMapXml
public static final void writeMapXml(Map val, String name, XmlSerializer out,WriteMapCallback callback) throws XmlPullParserException, java.io.IOException {
    if (val == null) {
        out.startTag(null, "null");
        out.endTag(null, "null");
        return;
    }

    out.startTag(null, "map");
    if (name != null) {
        out.attribute(null, "name", name);
    }

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

    out.endTag(null, "map");
}

we can figure out that the step is getting out all content from file and then transfered to be FileOutputStream.The changed key and value pair will be written to xml file.
The process indicates that we can not set numerous and complex data with commit method.

After execute enqueueDiskWrite method in commit method,we will find out that a CountDownLatch object will execute await method util it is counted down to zero.

try {
    mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
    return false;
} finally {
    if (DEBUG) {
        Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                + " committed after " + (System.currentTimeMillis() - startTime)
                + " ms");
    }
}

And at the end of writeToFile method,it will invoke:

mcr.setDiskWriteResult(true, true);
void setDiskWriteResult(boolean wasWritten, boolean result) {
    this.wasWritten = wasWritten;
    writeToDiskResult = result;
    writtenToDiskLatch.countDown();
}

writtenToDiskLatch.countDown(); means that value has been to zero,the next step can be execute,and commit result will be returned.

  • apply
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);
    notifyListeners(mcr);
}

the difference between commit is that postWriteRunnable argument to enqueueDiskWrite method is not null,and this runnable will be added to a QueueWork class:

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
  • queue
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);
        }
    }
}

The runnable will be executed in handler with one by one,the lopper of handler is HandlerThread’s looper…After writeToFile executed,the postWriteRunnable will run,this thread only invoke awaitCommit.run(),awaitCommit is awaitCommitRunnable,it contains
mcr.writtenToDiskLatch.await();,before this method is invoked,the result may be returned.In enqueueDiskWrite method:

final Runnable writeToDiskRunnable = new Runnable() {
    @Override
    public void run() {
        synchronized (mWritingToDiskLock) {
            writeToFile(mcr, isFromSyncCommit);
        }
        synchronized code (mLock) {
            mDiskWritesInFlight--;
        }
        if (postWriteRunnable != null) {
            postWriteRunnable.run();
        }
    }
};

postWriteRunnable.run();is not included in synchronized codes which can be executed firstly.

Invoking apply method can not ensure that our data is written in a efficient way,it may return result to invoker before data is written in xml file.If apply method is invoked many times in a time,the runnable will be waited in line to be invoked,thus,we should put values in editor in a time and then apply.

In ActivityThread,during operate onStop lifecycle,it will check QueuedWork state when sdk version more than HONEYCOMB(11):

public void handleStopActivity(IBinder token, boolean show, int configChanges,
            PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
    final ActivityClientRecord r = mActivities.get(token);
    r.activity.mConfigChangeFlags |= configChanges;

    final StopInfo stopInfo = new StopInfo();
    performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
            reason);

    if (localLOGV) Slog.v(
        TAG, "Finishing stop of " + r + ": show=" + show
        + " win=" + r.window);

    updateVisibility(r, show);

    // Make sure any pending writes are now committed.
    if (!r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }

    stopInfo.setActivity(r);
    stopInfo.setState(r.state);
    stopInfo.setPersistentState(r.persistentState);
    pendingActions.setStopInfo(stopInfo);
    mSomeActivitiesChanged = true;
}

in QueuedWork.waitToFinish()method,it will iterate all runnable in sWork and then invoke,before all runnable completing it will not return which means onStop method can be stucked before data is written in disk.

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

    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // Delayed work will be processed at processPendingWork() below
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);

            if (DEBUG) {
                hadMessages = true;
                Log.d(LOG_TAG, "waiting");
            }
        }

        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }

    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        processPendingWork();
    } finally {
        StrictMode.setThreadPolicy(oldPolicy);
    }

    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }
}

Conclusion

Android SharedPreferences
Real Size Picture Address:https://i.loli.net/2019/04/26/5cc2ba881badf.jpg
And article will be synced to wechat blog:Android部落格.