Across a question, is SharedPreferences thread-safe? Is SharedPreferences process secure? If not, how do you ensure thread-safety and process-safety?

SharedPreferences SharedPreferences is often used, also know is not process security, but this is from the book to see, and did not go deep into the study, this time from the source point of view to analyze the above several problems.

SharedPreferences are thread-safe? Is SharedPreferences process secure?

  • SharedPreferences are thread-safe because of the large amount of synchronized keyword security built in.
  • SharedPreferences are not process-safe because they are first read from disk and then from memory.

SharedPreferences source code parsing

Use SharedPreferences

The use of SharedPreferences is divided into saving data and reading data.

Each SharedPreferences corresponds to an XML file in the data/data/package_name/share_prefs/ directory of the current package. Saving data and reading data is really just writing and reading XML files.

Steps to save data:

  • Get the SharedPreferences object
  • Get the Editor object through the Editor
  • Write data as key-value pairs
  • Commit changes
There are two ways to get the SharedPreferences object
/ / way
Parameter 1: specifies the name of the file. Parameter 2: specifies the operation mode of the file. There are four operation modes.
// context. MODE_PRIVATE = 0: Indicates that the file is private data and can only be accessed by the application itself. In this mode, the written content overwrites the content of the original file
// context. MODE_APPEND = 32768: this mode checks if the file exists, appends to the file if it does, and creates a new file if it does not.
// context. MODE_WORLD_READABLE = 1: the current file can be read by other applications
// context. MODE_WORLD_WRITEABLE = 2: indicates that the current file can be written by other applications
SharedPreferences sharedPreferences = context.getSharedPreferences("trampcr_sp", Context.MODE_PRIVATE);
2 / / way
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);

// get the editor object
Editor editor = sharedPreferences.edit();

// write data as key-value pairs
editor.putString("name"."trampcr");
editor.putString("rank"."T6");

// There are two ways to commit changes
/ / way
editor.apply();
2 / / way
editor.commit();
Copy the code

Steps for reading data:

  • Get the SharedPreferences object
  • The previously saved value is read through the SharedPreferences object

SharedPreferences = SharedPreferences = SharedPreferences = SharedPreferences
SharedPreferences sharedPreferences = getSharedPreferences("ljq", Context.MODE_PRIVATE);

SharedPreferences getXxx() ¶
String name = sharedPreferences.getString("name"."");
String age = sharedPreferences.getInt("rank"."");
Copy the code

2, SharedPreferences source code parsing

(1) The SharedPreferences object can be obtained in two ways:
  • PreferenceManager.getDefaultSharedPreferences()
  • ContextImpl.getSharedPreferences()
// PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context), getDefaultSharedPreferencesMode());
}

// Context.java
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);

// ContextWrapper.java
public SharedPreferences getSharedPreferences(String name, int mode) {
    return mBase.getSharedPreferences(name, mode);
}
Copy the code

Here to find call mBase. GetSharedPreferences (name, mode) method, then the mBase exactly what is it?

Search the mBase definition and assignment:

// ContextWrapper.java
// Context mBase;
protected void attachBaseContext(Context base) {
    if(mBase ! =null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}
Copy the code

As you can see, mBase is a Context object that is assigned in the attachBaseContext() method, which looks familiar from the ActivityThread creating Activity code. To ActivityThread. HandleLaunchActivity () go and see.

// ActivityThread. Java code is deleted
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {... Activity a = performLaunchActivity(r, customIntent); . }// ActivityThread.java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {... ContextImpl appContext = createBaseContextForActivity(r); Activity activity =null;
    try{ java.lang.ClassLoader cl = appContext.getClassLoader(); activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent); . }catch (Exception e) {
        ...
    }

    try {
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);

        if(activity ! =null) {... appContext.setOuterContext(activity); activity.attach(appContext,this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback); . }... }...return activity;
}

// ActivityThread.java
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {... ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig); .return appContext;
}

// ActivityThread.java
static ContextImpl createActivityContext(ActivityThread mainThread, LoadedApk packageInfo, ActivityInfo activityInfo, 
    IBinder activityToken, int displayId, Configuration overrideConfiguration) {... ContextImpl context =new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName, activityToken, null.0, classLoader); .return context;
}
Copy the code

Here is a brief summary of the above content, and then back analysis.

ActivityThread.handleLaunchActivity() -> performLaunchActivity() -> createBaseContextForActivity() -> CreateActivityContext () -> Return to performLaunchActivity() and call Activity.Attach (appContext,...)Copy the code

The above process can be simply interpreted as creating a ContextImpl object and passing it to the activity.Attach () method.

// Activity.java
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, 
    int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, 
    Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances,
    Configuration config, String referrer, IVoiceInteractor voiceInteractor,
    Window window, ActivityConfigCallback activityConfigCallback) {
    // attachBaseContext()attachBaseContext(context); . }// ContextThemeWrapper.java
protected void attachBaseContext(Context newBase) {
    super.attachBaseContext(newBase);
}

// ContextWrapper.java
protected void attachBaseContext(Context base) {
    if(mBase ! =null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}
Copy the code

The ContextImpl object is created from the ActivityThread and assigned to mBase. So mBase. GetSharedPreferences () is ContextImpl. GetSharedPreferences ().

This is the second way to get SharedPreferences, so, The first way PreferenceManager. GetDefaultSharedPreferences () is the second way ContextImpl. GetSharedPreferences () packaging, Eventually are ContextImpl. GetSharedPreferences ().

// ContextImpl.java
public SharedPreferences getSharedPreferences(File file, int mode) {... SharedPreferencesImpl sp;// Create the SharedPreferences object using the synchronized keyword, so the instance creation process is thread-safe
    synchronized (ContextImpl.class) {
        // Get the SharedPreferences object from the cache
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        sp = cache.get(file);
        if (sp == null) {
            // If there is no cache, create one
            sp = new SharedPreferencesImpl(file, mode);
            cache.put(file, sp);
            returnsp; }}if((mode & Context.MODE_MULTI_PROCESS) ! =0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        sp.startReloadIfChangedUnexpectedly();
    }
    
    return sp;
}

// ContextImpl.java
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked(a) {
    if (sSharedPrefsCache == null) {
        sSharedPrefsCache = new ArrayMap<>();
    }

    final String packageName = getPackageName();
    ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
    if (packagePrefs == null) {
        packagePrefs = new ArrayMap<>();
        sSharedPrefsCache.put(packageName, packagePrefs);
    }

    return packagePrefs;
}
Copy the code

The SharedPreferences object is obtained from the cache. If it is not in the cache, create the SharedPreferences object. At the same time, instance creation is synchronized modified, so the process of creating a SharedPreferences object is thread-safe.

Let’s look at creating the SharedPreferences object:

// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
    mFile = file; 
    mBackupFile = makeBackupFile(file); // Backup file for recovery in case of write failure
    mMode = mode;
    mLoaded = false;
    mMap = null; // The set of data cached in memory, which is the source of getXxx() data
    startLoadFromDisk();
}

// SharedPreferencesImpl.java
private void startLoadFromDisk(a) {
    synchronized (mLock) {
        mLoaded = false;
    }
    
    // Start a thread to read data from disk files
    new Thread("SharedPreferencesImpl-load") {
        public void run(a) {
            loadFromDisk();
        }
    }.start();
}

// SharedPreferencesImpl.java
private void loadFromDisk(a) {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        // if there is a backup file, use the backup file (rename).
        if(mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); }}...try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            ...
            try {
                // read files from disk to memory
                str = new BufferedInputStream(new FileInputStream(mFile), 16*1024); map = XmlUtils.readMapXml(str); }... }}catch (ErrnoException e) {
    }

    synchronized (mLock) {
        AwaitLoadedLocked () is used by the awaitLoadedLocked() method
        mLoaded = true;
        if(map ! =null) {
            // 4. Save the contents of the file read from disk in the mMap field
            mMap = map;
            
            MODE_MULTI_PROCESS ()
            mStatTimestamp = stat.st_mtime;
            mStatSize = stat.st_size;
        } else {
            mMap = new HashMap<>();
        }
        
        Send a notifyAll() that it has finished reading and wakes up all the other threads waiting to loadmLock.notifyAll(); }}Copy the code

The SharedPreferencesImpl constructor calls startLoadFromDisk(), and in startLoadFromDisk() opens a thread calling loadFromDisk() to read data from the disk file, which does the following:

  1. If there is a backup file, use the backup file directly (rename)
  2. Reads files from disk to memory
  3. MLoaded = true; , which is used later in the awaitLoadedLocked() method
  4. Save the contents of the file read from disk in the mMap field
  5. Record the time when the file is read (mStatTimestamp = stat.st_mtime;) Will be used later in MODE_MULTI_PROCESS
  6. Issue a notifyAll() that it has finished reading and wakes up all other threads waiting to load
(2) Get the editor object
Editor editor = sharedPreferences.edit();

// SharedPreferencesImpl.java
public Editor edit(a) {
    synchronized (mLock) {
        awaitLoadedLocked();
    }

    return new EditorImpl();
}

// SharedPreferencesImpl.java
private void awaitLoadedLocked(a) {
    if(! mLoaded) { BlockGuard.getThreadPolicy().onReadFromDisk(); }while(! mLoaded) {try {
            // Wait until the configuration file is read. The Editor cannot be returned and data cannot be saved
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
}
Copy the code

AwaitLoadedLocked () is called before new EditorImpl(), and if mLoaded = false, it will get stuck before the configuration file has been read, Until SharedPreferencesImpl. LoadFromDisk () after reading the notifyAll () notifies all waiting threads will return EditorImpl object.

EditorImpl is an inner class of SharedPreferencesImpl with no constructor and only two properties initialized.

// SharedPreferencesImpl.java
public final class EditorImpl implements Editor {
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    // Save putXxx set of incoming data
    private final Map<String, Object> mModified = Maps.newHashMap();

    @GuardedBy("mLock")
    private boolean mClear = false; . }Copy the code
(3) Write data in the form of key-value pairs
editor.putString("name"."trampcr");
editor.putString("rank"."T6");

// SharedPreferencesImpl.EditorImpl.java
public Editor putString(String key, @Nullable String value) {
    synchronized (mLock) {
        // Save the key-value pair to mModified
        mModified.put(key, value);
        return this; }}Copy the code
(4) Submit the modification

There are two ways:

  • editor.apply();
  • editor.commit();
// SharedPreferencesImpl.EditorImpl.java
public void apply(a) {
    final long startTime = System.currentTimeMillis();
    
    // 1. Save the data written as key-value pair to memory
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        public void run(a) {
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }
        }
    };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
        public void run(a) { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); }};// Add the data stored in memory to an asynchronous queue, waiting for scheduling
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    notifyListeners(mcr);
}

// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory(a) {
    long memoryStateGeneration;
    List<String> keysModified = null;
    Set<OnSharedPreferenceChangeListener> listeners = null;
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        if (mDiskWritesInFlight > 0) {
            mMap = new HashMap<String, Object>(mMap);
        }
        
        // Save SharedPreferences. MMap to MCR. mapToWriteToDisk, which will later be written to diskmapToWriteToDisk = mMap; mDiskWritesInFlight++; .synchronized (mLock) {
            ...

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                
                if (v == this || v == null) {
                    if(! mMap.containsKey(k)) {continue;
                    }
                    mMap.remove(k);
                } else {
                    if (mMap.containsKey(k)) {
                        Object existingValue = mMap.get(k);
                        if(existingValue ! =null && existingValue.equals(v)) {
                            continue; }}// the putXxx method saves the key-value pair to editor. mModified. Here, the mModified data is written to SharedPreferences
                    mMap.put(k, v);
                }

                changesMade = true;
                if(hasListeners) { keysModified.add(k); } } mModified.clear(); . }}return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk);
}

// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run(a) {
            synchronized(mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); }... }}; . QueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit); }// SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {...booleanfileExists = mFile.exists(); .if (fileExists) {
        boolean needsWrite = false;

        // Write disks only if the disk state is older than the current commit state
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true; }}}}...boolean backupFileExists = mBackupFile.exists();

        if(! backupFileExists) {// Rename existing old files (with the suffix.bak) to backup files
            if(! mFile.renameTo(mBackupFile)) { mcr.setDiskWriteResult(false.false);
                return; }}else {
            // If you already have a backup file, delete the old configuration filemFile.delete(); }}try {
        FileOutputStream str = createFileOutputStream(mFile);

        if (str == null) {
            mcr.setDiskWriteResult(false.false);
            return;
        }
        
        // Write all key-value pairs saved in MCR. mapToWriteToDisk to mFileXmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); writeTime = System.currentTimeMillis(); .try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {
                // Record the time of writing to diskmStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; }}catch (ErrnoException e) {
            // Do nothing
        }

        // If writing to disk succeeds, delete the backup filemBackupFile.delete(); .return; }...if (mFile.exists()) {
        // If writing to disk fails, delete the file
        if(! mFile.delete()) { Log.e(TAG,"Couldn't clean up partially-written file " + mFile);
        }
    }

    mcr.setDiskWriteResult(false.false);
}
Copy the code

To summarize the two steps of apply() :

1. Store data written as key-value pairs to commitToMemory().

2. The data stored in memory is added to an asynchronous queue for scheduling, that is, data is asynchronously written to disks (enqueueDiskWrite).

Now that apply() is done, look at commit().

// SharedPreferencesImpl.EditorImpl.java
public boolean commit(a) {
    long startTime = 0;

    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally{... } notifyListeners(mcr);return mcr.writeToDiskResult;
}
Copy the code

Commit () is basically the same as apply(), with the following differences:

  • Apply () : synchronously writes to memory, asynchronously writes to disk, no return value.
  • Commit () : The write back logic is the same as apply(). The difference is that commit() does not return until the asynchronous write back is complete.
(5) Read the previously saved value through the SharedPreferences object
String name = sharedPreferences.getString("name"."");
String age = sharedPreferences.getInt("rank"."");

// SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        returnv ! =null? v : defValue; }}Copy the code

GetXxx is thread-safe because of the synchronized keyword modifier.

The awaitLoadedLocked() method is also called, and when SharedPreference is first created, getXxx is called, which is likely to wait until the file is loaded.

To sum up:

  • Because there’s a lot of synchronized protection inside, so. SharedPreferences are thread-safe.
  • Because SharedPreferences are first read from disk and then from memory, SharedPreferences are not process-safe.

How to ensure the security of SharedPreferences multi-process communication? (Multi-process solution)

There are four methods:

  • MODE_MULTI_PROCESS (API level 23 deprecated)
  • Inherit the ContentProvider and implement the SharedPreferences interface
  • With the help of a ContentProvider
  • mmkv

1, MODE_MULTI_PROCESS

// ContextImpl.java
public SharedPreferences getSharedPreferences(File file, int mode) {...if((mode & Context.MODE_MULTI_PROCESS) ! =0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        sp.startReloadIfChangedUnexpectedly();
    }
    
    return sp;
}
Copy the code

When the SharedPreferences object is created, if mode is equal to MODE_MULTI_PROCESS and targetVersion is less than 11, the last modification time and file size of the disk file are checked, and the file is reloaded from the disk once it is modified.

However, this does not guarantee real-time synchronization of multi-process data.

There is currently a comment on this mode:

* @deprecated This constant was deprecated in API level 23. * MODE_MULTI_PROCESS does not work reliably in some versions  of Android, and furthermore does not provide any * mechanism for reconciling concurrent modifications across processes. Applications  should not attempt to use it. Instead, * they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.Copy the code

MODE_MULTI_PROCESS is deprecated at API level 23 and ContentProvider is recommended, so this method is no longer available.

2. Inherit ContentProvider and implement SharedPreferences interface

This approach has a ready-made open source libraries: MultiprocessSharedPreferences

MultiprocessSharedPreferences implement multiple processes using a ContentProvider SharedPreferences, speaking, reading and writing:

1. ContentProvider naturally supports multi-process access.

2, use the internal private BroadcastReceiver implement multi-process OnSharedPreferenceChangeListener listening in.

3. Use ContentProvider

This solution also uses the ContentProvider, but only with the help of the ContentProvider to switch the process.

SharedPreferences Get and PUT are used in the same process (PROCESS_1), and ContentProvider is used to switch processes.

// SharedPreferencesManager.java
public void setString(String key, String value) {
    if (PROCESS_1.equals(getProcessName())) {
        // Whichever process is invoked, the save operation will end up in PROCESS_1
        SharedPreferences.Editor editor = mSharedPreferences.edit();
        editor.putString(key, value);
        editor.apply();
    } else {
        // If the non-process_1 process is called, it will go here, where the process is switched in MyContentProvider to PROCESS_1MyContentProvider.setStringValue(mContext, key, value); }}// SharedPreferencesManager.java
public String getString(String key, String defValue) {
    if (PROCESS_1.equals(getProcessName())) {
        // Whichever process is invoked, it will eventually be read in PROCESS_1
        return mSharedPreferences.getString(key, defValue);
    } else {
        // If the non-process_1 process is called, it will go here, where the process is switched in MyContentProvider to PROCESS_1
        returnMyContentProvider.getStringValue(mContext, key, defValue); }}// MyContentProvider.java
public static void setStringValue(Context context, String key, String value) {
    ContentValues contentvalues = new ContentValues();
    contentvalues.put(EXTRA_TYPE, TYPE_STRING);
    contentvalues.put(EXTRA_KEY, key);
    contentvalues.put(EXTRA_VALUE, value);

    try {
        // Switch the save operation by overriding the ContentProvider update() method
        context.getContentResolver().update(MY_CONTENT_PROVIDER_URI, contentvalues, null.null);
    } catch(Exception e ) { e.printStackTrace(); }}// MyContentProvider.java
// Switch the save operation by overriding the ContentProvider update() method
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
    if (values == null) {
        return 0;
    }

    int type = values.getAsInteger(EXTRA_TYPE);
    if (type == TYPE_STRING) {
        // When I call back to setString(), I have already been switched to PROCESS_1
        SharedPreferencesManager.getInstance(getContext()).setString( values.getAsString(EXTRA_KEY), 
            values.getAsString(EXTRA_VALUE));
    }

    return 1;
}

// MyContentProvider.java
public static String getStringValue(Context context, String key, String defValue){
    ContentValues contentvalues = new ContentValues();
    contentvalues.put(EXTRA_TYPE, TYPE_STRING);
    contentvalues.put(EXTRA_KEY, key);
    contentvalues.put(EXTRA_VALUE, defValue);

    Uri result;

    try {
        // Switch the process of reading by overriding the Insert () method of the ContentProvider
        result = context.getContentResolver().insert(MY_CONTENT_PROVIDER_URI, contentvalues);
    } catch (Exception e) {
        return defValue;
    }

    if (result == null) {
        return defValue;
    }

    return result.toString().substring(LENGTH_CONTENT_URI);
}

// MyContentProvider.java
// Override the ContentProvider insert() method to switch the save operation
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
    if (values == null) {
        return null;
    }

    String res = "";
    int type = values.getAsInteger(EXTRA_TYPE);
    if (type == TYPE_STRING) {
        // When I call back to getString(), I have already been switched to PROCESS_1
        res += SharedPreferencesManager.getInstance(getContext()).getString(
                values.getAsString(EXTRA_KEY), values.getAsString(EXTRA_VALUE));
    }

    return Uri.parse(MY_CONTENT_PROVIDER_URI.toString() + "/" + res);
}
Copy the code

To summarize this scenario:

Select a process to be used for GET and PUT operations (PROCESS_1 in this case), determine the process before each GET and PUT operation, and only the selected process can use SharedPreferences. Other processes switch through the ContentProvider.

2. Rewrite the Insert () and update() methods of ContentProvider to switch processes.

Write a Demo on the lot: MyMultiProcessSharedpreferences

The above two schemes are implemented using ContentProvider, the advantage is that data synchronization is not easy to make mistakes, simple and easy to use, the disadvantage is slow, slow start, slow access, so there is the fourth scheme, look down.

4, MMKV

MMKV is a key-value component based on MMAP memory mapping. Protobuf is used to realize the underlying serialization/deserialization, which has high performance and strong stability. It has been used on wechat since the middle of 2015, and its performance and stability have been verified over time. It has also been ported to Android/macOS/Windows platforms and is open source.

Github link: MMKV

Reference:

Understand SharedPreferences inside and out

SharedPreferences Multi-process solution

Android: This is a comprehensive & detailed SharePreferences study guide