A recent task is to optimize the application startup time, and then record the time occupied by each step of the system startup. One method is to operate SharedPreferences, where only two keys are read, then I updated the value and wrote it back. It took more than MS (when the application was first installed). I was very surprised. In the past, I only vaguely knew that SharedPreferences correspond to an xml file on the hard disk. The specific implementation has not been studied yet. Let's take a look at what SharedPreferences are, why is efficiency so low?
SharedPreferences are stored in ContextImpl. Therefore, first write the ContextImpl class:
ContextImpl. java (https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/ContextImpl.java ):
/*** Map from package name, to preference name, to cached preferences. */private static ArrayMap
> SSharedPrefs; // a copy of the cache in the memory @ Overridepublic SharedPreferences getSharedPreferences (String name, int mode) {SharedPreferencesImpl sp; synchronized (ContextImpl. class) {// synchronous if (sSharedPrefs = null) {sSharedPrefs = new ArrayMap
> ();} Final String packageName = getPackageName (); ArrayMap
PackagePrefs = sSharedPrefs. get (packageName); if (packagePrefs = null) {packagePrefs = new ArrayMap
(); SSharedPrefs. put (packageName, packagePrefs);} // 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 wocould stringify it to "null. xml ". nice. if (mPackageInfo.getApplicationInfo().tar getSdkVersion <Build. VERSION_CODES.KITKAT) {if (name = null) {name = "null" ;}} sp = packagePrefs. get (name); if (sp = n Ull) {File prefsFile = getSharedPrefsFile (name); // find the File sp = new SharedPreferencesImpl (prefsFile, mode). // Initialization is performed here, load data packagePrefs from the hard disk. put (name, sp); // return sp ;}} if (mode & Context. MODE_MULTI_PROCESS )! = 0 | getApplicationInfo().tar getSdkVersion <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 unencrypted ented) behavior. sp. startReloadIfChangedUnexpectedly ();} return sp ;}
GetSharedPreferences () is easy to understand. Let's focus on SharedPreferencesImpl. java:
SharedPreferencesImpl. java (https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/SharedPreferencesImpl.java)
The first is the constructor:
SharedPreferencesImpl (File file, int mode) {mFile = file; // This is the file mBackupFile on the hard disk = makeBackupFile (File); // This is the backup file, when a crash occurs in the mFile, mBackupFile is used to replace mMode = mode; // This is the open mode mLoaded = false; // This is a flag bit and whether the file is loaded successfully, because file loading is an asynchronous process, mMap = null; // use startLoadFromDisk () to save data; // start asynchronous loading from the hard disk} // two important members are also involved: private int mDiskWritesInFlight = 0; // Number of write operations with no commit to disk in batches. Each batch may correspond to multiple k-vprivate final Object mWritingToDiskLock = new Object (); // lock when writing Hard Disk Files
// Load private void startLoadFromDisk () {synchronized (this) {// first set the status to not loaded mLoaded = false;} new Thread ("SharedPreferencesImpl-load ") {// starts a thread and asynchronously loads public void run () {synchronized (SharedPreferencesImpl. this) {loadFromDiskLocked (); // By SharedPreferencesImpl. this lock protection }}}. start ();}
// Load private void loadFromDiskLocked () {if (mLoaded) {// if it has been loaded, directly exit return;} if (mBackupFile. exists () {// If a backup file exists, use the backup file mFile first. delete (); mBackupFile. renameTo (mFile);} // Debugging if (mFile. exists ()&&! MFile. canRead () {Log. w (TAG, "Attempt to read preferences file" + mFile + "without permission");} Map map = null; StructStat stat = null; try {stat = Libcore. OS. stat (mFile. getPath (); if (mFile. canRead () {BufferedInputStream str = null; try {str = new BufferedInputStream (new FileInputStream (mFile), 16*1024); // read data from the hard disk map = XmlUtils. readMapXml (str); // perform xml parsing} catch (XmlPullParserException E) {Log. w (TAG, "getSharedPreferences", e);} catch (FileNotFoundException e) {Log. w (TAG, "getSharedPreferences", e);} catch (IOException e) {Log. w (TAG, "getSharedPreferences", e);} finally {IoUtils. closeQuietly (str) ;}} catch (ErrnoException e) {} mLoaded = true; // set the flag, if (map! = Null) {mMap = map; // save it to mMap mStatTimestamp = stat. st_mtime; // The time stamp of the record file mStatSize = stat. st_size; // record file size} else {mMap = new HashMap
();} Yyall (); // wake up the waiting thread}
Then we can read a read request at will:
Public int getInt (String key, int defValue) {synchronized (this) {// you must first obtain this lock awaitLoadedLocked (); // After completing this step, it indicates that Integer v = (Integer) mMap has been loaded. get (key); // read return v directly from memory! = Null? V: defValue ;}}
// Wait until the data load is complete. private void awaitLoadedLocked () {if (! MLoaded) {// if it has not been loaded // 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 (); // load from hard disk} while (! MLoaded) {// If the try {wait (); // and so on} catch (InterruptedException unused ){}}}
Let's take a look at the write operation. The write is done through the Editor:
Public Editor edit () {// TODO: remove the need to call awaitLoadedLocked () when // requesting an editor. will require some work on the // Editor, but then we shoshould be able to do: ///context. getSharedPreferences (..). edit (). putString (..). apply ()////... all without blocking. // The annotation is very interesting. You can remove this synchronization when getting edit, but you need to do some work on the Editor (???). // However, the benefit is context. getSharedPreferences (..). edit (). putString (..). apply () does not block synchronized (this) {// wait for loading to complete awaitLoadedLocked ();} return new EditorImpl (); // return an EditorImpl, it is an internal class}
Public final class EditorImpl implements Editor {// The write operation will temporarily put the data here private final Map
MModified = Maps. newHashMap (); // protected by this lock // whether to clear all preferences private boolean mClear = false; public Editor putInt (String key, int value) {synchronized (this) {// first obtain this lock mModified. put (key, value); // instead of directly modifying mMap, put it in mModified and return this ;}}}
Take a look at commit:
Public boolean commit () {MemoryCommitResult mcr = commitToMemory (); // submit it to the memory SharedPreferencesImpl first. this. enqueueDiskWrite (mcr, null/* sync write on this thread okay */); // then submit it to the hard disk try {mcr. writtenToDiskLatch. await (); // wait for Hard Disk Writing to complete} catch (InterruptedException e) {return false;} yylisteners (mcr); return mcr. writeToDiskResult ;}
The commitToMemory () method is mainly used to update the mMap of the memory cache:
// Returns true if any changes were madeprivate MemoryCommitResult commitToMemory () {MemoryCommitResult mcr = new MemoryCommitResult (); synchronized (SharedPreferencesImpl. this) {// Add the SharedPreferencesImpl lock, when writing memory, it is not allowed to read // We optimistically don't make a deep copy until a memory commit comes in when we're already writing to disk. if (mDiskWritesInFlight> 0) {// if no write is submitted, mDiskWritesInFlight is a member variable of SharedPreferences // We can't modify our mMap as a currently in-flight write owns it. clone it before modifying it. // noinspection unchecked mMap = new HashMap
(MMap); // clone an mMap, not clear! } Mcr. mapToWriteToDisk = mMap; mDiskWritesInFlight ++; // Add 1 boolean hasListeners = mListeners. size ()> 0; if (hasListeners) {mcr. keysModified = new ArrayList
(); Mcr. listeners = new HashSet
(MListeners. keySet ();} synchronized (this) {// lock the current Editor if (mClear) {// only when clear () is called () set this value to true if (! MMap. isEmpty () {// If mMap is not empty mcr. changesMade = true; mMap. clear (); // clear mMap. MMap stores the entire Preferences} mClear = false;} for (Map. Entry
E: mModified. entrySet () {// traverses all the entry String k = e. getKey (); Object v = e. getValue (); if (v = this) {// magic value for a removal mutation if (! MMap. containsKey (k) {continue;} mMap. remove (k);} else {boolean isSame = false; if (mMap. containsKey (k) {Object existingValue = mMap. get (k); if (existingValue! = Null & existingValue. equals (v) {continue ;}} mMap. put (k, v); // put it inside, because the outermost layer has a pair of SharedPreferencesImpl. this locks and writes are OK} mcr. changesMade = true; if (hasListeners) {mcr. keysModified. add (k) ;}} mModified. clear (); // clear editor} return mcr ;}
// This is the subsequent hard drive private void commit (final MemoryCommitResult mcr, final Runnable failed) {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 it is commit, postWriteRunnable is null // Typical # commit () path with fewer allocations, doing a write on // the current thread. if (isFromSyncCommit) {// if it is the called commit boolean wasEmpty = false; synchronized (SharedPreferencesImpl. this) {wasEmpty = mDiskWritesInFlight = 1; // if only one batch is waiting for writing} if (wasEmpty) {WriteToDiskRunnable. run (); // run directly in the current thread without starting another thread. nice optimization! Return ;}} // if it is not the called commit, it will go down the branch. // If there are multiple batches waiting for writing, start another thread to write, it can be seen from the method name that it is also a serial writing, and the writing file should be serialized! Queuedwork.singlethreadexecutor(cmd.exe cute (writeToDiskRunnable );}
Let's take a look at what writeToDiskRunnable has done:
Final Runnable writeToDiskRunnable = new Runnable () {// This is working on another thread public void run () {synchronized (mWritingToDiskLock) {// mWritingToDiskLock is a member variable of SharedPreferencesImpl, ensure that files are written in a single thread. // this lock cannot be used because there may be multiple commit or apply/locks on the editor, and the SharedPreferences lock cannot be used because it will block reading, which is good! WriteToFile (mcr); // write to file} synchronized (SharedPreferencesImpl. this) {mDiskWritesInFlight --; // batch minus 1} if (postWriteRunnable! = Null) {postWriteRunnable. run (); // This is the callback after writing }}};
The following figure shows the hard drive:
// Note: must hold mWritingToDiskLock private void writeToFile (MemoryCommitResult mcr) {// Rename the current file so it may be used as a backup during the next read if (mFile. exists () {if (! Mcr. changesMade) {// If no modification is made, directly return // If the file already exists, but no changes were // made to the underlying map, it's wasteful to // re-write the file. return as if we wrote it // out. mcr. setDiskWriteResult (true); return;} if (! MBackupFile. exists () {// back up if (! MFile. renameTo (mBackupFile) {Log. e (TAG, "Couldn't rename file" + mFile + "to backup file" + mBackupFile); mcr. setDiskWriteResult (false); return ;}} else {// Delete and recreate the 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 (str = null) {mcr. setDiskWriteResult (false); return;} XmlUtils. writeMapXml (mcr. mapToWriteToDisk, str); FileUtils. sync (str); // forcibly write to the hard drive str. close (); ContextImpl. setFilePermissionsFromMode (mFile. getPath (), mMode, 0); try {final StructStat stat = Libcore. OS. stat (mFile. getPath (); synchronized (this) {mStatTimestamp = stat. st_mtime; // update the file timestamp mStatSize = stat. st_siz E; // Update file size} catch (ErrnoException e) {// Do nothing} // Writing was successful, delete the backup file if there is one. mBackupFile. delete (); mcr. setDiskWriteResult (true); 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. exist S () {if (! MFile. delete () {Log. e (TAG, "Couldn't clean up partially-written file" + mFile) ;}} mcr. setDiskWriteResult (false );}
Public static boolean sync (FileOutputStream stream) {try {if (stream! = Null) {stream. getFD (). sync (); // force write hard disk} return true;} catch (IOException e) {} return false ;}
Here, another method that looks very similar to commit is apply ():
Public void apply () {final MemoryCommitResult mcr = commitToMemory (); // It is also submitted to the memory final Runnable awaitCommit = new Runnable () {public void run () {try {mcr. writtenToDiskLatch. await (); // wait for writing to the hard disk} catch (InterruptedException ignored) {}}; QueuedWork. add (awaitCommit); Runnable postWriteRunnable = new Runnable () {public void run () {awaitCommit. run (); QueuedWork. remove (awaitCommit) ;}}; SharedPreferencesImpl. this. enqueueDiskWrite (mcr, postWriteRunnable ); // the postWriteRunnable passed here is no longer null // Okay to handle y the listeners before it's hit disk // because the listeners shoshould always get the same // SharedPreferences instance back, which has the // changes reflected in memory. notifyListeners (mcr );}
We have read the enqueueDiskWrite () method. Because the postWriteRunnable parameter is not null, the following code is executed:
Queuedwork.singlethreadexecutor(cmd.exe cute (writeToDiskRunnable );
This is to write the hard disk on a separate thread. After writing it, it will call back postWriteRunnable, waiting for the hard disk to be written!
From the above code, we can draw the following conclusions:
(1) SharedPreferences reads files asynchronously from the hard disk during the first loading, and then caches the files in the memory.
(2) read by SharedPreferences is the READ memory cache.
(3) For commmit () writing, the data is first updated to the memory and then synchronized to the hard disk. The whole process is done in the same thread.
(4) If it is an apply () write, it is also written to the memory first, but another thread will write the hard disk asynchronously. Because we read data directly from the memory during reading, using apply () instead of commit () will improve the performance.
(5) If multiple keys are to be written, do not use commit or apply each time, because there will be a lot of lock operations, and the more efficient way is as follows: editor. putInt ("",""). putString ("",""). putBoolean ("",""). apply (); and all putXXX () returns this at the end to facilitate chained programming.
(6) There are three levels of locks: SharedPreferences, Editor, and mWritingToDiskLock.
MWritingToDiskLock is the file on the corresponding hard disk. Editor protects mModified and SharedPreferences protects mMap.
Refer:
Http://stackoverflow.com/questions/19148282/read-speed-of-sharedpreferences
Http://stackoverflow.com/questions/12567077/is-sharedpreferences-access-time-consuming