Reviewed the use of SharePreference, and understand the source code of SharePreference, to solve the SharePreference problem in the case of multi process, make notes.

Reference article:

Source code analysis:

www.jianshu.com/p/8eb2147c3…

www.jianshu.com/p/3b2ac6201…

SharePreference’s multi-process solution:

Juejin. Cn/post / 684490…

SharePreference

A lightweight repository for storing application configuration information in the Android platform. Essentially an XML file that holds data in key-value pairs.

File storage address: /data/data/package name/shared_prefs to view the file

Simple Usage Examples

// The first parameter is the name of the file, and the second parameter is the mode of operation. SharedPreferences = getSharedPreferences("test", MODE_PRIVATE); // Create Editor object SharedPreferences.Editor Editor = SharedPreferences.Edit (); // Save data editor.putString("name"."donggua"); //editor.commit(); editor.apply(); / / read the data String result = sharedPreferences. Get String ("name"."Default value");
Copy the code

The difference between commit and apply

When we commit data using commit, we find that the IDE prompts us to use apply.

  • Commit: commit synchronously. Commit synchronously writes data to disk and memory cache, and returns a value.
  • Apply: Asynchronous commit, data will be synchronized to the memory cache, and then asynchronously saved to disk, may fail, failure will not receive error callback.

The difference between the two:

  • Commit is slightly slower than Apply. In a process, the Apply method takes precedence if you don’t care if the result of the submission is successful.
  • Both are atomic operations, but atoms operate differently. Commit data is committed, saved to memory, and then saved to disk without interruption. The apply method saves the data to memory and then returns, asynchronously saves the data to disk,

Source code analysis

Get the SharePreference object

ContextImpl is the implementation class of Context, which implements the getSharedPreferences method.

  • Since SharedPreferences support custom File names, ArrayMap

    is used to cache SharedPreferencesImpl objects for different files. A File corresponds to a SharePreference object.
    ,>
  • GetSharedPreferencesCacheLocked (), obtain the cache ArrayMap < File, SharedPreferencesImpl > object, there is not one of them is created.
@Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; Synchronized (contextimpl.class) {// obtain cached map final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); SharePreference sp = cache.get(file);if (sp == null) {
            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"); 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) { // If somebodyelse (some other process) changed the prefs
        // file behind our back, we reload it.  This has been the
        // historical (if undocumented) behavior.
        sp.startReloadIfChangedUnexpectedly();
    }
    return sp;
}


@GuardedBy("ContextImpl.class")
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
    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

SharedPreferencesImpl is an implementation class of the SharedPreferences interface that implements the COMMIT and apply methods. Take a look at the SharedPreferencesImpl constructor. A startLoadFromDisk method is called asynchronously, which reads the XML information stored in the SharePreference file from disk into memory and saves it into a Map.

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

private void loadFromDisk(){// omit some code try {stat = Os.stat(mFile.getPath());
    if(mFile.canRead()) { BufferedInputStream str = null; try { str = new BufferedInputStream( new FileInputStream(mFile), 16 * 1024); Map = (map <String, Object>) xmlutils.readmapxml (STR); } catch (Exception e) { Log.w(TAG,"Cannot read " + mFile.getAbsolutePath(), e);
        } finally {
            IoUtils.closeQuietly(str);
        }
    }
} catch (ErrnoException e) {
    // An errno exception means the statfailed. Treat as empty/non-existing by // ignoring. } catch (Throwable t) { thrown = t; }} // Omit some code. // Assign the map saved as a result of parsingif(map ! = null) { mMap = map; mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; }Copy the code

Read the data

For example, SharedPreferencesImpl implements getString(), which reads data directly from mMap in memory, without involving disk operations. (Suddenly, I thought that reading data also need to read the file file)

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

Save the data

The EditorImpl class implements the Editor interface. Both Apply and COMMIT call the commitToMemory method to save the data to memory, followed by enqueueDiskWrite to save the data to disk.

Private final map <String, Object> mModified = new HashMap<>(); //CountDownLatch waits until save to disk is complete. final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); result@override public Boolean Result@override public Boolean result@override public Booleancommit() {
    long startTime = 0;
    if(DEBUG) { startTime = System.currentTimeMillis(); } // Save to MemoryCommitResult MCR = commitToMemory(); / / save to disk SharedPreferencesImpl. This. EnqueueDiskWrite (MCR, null / * sync write on this thread okay * /); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) {return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    returnmcr.writeToDiskResult; } // The result of the operation cannot be obtainedapply() { final long startTime = System.currentTimeMillis(); // Save to memory. Final MemoryCommitResult MCR = commitToMemory(); final Runnable awaitCommit = newRunnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms"); }}}; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = newRunnable() {
            @Override
            public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); }}; 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

SharePreference in multiple processes

The default is SharePreference in a single process, and the read operation is directly from the Map in memory, without involving IO operations. In the case of multiple processes, memory is not shared between different processes, and reading and writing to the same SharePreference can be problematic. For example, when multiple processes modify the same SharedPreference, one process will always get a result that is not the result after real-time modification.

Solution: It is recommended to use ContentProvider to handle file sharing between multiple processes.

Features of ContentProvider:

  • The internal synchronization mechanism of the ContentProvider prevents multiple processes from accessing the content at the same time to avoid data conflicts.
  • Update () and Query () are the core operations of ContentProvider. The data sources can be replaced with files or SharedPreferences.

So we can use ContentProvider to do an intermediary, let it help us to implement multi-process synchronization mechanism, inside the operation of data to SharedPreferences to achieve. This allows cross-process access to SharePreference.

Let’s write a simple demo and read it by passing in the uri. For example, in the following code, the second of the path field is fileName and the third is key.

public class MultiProcessSharePreference extends ContentProvider{ @Nullable @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @nullable String sortOrder) {// Get XML file name, default path first log. d(TAG,"query: uri:" + uri);
    String tableName = uri.getPathSegments().get(0);
    String name = uri.getPathSegments().get(1);
    String key = uri.getPathSegments().get(2);
    Log.d(TAG, "query: tableName:" + tableName);
    Log.d(TAG, "query: fileName:" + name);
    Log.d(TAG, "query: key:"+ key); SharedPreferences sharedPreferences = getContext().getSharedPreferences(name, Context.MODE_PRIVATE); // Create a cursor object MatrixCursor cursor = null; switch (uriMatcher.match(uri)) {case CODE_PREFERENCE_STRING:
            String value = sharedPreferences.getString(key, "Default value");
            cursor = new MatrixCursor(PREFERENCE_COLUMNS, 1);
            MatrixCursor.RowBuilder rowBuilder = cursor.newRow();
            rowBuilder.add(value);
            break;
        default:
            Log.d(TAG, "query: Uri No Match");
    }
    returncursor; }}Copy the code
  • MatrixCursor: If you need a cursor and do not have a ready-made cursor, you can use a MatrixCursor to implement a virtual table. RowBuilder is used to add rows to the matrixcursor. RowBuilder add method is used to add values to rows. Use scenarios: For example, when a ContentProvider query method returns a cursor and the data source uses SharePreference, a MatrixCursor can be used. Essentially, MartixCursor uses a bit data to simulate a two-dimensional data, and the corresponding data can be found according to the row value and column value.

MatrixCursor source code parsing: blog.csdn.net/zhang_jun_l…

Public static final String COLUMN_VALUE ="value"; Private static String[] PREFERENCE_COLUMNS = {COLUMN_VALUE}; private static String[] PREFERENCE_COLUMNS = {COLUMN_VALUE}; MatrixCursor cursor = new MatrixCursor(PREFERENCE_COLUMNS, 1); RowBuilder = cursor.newRow(); // Add a row to the addRow method of matrixCursor.rowBuilder = cursor.newrow (); rowBuilder.add(value);Copy the code
  • To optimize the idea:
    • So inside the ContentProvider, we’re going to cache a HashMap

      , where the key is the file name and the value is the corresponding SharePreference object, You don’t have to load the SharePreference object every time.
      ,sharepreference>
    • Implement a callback listener inside the ContentProvider to notify the subscriber of any key changes.