Sp Efficiency Analysis and Understanding

Keywords: Mobile xml Android Java github

Introduction to Catalogue

  • 01.Sp Brief Introduction
    • 1.1 Sp Action Analysis
    • 1.2 Case Analysis
  • 02.Sp Initialization Operation
    • 2.1 How to get sp
    • 2.2 Shared Preferences Impl Construction
  • 03.edit method source code
  • 04.put and get method source code
    • 4.1 put method source code
    • 4.2 get method source code
  • 05.commit and apply
    • 5.1 commit source
    • 5.2 apply source code
  • 06. Summary and analysis

Hot wire

  • Blog Notes Summary [March 16 to date], including Java foundation and in-depth knowledge points, Android technology blog, Python learning notes and so on, including the usual development of the bug summary, of course, also in the spare time to collect a large number of interview questions, long-term updates and maintenance and correction, continuous improvement... Open source files are in markdown format! At the same time, life blog has also been open source. Since 12 years, it has accumulated a total of N articles [nearly 1 million words, one after another moved to the Internet]. Please indicate the source for reprinting. Thank you!
  • Link Address: https://github.com/yangchong211/YCBlogs
  • If you feel good, you can start. Thank you. Of course, you are also welcome to make suggestions. Everything starts at a slight moment, and quantitative change causes qualitative change.

01.Sp Brief Introduction

1.1 Sp Action Analysis

  • Explanation of sp function
    • Shared Preferences is a common storage method in Android. It can be used to store some smaller set of key-value pairs and eventually generate an xml file to store data in the mobile phone's / data / package_name / shared_prefs / directory.
  • Analyzing what sp contains
    • What did the system do in getting SharedPreferences objects?
    • What does the getXxx method do?
    • What does the putXxx method do?
    • How can the commit/apply method achieve synchronous/asynchronous disk writing?
  • Analysis sp contains those source codes
    • Shared Preferences Interface
    • Shared Preferences Impl Implementation Class
    • QueuedWork class

1.2 Case Analysis

1.2.1 edit Usage Analysis
  • The code is as follows
    long startA = System.currentTimeMillis();
    for (int i=0 ; i<200 ; i++){
        SharedPreferences preferences = this.getSharedPreferences("testA", 0);
        SharedPreferences.Editor edit = preferences.edit();
        edit.putString("yc"+i,"yangchong"+i);
        edit.commit();
    }
    long endA = System.currentTimeMillis();
    long a = endA - startA;
    Log.i("test A","----"+a);
    
    
    long startB = System.currentTimeMillis();
    SharedPreferences preferencesB = this.getSharedPreferences("testB", 0);
    SharedPreferences.Editor editB = preferencesB.edit();
    for (int i=0 ; i<200 ; i++){
        editB.putString("yc"+i,"yangchong"+i);
    }
    editB.commit();
    long endB = System.currentTimeMillis();
    long b = endB - startB;
    Log.i("test B","----"+b);
    
    
    long startC = System.currentTimeMillis();
    SharedPreferences.Editor editC = null;
    for (int i=0 ; i<200 ; i++){
        SharedPreferences preferencesC = this.getSharedPreferences("testC", 0);
        if (editC==null){
            editC = preferencesC.edit();
        }
        editC.putString("yc"+i,"yangchong"+i);
    }
    editC.commit();
    long endC = System.currentTimeMillis();
    long c = endC - startC;
    Log.i("test C","----"+c);
    
  • Then start the operation.
    • A operation and B operation should be the same in code logic. They all want to write 200 different fields in SP. The difference is that A operation obtains new Editor every time, while B operation only uses one Eidtor to store. Both operations are performed twice.
    • A operation and C operation should be the same in code logic. They all want to write 200 different fields in SP. The difference is that A operation obtains new Editor every time, while C operation only uses one Editor to store and commit once. Both operations are performed twice.
    • The operations of B and C are almost the same. The only difference is that B only gets preferences B object once, while C only gets preferences C operation 200 times.
  • Then take a look at the execution results.
    2019-08-30 15:08:16.982 3659-3659/com.cheoo.app I/test A: ----105
    2019-08-30 15:08:17.035 3659-3659/com.cheoo.app I/test B: ----52
    2019-08-30 15:08:17.069 3659-3659/com.cheoo.app I/test C: ----34
    2019-08-30 15:08:20.561 3659-3659/com.cheoo.app I/test A: ----25
    2019-08-30 15:08:20.562 3659-3659/com.cheoo.app I/test B: ----1
    2019-08-30 15:08:20.564 3659-3659/com.cheoo.app I/test C: ----2
    
  • Result analysis
    • By comparing A and B operations, we can see that the execution efficiency of new and modified Editor is very poor when using commit() method and using sp.edit() method to get a new Editor every time. That is to say, there is a difference in efficiency between storing a Key that has never been used and modifying an existing Key.
    • By comparing B and C operations, we can see that getShared Preferences operations are not much different from one or more times, because there is a cache, and if there is one, it will be fetched from the cache.
  • Then look at the stored values inside.
    • The stored values are not in order.
    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
        <string name="yc110">yangchong110</string>
        <string name="yc111">yangchong111</string>
        <string name="yc118">yangchong118</string>
        <string name="yc119">yangchong119</string>
        <string name="yc116">yangchong116</string>
        <string name="yc117">yangchong117</string>
        <string name="yc114">yangchong114</string>
        <string name="yc115">yangchong115</string>
        <string name="yc112">yangchong112</string>
        <string name="yc113">yangchong113</string>
        <string name="yc121">yangchong121</string>
        <string name="yc122">yangchong122</string>
        <string name="yc120">yangchong120</string>
        <string name="yc129">yangchong129</string>
        <string name="yc127">yangchong127</string>
        <string name="yc128">yangchong128</string>
        <string name="yc125">yangchong125</string>
        <string name="yc126">yangchong126</string>
        <string name="yc123">yangchong123</string>
        <string name="yc124">yangchong124</string>
        <string name="yc1">yangchong1</string>
        <string name="yc109">yangchong109</string>
        <string name="yc0">yangchong0</string>
        <string name="yc3">yangchong3</string>
    </map>
    
1.2.2 commit and apply
  • The code is as follows
    long startA = System.currentTimeMillis();
    for (int i=0 ; i<200 ; i++){
        SharedPreferences preferences = activity.getSharedPreferences("testA", 0);
        SharedPreferences.Editor edit = preferences.edit();
        edit.putString("yc"+i,"yangchong"+i);
        edit.apply();
    }
    long endA = System.currentTimeMillis();
    long a = endA - startA;
    Log.i("test A","----"+a);
    
    
    long startB = System.currentTimeMillis();
    SharedPreferences preferencesB = activity.getSharedPreferences("testB", 0);
    SharedPreferences.Editor editB = preferencesB.edit();
    for (int i=0 ; i<200 ; i++){
        editB.putString("yc"+i,"yangchong"+i);
    }
    editB.apply();
    long endB = System.currentTimeMillis();
    long b = endB - startB;
    Log.i("test B","----"+b);
    
    
    long startC = System.currentTimeMillis();
    SharedPreferences.Editor editC = null;
    for (int i=0 ; i<200 ; i++){
        SharedPreferences preferencesC = activity.getSharedPreferences("testC", 0);
        if (editC==null){
            editC = preferencesC.edit();
        }
        editC.putString("yc"+i,"yangchong"+i);
    }
    editC.apply();
    long endC = System.currentTimeMillis();
    long c = endC - startC;
    Log.i("test C","----"+c);
    
  • Then take a look at the execution results.
    2019-08-30 15:17:07.341 5522-5522/com.cheoo.app I/test A: ----54
    2019-08-30 15:17:07.346 5522-5522/com.cheoo.app I/test B: ----5
    2019-08-30 15:17:07.352 5522-5522/com.cheoo.app I/test C: ----6
    2019-08-30 15:17:10.541 5522-5522/com.cheoo.app I/test A: ----32
    2019-08-30 15:17:10.542 5522-5522/com.cheoo.app I/test B: ----1
    2019-08-30 15:17:10.543 5522-5522/com.cheoo.app I/test C: ----1
    
  • Come to conclusion
    • From the results of execution, we can find that using apply is basically time-consuming because it is an asynchronous operation, and its efficiency is OK. From this conclusion, where apply affects efficiency, it is in the sp.edit() method.
  • It can be seen that executing edit methods many times still has a great impact on efficiency.
    • In edit(), there is a synchronized synchronized lock to ensure thread safety. Looking at the implementation of EditorImpl.java, we can see that most operations have synchronized locks, but only locks (this), that is, only valid for the current object. The edit() method is to rebuild the new EditorImpl() Eidtor interface every time. Implementation class. So efficiency should be affected here.
    @Override
    public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {
            awaitLoadedLocked();
        }
    
        return new EditorImpl();
    }
    
Recommendations given in 1.2.3
  • Editor() is efficient, so don't call the eating method in the loop. It's better to share the same Editor() object with the Editor object in the loop outside of the loop.
  • When commit(), the efficiency of "new-key" and "update-key" is different, but there is a return result.
  • apply() is an asynchronous operation, and its effect on efficiency is basically ms level, which can be ignored or forgotten.

02.Sp Initialization Operation

2.1 How to get sp

  • First look at the ContextWrapper source code
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        return mBase.getSharedPreferences(name, mode);
    }
    
  • Then take a look at the ContextImpl class
    @Override
    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<>();
            }
            file = mSharedPrefsPaths.get(name);
            if (file == null) {
                // Create a File object corresponding to the path / data/data/packageName/name
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
    
        // Here we call the getSharedPreferences(File file, int mode) method
        return getSharedPreferences(file, mode);
    }
    
  • Then take a look at the source code for the getSharedPreferences(file, mode) method
    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
    
        // The synchronized keyword is used to ensure that the construction of SharedPreferences objects is thread-safe.
        synchronized (ContextImpl.class) {
    
            // Get the cache of the SharedPreferences object and copy it to the cache
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
    
            // Take the parameter file as the key to get the cached object
            sp = cache.get(file);
    
            if (sp == null) {  // If there is no SharedPreferences object in the cache
                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");
                    }
                }
    
                // Construct a Shared Preferences Impl object
                sp = new SharedPreferencesImpl(file, mode);
                // Put it in the cache to facilitate the next time to get it directly from the cache
                cache.put(file, sp);
                // Returns the newly constructed SharedPreferencesImpl object
                return sp;
            }
        }
    
        // This involves the logic of multiple processes.
        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.
    
            // If the Shared Preferences file is modified by other processes, we will reload it.
            sp.startReloadIfChangedUnexpectedly();
        }
    
        // The program goes here to indicate that the cache has been hit, Shared Preferences has been created and returned directly.
        return sp;
    }
    
  • The flow of this source code is clear and easy to understand. The annotations are very clear. Here we summarize the main points of this method:
    • The SharedPreferences object is constructed only if the cache is not hit. That is to say, calling the getSharedPreferences method several times does not have much impact on performance because of the caching mechanism.
    • The creation of SharedPreferences objects is thread-safe because synchronize keywords are used.
    • If the cache is hit and the parameter model uses Context.MODE_MULTI_PROCESS, the sp.startReloadIfChangedUnexpectedly() method will be called. In the startReloadIfChangedUnexpectedly method, it will be judged whether the file has been modified by other processes, and if so, the number of files loaded from disk will be read again. According to.

2.2 Shared Preferences Impl Construction

  • See how Shared Preferences Impl is constructed. The source code is as follows
    • Save the incoming parameters file and mode in mFile and mMode, respectively.
    • Create A. bak backup file and restore it when the user fails to write
    • Initialize the mMap that holds key-value pairs to null
    • Call the startLoadFromDisk() method to load data
    // SharedPreferencesImpl.java
    // Construction method
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        // Create a disaster preparedness file named prefsFile. getPath ()+". bak"
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        // mLoaded represents whether the data has been loaded
        mLoaded = false;
        // The key-value pairs obtained by parsing xml files are stored in mMap
        mMap = null;
        // As the name implies, this method is used to load the xml file on the disk mFile
        startLoadFromDisk();
    }
    
    // Create disaster preparedness files to recover data when a user fails to write
    private static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    
  • Then take a look at calling the startLoadFromDisk() method to load the data
    // SharedPreferencesImpl.java
    private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
    
        //Note: Here we can see that Shared Preferences loads data asynchronously by opening a thread
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                // This method is really responsible for reading xml file data from disk
                loadFromDisk();
            }
        }.start();
    }
    
    private void loadFromDisk() {
        synchronized (SharedPreferencesImpl.this) {
            // If the data is being loaded, return directly
            if (mLoaded) {
                return;
            }
    
            // If the backup file exists, delete the original file and rename the backup file to the name of the original file.
            // We call this behavior rollback.
            if (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 map = null;
        StructStat stat = null;
        try {
            // Get file information, including file modification time, file size, etc.
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    // Read the data and parse it into jia
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), *);
                    map = XmlUtils.readMapXml(str);
                } catch (XmlPullParserException | IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }
    
        synchronized (SharedPreferencesImpl.this) {
            // Load data successfully, set mLoaded to true
            mLoaded = true;
            if (map != null) {
                // Assign the parsed key values to the mMap
                mMap = map;
                // Save the modified timestamp of the file in mStatTimestamp
                mStatTimestamp = stat.st_mtime;
                // Save the file size to mStatSize
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
    
            // Notification wakes up all waiting threads
            notifyAll();
        }
    }
    
  • The startLoadFromDisk() method is analyzed, and the following conclusions can be drawn from the analysis:
    • If there is a backup file, use the backup file to roll back directly
    • The first time the getSharedPreferences method is called, the data is loaded from disk, and the loadFromDisk method is called to read asynchronously by opening a subthread when the data is loaded.
    • Save the parsed key pair data in mMap
    • Save the file's modification timestamp and size in mStatTimestamp and mStatSize, respectively. (What's the use of saving these two values? When we analyzed the getSharedPreferences method, we said that if other processes modified the file and the model was MODE_MULTI_PROCESS, it would be judged to reload the file. How to determine whether the file has been modified by other processes, yes, according to the time of file modification and file size can be known)
    • Call the notifyAll() method to alert other waiting threads that the data has been loaded

03.edit method source code

  • The source code method is shown below.
    @Override
    public Editor edit() {
        // TODO: remove the need to call awaitLoadedLocked() when
        // requesting an editor.  will require some work on the
        // Editor, but then we should be able to do:
        //
        //      context.getSharedPreferences(..).edit().putString(..).apply()
        //
        // ... all without blocking.
        synchronized (mLock) {
            awaitLoadedLocked();
        }
    
        return new EditorImpl();
    }
    

04.put and get method source code

4.1 put method source code

  • Take putString as an example to analyze the source code. SharedPreferences.Editor returned by the sharedPreferences.edit() method, all our writes to SharedPreferences are based on this Editor class. In Android system, Editor is an interface class. Its concrete implementation class is EditorImpl:
    public final class EditorImpl implements Editor {
    
        // Writing methods such as putXxx/remove/clear do not operate directly on mMap, but instead on all.
        // Write operations are first recorded in mModified, and will not be called until the commit/apply method is called
        // All write operations are synchronized to mMap in memory and to disk
        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;
            }
        }
    
        ......
        //Other methods
        ......
    }
    
  • From the source code of the EditorImpl class, we can draw the following conclusions:
    • Shared Preferences writes are thread-safe because synchronize keywords are used
    • The addition and deletion records of key values to data are stored in mModified instead of directly operating SharedPreferences.mMap (mModified will synchronize memory SharedPreferences.mMap and disk data in commit/apply method)

4.2 get method source code

  • Take getString as an example to analyze the source code
    @Nullable
    public String getString(String key, @Nullable String defValue) {
    
        // The synchronize keyword is used to ensure that the getString method is thread-safe
        synchronized (this) {
    
            // The awaitLoadedLocked() method is used to ensure that the data is loaded and saved to the mMap for data reading.
            awaitLoadedLocked();
    
            // Get value from mMap according to key
            String v = (String)mMap.get(key);
    
            // If value is not null, return value, and if null, return default value
            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();
        }
    
        // As we said earlier, mLoaded represents whether the data has been loaded.
        while (!mLoaded) {
            try {
                // Waiting for data loading to complete before returning to continue executing code
                wait();
            } catch (InterruptedException unused) {
            }
        }
    }
    
  • The code of the getString method is very simple. Other methods, such as getInt and getFloat, are the same. This question is summarized directly.
    • The getXxx method is thread-safe because the synchronize keyword is used
    • The getXxx method operates directly on memory, reading value directly from the mMap in memory based on the incoming key
    • The getXxx method may get stuck in the awaitLoadedLocked method, which results in thread blocking waiting (when will this blocking occur? As we have previously analyzed, the first call to the getSharedPreferences method creates a thread to load data asynchronously. If the getXxx method is called immediately after the getSharedPreferences method is called, then the mLoaded is likely to be false, which will cause the awaiteLoadedLocked method to block and wait until l_SharedPreferences method is called. The oadFromDisk method loads the data and calls notifyAll to wake up all waiting threads)

05.commit and apply

5.1 commit source

  • Analysis of commit() method
    public boolean commit() {
        // As we said earlier in our analysis of putXxx, the records of write operations are stored in mModified
        // Here, the commitToMemory() method is responsible for synchronizing the write records saved by mModified into the mMap in memory.
        // And return a MemoryCommitResult object
        MemoryCommitResult mcr = commitToMemory();
    
        // The enqueueDiskWrite method is responsible for landing data on disk
        SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */);
    
        try {
            // Synchronize waiting for data to land on disk before returning
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException e) {
            return false;
        }
    
        // Notify the observer
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }
    
    • The main structure of the commit() method is clear and simple:
      • First, synchronize write operation records to Shared Preferences. mMap in memory (synchronize mModified to mMap)
      • Then call the enqueueDiskWrite method to write the data to disk
      • Synchronization waits for the write disk operation to complete (which is why the commit() method synchronously blocks the wait)
      • Notify listeners (you can register listeners through the registerOnSharedPreferenceChangeListener method)
      • Finally, return the execution result: true or false
  • Next, let's look at the commitToMemory() method it calls:
    private MemoryCommitResult commitToMemory() {
        MemoryCommitResult mcr = new MemoryCommitResult();
        synchronized (SharedPreferencesImpl.this) {
            // 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);
            }
    
            // Assigning mMap to mcr.mapToWriteToDisk, mcr.mapToWriteToDisk points to the data that is finally written to disk.
            mcr.mapToWriteToDisk = mMap;
    
            // mDiskWritesInFlight stands for "the number of times that data needs to be written to disk at this time, but has not been processed or completed yet."
            // Increase mDiskWritesInFlight by 1 (this is the only place where mDiskWritesInFlight will increase)
            mDiskWritesInFlight++;
    
            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                mcr.keysModified = new ArrayList<String>();
                mcr.listeners =
                        new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
            }
    
            synchronized (this) {
    
                // mClear is true only if the clear() method is called
                if (mClear) {
                    if (!mMap.isEmpty()) {
                        mcr.changesMade = true;
    
                        // When mClear is true, clear mMap
                        mMap.clear();
                    }
                    mClear = false;
                }
    
                // Traversing mModified
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey(); // Get key
                    Object v = e.getValue(); // Get value
    
                    // When the value of value is "this" or null, the key value of the corresponding key is removed from the mMap.
                    if (v == this || v == null) {
                        if (!mMap.containsKey(k)) {
                            continue;
                        }  
                        mMap.remove(k);
                    } else { // Otherwise, update or add key value pairs to the data
                        if (mMap.containsKey(k)) {
                            Object existingValue = mMap.get(k);
                            if (existingValue != null && existingValue.equals(v)) {
                                continue;
                            }
                        }
                        mMap.put(k, v);
                    }
    
                    mcr.changesMade = true;
                    if (hasListeners) {
                        mcr.keysModified.add(k);
                    }
                }
    
                // After synchronizing mModified to mMap, clear the mModified history
                mModified.clear();
            }
        }
        return mcr;
    }
    
    • The commitToMemory() method mainly does these things:
      • MDiskWritesInFlight self-increasing 1 (mDiskWritesInFlight stands for "the number of times that data needs to be written to disk at this time, but has not been processed or completed yet"), suggesting that in the source code of SharedPreferences, only one code in commitToMemory() method will increase mDiskWritesInFlight, and others. Everywhere is reduced)
      • Pointing mcr.mapToWriteToDisk to mMap, mcr.mapToWriteToDisk is the data that needs to be written to disk finally.
      • Determine the value of mClear. If true, empty the mMap (call the clear() method and set mClear to true)
      • Synchronize mModified data into mMap, then empty mModified and finally return a MemoryCommitResult object whose mapToWriteToDisk parameter points to the mMap that eventually needs to be written to disk.
  • The call enqueueDiskWrite method is analyzed:
    private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
        // Create a Runnable object that is responsible for writing disk operations
        final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    // As the name implies, this is the way to finally write data to disk through file manipulation.
                    writeToFile(mcr);
                }
                synchronized (SharedPreferencesImpl.this) {
                    // After writing to disk, reduce mDiskWritesInFlight by 1, representing a reduction in the need to write to disk
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    // Execute postWriteRunnable (hint, in apply, postWriteRunnable is not null)
                    postWriteRunnable.run();
                }
            }
        };
    
        // If the passed parameter postWriteRunnable is null, then isFromSyncCommit is true
        // Warm Tip: From the commit() method source code above, you can see that the postWriteRunnable passed in by calling commit() method is null.
        final boolean isFromSyncCommit = (postWriteRunnable == null);
    
        // Typical #commit() path with fewer allocations, doing a write on the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (SharedPreferencesImpl.this) {
                // If there is only one commit request (note, commit request, not apply) that is not processed at this time, wasEmpty is true
                wasEmpty = mDiskWritesInFlight == 1;
            }
    
            if (wasEmpty) {
                // When there is only one commit request that is not processed, no threads need to be opened for processing, and writeToDiskRunnable can be executed directly in this thread.
                writeToDiskRunnable.run();
                return;
            }
        }
    
        // Execute the writeToDiskRunnable method in the thread pool
        // There are two possibilities for the program to be executed here:
        // 1. The commit() method is called, and there is currently only one commit request that is not processed.
        // 2. The application () method is called
        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }
    
    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 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()) {
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                            + " to backup file " + mBackupFile);
                    mcr.setDiskWriteResult(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 (str == null) {
                mcr.setDiskWriteResult(false);
                return;
            }
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            FileUtils.sync(str);
            str.close();
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            try {
                final StructStat stat = Libcore.os.stat(mFile.getPath());
                synchronized (this) {
                    mStatTimestamp = stat.st_mtime;
                    mStatSize = stat.st_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.exists()) {
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        mcr.setDiskWriteResult(false);
    }
    
    • The writeToFile approach is roughly divided into three processes:
      • First rename the existing old SP file (add ". bak" suffix), then delete the old SP file, which is equivalent to backup (disaster preparedness).
      • Write all key-value pair data to mFile at one time, that is mcr. mapToWriteToDisk (that is, the field in which all key-value pairs of data are saved by commitToMemory) to disk at one time.
      • If the write is successful, delete the backup (disaster) file and record the synchronization time. If the write to disk fails, delete the semi-finished SP file.

5.2 apply source code

  • Application () method analysis
    public void apply() {
    
        // Synchronize the write records saved by mModified into the mMap in memory and return a MemoryCommitResult object
        final MemoryCommitResult mcr = commitToMemory();
    
    
        final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };
    
        QueuedWork.add(awaitCommit);
    
        Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };
    
        // Place the data on disk. Note that the incoming postWriteRunnable parameter is not null, so in
        // The enqueueDiskWrite method opens sub-threads to write data asynchronously to disk
        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);
    }  
    
    • Summarize the apply() method:
      • The commitToMemory() method synchronously writes back the write operations recorded in mModified to memory SharedPreferences.mMap. At this point, any getXxx method can get the latest data.
      • Call writeToFile through the enqueueDiskWrite method to write all data asynchronously to disk

06. Summary and analysis

  • Shared Preferences is thread-safe and its internal implementation uses a large number of synchronized keywords
  • Shared Preferences is not process-safe
  • The first call to getSharedPreferences loads the disk xml file (this loading process is asynchronous and executed through new Thread, so it does not block threads while constructing SharedPreferences, but blocks calls such as getXxx/putXxx/remove/clear), but subsequent calls to getSharedPreferences are cached from memory. Get from. If the first call to getShared Preferences calls getXxx/putXxx before the disk is loaded, the getXxx/putXxx operation will block until the data loaded from the disk is complete.
  • All getXxx is data taken from memory from Shared Preferences. mMap
  • apply synchronous write-back (commitToMemory()) memory SharedPreferences.mMap, and then asynchronous write-back disk tasks are placed in a single thread pool queue for scheduling. apply does not need to wait for the write disk to complete, but returns immediately
  • Commit synchronous write-back (commitToMemory()) memory SharedPreferences.mMap, and then if the value of mDiskWritesInFlight (the number of times that data needs to be written to disk at this time but has not been processed or completed yet) equals 1, write back to disk directly in the thread calling commit, otherwise asynchronous write back to disk Tasks are placed in a single thread pool queue for scheduling. Commit blocks the calling thread until the write to disk is complete before returning
  • MODE_MULTI_PROCESS checks the last modification time and file size of the configuration file on disk every getShared Preferences. Once all modifications are made, the file will be loaded from disk again, so it can not guarantee the real-time synchronization of multi-process data.
  • Starting with Android N, MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE are not supported. Once specified, throw an exception directly

Other presentations

01. Summary of blog links

02. About my blog

Posted by Zanne on Fri, 30 Aug 2019 01:00:49 -0700