preface

It has been a long time since the last Monday. During that time, I went to start a business and worked as a product manager, and both projects failed. Now I restart to start a new technical road. Android third-party FrameWork and FrameWork layer content will be a more systematic arrangement. In the first chapter, we will start with a simple method, SharedPreferenced, which is the most common data persistence method in Android. SharePreference allows us to access data in the form of key-value pairs. Start with the writing method in accordance with the general writing convention.

Qaishou, Bytedance, Baidu, Meituan Offer tour (Android experience sharing)

Based on using

SharedPreferences preferences = getSharedPreferences("name", MODE_PRIVATE);
preferences.getString("name"."");
preferences.edit().putString("name", null).apply();
preferences.edit().putString("name", null).commit();
Copy the code

The above is an implementation of SharedPreferences. To obtain a SharedPreference by specifying the name and type, the type will be expanded, and then the get method can obtain the corresponding access value according to the key value. For data writing, The data is added by calling the put method of the corresponding data type after the Edit method, and finally committed by calling the Apply and COMMIT methods. Next, let’s take a look at how SharedPreferences implement read and write operations and what the differences are between different types of SharedPreferences.

SharedPreferences implementation

Here are the modes supported by SharedPreferences

  • MODE_PRIVATE

An application whose userID can only be read or shared by the currently created application

  • MODE_WORLD_READABLE

Other applications can read

  • MODE_WORLD_WRITEABLE

Other applications can read and write

These are the three common modes for SharedPreferences implementations

Each Android package file (.apk) installed on the device is assigned its own unified Linux user ID, and a sandbox is created for it to prevent it from affecting other applications (or other applications affecting it). The user ID is assigned when the application is installed on the device and remains permanent on the device. With the Shared User ID, multiple APKs with the same User ID can be configured to run in the same process. So by default, you can access arbitrary data from one another. It can also be configured to run as different processes and access databases and files in the data directories of other APKs. Just like accessing the program’s data.

How to read?

In the context, we can get SharePreferences by calling the getSharePreferenced method, so let’s look at the implementation.

File file;
synchronized (ContextImpl.class) {
    if (mSharedPrefsPaths == null) {
        mSharedPrefsPaths = new ArrayMap<>();
    }
    file = mSharedPrefsPaths.get(name);
    if(file == null) { file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); }}return getSharedPreferences(file, mode);
Copy the code

First look by name from SharedPrefsPaths, which is an ArrayMap with the name we passed as the key and the disk store File as the value. When SharedPrefsPaths is null, we create a Get a value from it based on name. If not, call getSharedPreferencesPath to create a File, which is then stored in SharedPrefsPaths. Finally, getSharedPreferences is called to obtain SharePreference based on File and Mode

SharePrefsPaths is an ArrayMap with name as the key and File as the value. When File is not found, a File named name.xml is created in the specified folder based on the name. And then cache it.

public SharedPreferences getSharedPreferences(File file, int mode) {
    SharedPreferencesImpl sp;
    synchronized (ContextImpl.class) {
        final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
        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; }}...return sp;
}
Copy the code

The actual retrieval of SharedPreferences is stored in an ArrayMap with File as the key and SharedPreferencesImpl as the value, and is recreated when it cannot be found in the Cache. The core implementation of SharedPreferences is in the SharedPreferencesImpl. Next, let’s look at the concrete implementation of the SharedPreferencesImpl.

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

The data is loaded by calling startLoadFromDisk in its constructor, which is implemented by opening a new thread. The core implementation code of loadFromDisk is as follows.

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

By parsing XML to get a Map, then you can query the Map by name to get the corresponding value. Now that we know how to read values from SharedPreferences, we can read values from the local disk to the Map in memory, and we can search the Map first. Write back to disk when changes are made. So let’s look at how the data should be written back.

How to write?

For SharedPreferenced values, we start with commit. Before analyzing commit, let’s take a look at how Edit is implemented.

public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}
Copy the code

First, when the edit method is called to perform a write operation, a read lock is acquired to wait for the read operation to complete. Since the read operation is performed in a child thread, we need to await the read operation. An EditorImpl instance is returned. It has an internal Map to store the data to be written and modified. Later, when we call commit and apply, we modify the data in memory first, and then the local file. Next, take a look at the commit method.

@Override
public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}
Copy the code

In the COMMIT method, the commitToMemory method is called first, followed by the enqueueDiskWrite method to write data to disk.

private MemoryCommitResult commitToMemory() {
    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);
        }
        mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                if(! mapToWriteToDisk.isEmpty()) { changesMade =true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }

            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // "this" is the magic value for a removal mutation. In addition,
                // setting a value to "null" for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == this || v == null) {
                    if(! mapToWriteToDisk.containsKey(k)) {continue;
                    }
                    mapToWriteToDisk.remove(k);
                } else {
                    if (mapToWriteToDisk.containsKey(k)) {
                        Object existingValue = mapToWriteToDisk.get(k);
                        if(existingValue ! = null && existingValue.equals(v)) {continue;
                        }
                    }
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    keysModified.add(k);
                }
            }

            mModified.clear();

            if(changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; }}return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
            mapToWriteToDisk);
}
Copy the code

The implementation of commitToMemory is to synchronize the modified Map of records to the Map originally loaded from locally. At the heart of the enqueueDiskWrite implementation is a runnable, which encapsulates file writes. When a commit call is made, it is executed in the current thread, and when apply is called, it is executed in the HandlerThread of a child thread.

final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                mDiskWritesInFlight--;
            }
            if(postWriteRunnable ! = null) { postWriteRunnable.run(); }}};Copy the code

The apply implementation writes changes to memory first and then to disk. Commit writes directly to the current thread, whereas apply writes through a HandlerThread. The queued implementation, which is implemented via CountDownLatch, can await blocking and be written to disk when its countDown method is called. CountDownLatch can be used to wait for multiple tasks to be executed. If a task needs to be executed after three tasks have been executed, CountDownLatch can be used. Since apply is executed through a separate thread, does it block the main thread? The answer is yes, in the waitToFinish method in QueueWork, which is called during the onPause of the Activity and completes all tasks in the queue. So it also blocks execution of the main thread.

Disk file loading and writing

SharedPreferences SharedPreferences SharedPreferences SharedPreferences SharedPreferences

synchronized (mLock) {
    awaitLoadedLocked();
}

return new EditorImpl();
Copy the code

When the EditorImpl implementation is returned, awaitLoadedLocked is first called to wait for locks to be read from the local disk. Write operations are performed only when the local file has been loaded into memory.

For operation on a local disk file, in order to prevent abnormal occurred in the process of writing, so at the time of writing, will do a backup the current file first, and then to write, if successful, will delete backup file, the time to read and write if determine to have a backup file, you can think of the last file is written to failure, When the data is read, it is read from the backup file, and the backup file is renamed. Here the file operation with the backup file to do conversion to prevent data write error design is quite clever.

The problem?

Does SharePreference support multithreading?

SharePreference supports multi-threaded read and write operations, which can be performed without data confusion and controlled by internal locks. There is only one instance of SharePreference for the same process.

Does SharePreference support multiple processes?

SharePreference does not support multiple processes, because data on disk is loaded only once. Therefore, changes made by one process cannot be reflected in another process.

How does Mode work in SharePreference?

After data is written, set permissions for the current file according to Mode through setPermission of FileUtils, and then judge the read and write permissions of the file according to the Mode written at the time of writing.

conclusion

Through the analysis of SharedPreferences, it can be seen that its general implementation is to store files in the local disk through XML. When we obtain an instance of SharedPreferences, start threads to read the data from the local disk into memory and store it through a Map. Then, a new Map will be created to access the current modified data. When commit and apply are called, the modified data will be written back to memory and disk, and the corresponding judgment will be made according to the MODE set.