The friend that likes, dot praise bai encouragement bai ~

This article analyzes Android 7.0 source code

SharedPreferences is a common storage method in Android that can be used to store small sets of key-value pairs. Finally, an XML file will be generated in the /data/data/package_name/shared_prefs/ directory of the mobile phone to store data. It’s very simple to use, and it’s a basic skill for an Android developer that I won’t cover here.

SharedPreferences has given us a very simple and easy-to-use data storage read and write functionality, but have you ever wondered how it is implemented?

Through ContextImpl getSharedPreferences method can obtain SharedPreferences object, through the getXxx/putXxx method can read and write operations, through the commit method synchronous write disk, asynchronous write disk through the apply method. It involves the following issues:

  • To obtainSharedPreferencesWhat does the system do during the object process?
  • getXxxWhat does the method do?
  • putXxxWhat does the method do?
  • commit/applyHow do I write to a disk synchronously or asynchronously?

Here, we answer those questions one by one.


Question 1: What does the system do to get the SharedPreferences object?

Get the SharedPreferences object

We see ContextImpl directly. GetSharedPreferences source code:

@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 corresponding path/data/data/packageName the name of the File objectfile = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); }}The getSharedPreferences(File File, int Mode) method is called
    return getSharedPreferences(file, mode);
}Copy the code
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;

    The synchronized keyword is used here to ensure that the SharedPreferences object is constructed thread-safe
    synchronized (ContextImpl.class) {

        // Get the cache of the SharedPreferences object and copy it to the cache
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();

        // Get the cache object with file as the key
        sp = cache.get(file);

        if (sp == null) {  // If the SharedPreferences object does not exist 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 SharedPreferencesImpl object
            sp = new SharedPreferencesImpl(file, mode);
            // Put it in cache cache, so that it can be retrieved directly from cache next time
            cache.put(file, sp);
            // Returns the newly constructed SharedPreferencesImpl object
            returnsp; }}// Multiprocess logic is involved here
    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 SharedPreferences file is modified by another process, we will reload it
        sp.startReloadIfChangedUnexpectedly();
    }

    SharedPreferences have been created. SharedPreferences have been created
    return sp;
}Copy the code

The source code flow is clear and easy to understand, the comments have been made very clear, here we summarize the main points of this method:

  • Cache not hit, constructedSharedPreferencesObject, that is, called multiple timesgetSharedPreferencesMethod doesn’t have much of a performance impact because of caching

  • SharedPreferencesObject creation is thread-safe because it is usedsynchronizeThe keyword
  • If the cache is hit and the parametermodeUsing theContext.MODE_MULTI_PROCESS, will be calledsp.startReloadIfChangedUnexpectedly()Methods,startReloadIfChangedUnexpectedlyMethod to determine whether the file has been modified by another process and, if so, reload the file from disk

Next, we focus on the comment sp = new SharedPreferencesImpl(file, mode); Construct a SharedPreferencesImpl object.

Tectonic SharedPreferencesImpl

// SharedPreferencesImpl.java
// constructor
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    // Create a disaster recovery file named prefsfile.getPath () + ".bak"
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    // mLoaded indicates whether data has been loaded
    mLoaded = false;
    // The key-value pairs from 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 a Dr File to recover data when user writes fail
private static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}Copy the code

Let’s summarize the SharedPreferencesImpl constructor:

  • Parameters to be passed infileAs well asmodeKeep separately inmFileAs well asmModeIn the
  • To create a.bakBackup file. If a user fails to write data, the backup file will be used to restore data
  • Will store key-value pairsmMapInitialized tonull
  • callstartLoadFromDisk()Method load data

Of the four points above, the most important is the last step, which calls the startLoadFromDisk() method to load data:

// SharedPreferencesImpl.java
private void startLoadFromDisk(a) {
    synchronized (this) {
        mLoaded = false;
    }

    SharedPreferences is used to load data asynchronously by starting a thread
    new Thread("SharedPreferencesImpl-load") {
        public void run(a) {
            // This method is really responsible for reading the XML file data from disk
            loadFromDisk();
        }
    }.start();
}

private void loadFromDisk(a) {
    synchronized (SharedPreferencesImpl.this) {
        // If 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 original file name
        // We call this behavior a 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 to 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) {
        // Data loaded successfully, set mLoaded to true
        mLoaded = true;
        if(map ! =null) {
            // Assign the parsed key-value pair data to mMap
            mMap = map;
            // Save the modified timestamp of the file to mStatTimestamp
            mStatTimestamp = stat.st_mtime;
            // Save the file size to mStatSize
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }

        // The notification wakes up all waiting threadsnotifyAll(); }}Copy the code

StartLoadFromDisk () = startLoadFromDisk();

  • If there is a backup file, use the backup file to roll back
  • First callgetSharedPreferencesMethod is used to load data from disk, which is invoked by starting a child threadloadFromDiskMethod to read asynchronously
  • Save the parsed key-value pair data inmMapIn the
  • Save the modified timestamp and size of the file inmStatTimestampAs well asmStatSizeWhat is the use of saving these two values? We’re analyzinggetSharedPreferencesMethod says that if another process modifies the file, andmodeforMODE_MULTI_PROCESS, will decide to reload the file. How to tell if a file has been modified by another process?
  • callnotifyAll()Method notifies other waiting threads to wake up that data has been loaded

Ok, now, we will solve the first question: call ContextImpl. GetSharedPreferences method to get a SharedPreferences object process, what system do?

A sequence flow chart is given below:




Question 2: What does getXxx do?

Let’s analyze this with getString:

@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 data is loaded and saved to mMap before data is read
        awaitLoadedLocked();

        // Get value from mMap based on key
        String v = (String)mMap.get(key);

        // If value is not null, return value; if it is null, return default value
        returnv ! =null? v : defValue; }}private void awaitLoadedLocked(a) {
    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();
    }

    // mLoaded indicates whether the data has been loaded
    while(! mLoaded) {try {
            // Wait for the data load to complete before returning to continue executing the code
            wait();
        } catch (InterruptedException unused) {
        }
    }
}Copy the code

The getString method code is very simple, other methods such as getInt, getFloat method is the same principle, we directly summarize the question:

  • getXxxThe method is thread-safe because it is usedsynchronizeThe keyword
  • getXxxMethods operate directly in memory, directly from memorymMapAccording to the incomingkeyreadvalue
  • getXxxMethods can get stuckawaitLoadedLockedMethod, which causes the thread to block waiting (When does this blockage occur? We looked at it earlier, the first callgetSharedPreferencesMethod, a thread is created to load data asynchronously, so if after the callgetSharedPreferencesMethod immediately aftergetXxxMethod, at this pointmLoadedThere is a good chance thatfalse, which can lead toawaiteLoadedLockedMethod blocks and waits untilloadFromDiskMethod loads the data and callsnotifyAllTo wake up all waiting threads)


Question 3: What does the putXxx method do?

The first thing that comes to mind is the sharedPreferences. Editor class returned by the sharedPreferences.edit() method. All of our sharedPreferences writing is based on this Editor class. In Android, Editor is an interface class whose implementation class is EditorImpl:

public final class EditorImpl implements Editor {

    // putXxx/remove/clear write operations are not direct operations on mMap
    // write operations are recorded in mModified until commit/apply 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; }}public Editor putStringSet(String key, @Nullable Set<String> values) {
        synchronized (this) {
            mModified.put(key, (values == null)?null : new HashSet<String>(values));
            return this; }}public Editor putInt(String key, int value) {
        synchronized (this) {
            mModified.put(key, value);
            return this; }}public Editor putLong(String key, long value) {
        synchronized (this) {
            mModified.put(key, value);
            return this; }}public Editor putFloat(String key, float value) {
        synchronized (this) {
            mModified.put(key, value);
            return this; }}public Editor putBoolean(String key, boolean value) {
        synchronized (this) {
            mModified.put(key, value);
            return this; }}public Editor remove(String key) {
        synchronized (this) {
            mModified.put(key, this);
            return this; }}... Other methods...... }Copy the code

From the source of the EditorImpl class we can draw the following summary:

  • SharedPreferencesThe write operation is thread-safe because it is usedsynchronizeThe keyword
  • Records of adding and deleting key-value pair data are saved inmModified“, but not directlySharedPreferences.mMapTo operate (mModifiedWill be incommit/applyMethod to synchronize memorySharedPreferences.mMapAnd the role of disk data)


Question 4: How do commit()/apply() write disks synchronously/asynchronously?

Commit () method analysis

Commit ();

public boolean commit(a) {
    PutXxx putXxx putXxx putXxx putXxx putXxx putXxx putXxx
    // In this case, the commitToMemory() method is responsible for synchronizing write records saved in mModified to the mMap in memory
    // And returns a MemoryCommitResult object
    MemoryCommitResult mcr = commitToMemory();

    // The enqueueDiskWrite method is responsible for landing data to disk
    SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */);
    
    try {
        // Returns after data is synchronized to the disk
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }

    // Notify the observer
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}Copy the code

The body structure of the commit() method is clear and simple:

  • The write operation records are first synchronized to memorySharedPreferences.mMap(will bemModifiedSynchronized to themMap)
  • And then callenqueueDiskWriteMethod to write data to disk
  • Synchronous waiting for the write operation to complete (is that whycommit()Method blocks wait synchronously)
  • Notify the listener (can passregisterOnSharedPreferenceChangeListenerMethod register listener)

  • Return the execution result:true or false


After committing (), let’s look at the commitToMemory() method it calls:

private MemoryCommitResult commitToMemory(a) {
    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);
        }

        // Assign mMap to McR.maptowritetodisk, which points to the data eventually written to disk
        mcr.mapToWriteToDisk = mMap;

        // mDiskWritesInFlight represents "the number of times that data needs to be written to disk at this time but has not been processed or completed."
        // Increment mDiskWritesInFlight by 1 (this is the only place where mDiskWritesInFlight is inflight)
        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 clear() is called
            if (mClear) {
                if(! mMap.isEmpty()) { mcr.changesMade =true;

                    // When mClear is true, mMap is cleared
                    mMap.clear();
                }
                mClear = false;
            }
            
            / / traverse mModified
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey(); / / get the key
                Object v = e.getValue(); / / get the value
                
                // When value is" this" or null, the corresponding key-value pair is removed from mMap
                if (v == this || v == null) {
                    if(! mMap.containsKey(k)) {continue;
                    }  
                    mMap.remove(k);
                } else { // Otherwise, update or add key-value pair 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 historymModified.clear(); }}return mcr;
}Copy the code

In general, the commitToMemory() method mainly does these things:

  • mDiskWritesInFlightSince (1-6mDiskWritesInFlightIndicates the number of times that data needs to be written to disk at this time but has not been processed or completedSharedPreferencesIn the source code, only incommitToMemory()One and only code in a method is correctmDiskWritesInFlightAdd, subtract everywhere else)
  • willmcr.mapToWriteToDiskPoint to themMap.mcr.mapToWriteToDiskThis is the data that ultimately needs to be written to disk
  • judgemClearThe value of, if yestrueEmpty,mMap(callclear()Method, will setmClearfortrue)
  • synchronousmModifiedData to themMapMedium, and then emptymModified
  • And finally return oneMemoryCommitResultObject, of this objectmapToWriteToDiskThe parameter points to what will eventually be written to diskmMap

It should be noted that in thecommitToMemory()Method, whenmClearfortrue, now emptymMapBut not emptymModified, so it’s still iteratingmModifiedTo synchronize the write records saved therein tomMap, so the following is incorrect:

sharedPreferences.edit()
    .putString("key1"."value1")    // Key1 will not be cleared, but will still be written to disk after commit
    .clear()
    .commit();Copy the code


After analyzing the commitToMemory() method, we go back to the commit() method and analyze the enqueueDiskWrite method it calls:

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    // Create a Runnable object that writes to the disk
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run(a) {
            synchronized (mWritingToDiskLock) {
                // As the name implies, this is how data is finally written to disk via file operations
                writeToFile(mcr);
            }
            synchronized (SharedPreferencesImpl.this) {
                // After writing data into a disk, mDiskWritesInFlight decreases by 1, indicating that the need to write data into a disk is reduced
                mDiskWritesInFlight--;
            }
            if(postWriteRunnable ! =null) {
                // Execute postWriteRunnable (postWriteRunnable is not null in Apply)postWriteRunnable.run(); }}};If postWriteRunnable is null, isFromSyncCommit is true
    PostWriteRunnable is null when the commit() method is called
    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) {
            WasEmpty is true if only one commit request is unprocessed at this point
            wasEmpty = mDiskWritesInFlight == 1;
        }
        
        if (wasEmpty) {
            WriteToDiskRunnable if only one commit request is unprocessed, writeToDiskRunnable can be executed in this thread without starting the thread
            writeToDiskRunnable.run();
            return; }}// The writeToDiskRunnable method is executed in the thread pool
    // At this point in the program, there are two possibilities:
    // 1. The commit() method is called, and only one commit request is currently unprocessed
    // 2. The apply() method is called
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}Copy the code

WriteToFile: writeToFile: writeToFile: writeToFile

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

writeToFile


  • Rename the existing SP file (add the suffix “.bak “) and delete the existing SP file. This is equivalent to making a backup (DISASTER recovery).
  • tomFile, writes all key-value pair data at once, i.emcr.mapToWriteToDisk(this iscommitToMemoryThe field that holds all key-value pair data) is written to disk once. If the data is written successfully, the backup (DISASTER recovery) file is deleted and the synchronization time is recorded

  • If writing data to disk fails, the semi-finished SP file is deleted

From the above analysis, we have an idea of the entire call chain of the commit() method and what it does.




Apply () method analysis

After analyzing the commit() method, it’s much easier to analyze the apply() method:

public void apply(a) {

    // Synchronize write records saved by mModified to mMap in memory and return a MemoryCommitResult object
    final MemoryCommitResult mcr = commitToMemory();

    
    final Runnable awaitCommit = new Runnable() {
        public void run(a) {
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }
        }
    };

    QueuedWork.add(awaitCommit);
    
    Runnable postWriteRunnable = new Runnable() {
        public void run(a) { awaitCommit.run(); QueuedWork.remove(awaitCommit); }};// Drop the data to disk. Note that the postWriteRunnable parameter passed in is not null, so the
    The enqueueDiskWrite method enables child threads to asynchronously write data 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);
}  Copy the code

To summarize the apply() method:

  • commitToMemory()Method will bemModifiedWrite operations recorded in theSharedPreferences.mMapIn the. At this point, anygetXxxMethod can get the latest data

  • throughenqueueDiskWriteThe method callwriteToFileWill method all dataasynchronousWrite to disk

Here is also a sequence flow chart of apply() for memorizing and understanding:




conclusion

  • SharedPreferencesIs thread-safe, and its internal implementation uses a lot of itsynchronizedThe keyword
  • SharedPreferencesNot process safe
  • First callgetSharedPreferencesThe disk XML file is loaded (this loading process is asynchronous, throughnew ThreadTo execute, so there is no constructionSharedPreferences“, but blocksgetXxx/putXxx/remove/clearEtc.), but subsequent callsgetSharedPreferencesWill be retrieved from the memory cache. If I call it the first timegetSharedPreferencesIs called immediately before it has finished loading from diskgetXxx/putXxx, thengetXxx/putXxxThe operation blocks and does not return until loading data from disk is complete
  • All of thegetXxxIt’s all data from memory, data fromSharedPreferences.mMap
  • applySynchronous write back (commitToMemory()) memorySharedPreferences.mMap, and put the asynchronous write back to disk into a single-threaded thread pool queue for scheduling.applyInstead of waiting for the disk write to complete, return immediately
  • commitSynchronous write back (commitToMemory()) memorySharedPreferences.mMapAnd then ifmDiskWritesInFlight(The number of times that data needs to be written to disk, but has not been processed or not completed) is equal to 1, then the call is directly executedcommitOtherwise, asynchronous write back tasks are placed in a single-threaded thread pool queue waiting for scheduling.commitBlocks the calling thread until the write to disk is complete
  • MODE_MULTI_PROCESSIn every timegetSharedPreferencesCheck the last modification time and file size of the configuration file on the disk. Once all modifications are made, the file will be loaded from the disk again. Therefore, real-time data synchronization between multiple processes cannot be guaranteed
  • Starting with Android N, it is not supportedMODE_WORLD_READABLE & MODE_WORLD_WRITEABLE. Once specified, an exception is thrown directly


Precautions for Use

  • Do not use SharedPreferences as a means of multi-process communication. Because cross-process locks are not used, frequent cross-process reads and writes of SharedPreferences may result in total data loss, even if MODE_MULTI_PROCESS is used. According to online statistics, SP has a damage rate of about 1 in 10,000

  • Each SP file cannot be too large. The file storage performance of SharedPreference is related to file size, so don’t store unrelated configuration items in the same file, and consider isolating items that change frequently

  • Or each SP file should not be too large. In the first getSharedPreferences, the SP file will be loaded into memory first, too large SP file will cause blocking, even ANR

  • Still, each SP file should not be too large.Every timeapplyorcommit, all data will be written to disk at once, so the SP file should not be too large, affecting the overall performance

The friend that likes, dot praise bai encouragement bai ~