An in-depth understanding of the principle and mechanism of SharedPreference

1, SharedPreferences is easy to use

1. Create

The first parameter is the name of the stored xml file, and the second parameter is the opening method, which is generally used

Context.MODE_PRIVATE;
SharedPreferences sp=context.getSharedPreferences("name", Context.MODE_PRIVATE);

2. Write

//You can create a new SharedPreference to manipulate the stored files
SharedPreferences sp=context.getSharedPreferences("name", Context.MODE_PRIVATE);
//Like writing data in SharedPreference, you need to use Editor
SharedPreference.Editor editor = sp.edit();
//Similar key value pairs
editor.putString("name", "string");
editor.putInt("age", 0);
editor.putBoolean("read", true);
//editor.apply();
editor.commit();
  • Both apply and commit are submitted and saved. The difference is that apply is executed asynchronously and does not need to wait. No matter deleting, modifying or adding, you must call apply or commit to submit and save;
  • About update: if the inserted key already exists. Then the original key will be updated;
  • Once the application is uninstalled, SharedPreference will also be deleted;

3. Read

SharedPreference sp=context.getSharedPreferences("name", Context.MODE_PRIVATE);
//The first parameter is the key name and the second is the default value
String name=sp.getString("name", "Not yet");
int age=sp.getInt("age", 0);
boolean read=sp.getBoolean("isRead", false);

4. Search

SharedPreferences sp=context.getSharedPreferences("name", Context.MODE_PRIVATE);
//Check whether the current key exists
boolean isContains=sp.contains("key");
//Use getAll to return all available key values
//Map<String,?> allMaps=sp.getAll();

5. Delete

When we want to clear the data in SharedPreferences, we must first clear() and then commit(), and we cannot delete the xml file directly;

SharedPreference sp=getSharedPreferences("name", Context.MODE_PRIVATE);
SharedPrefence.Editor editor=sp.edit();
editor.clear();
editor.commit();
  • getSharedPreference() will not generate files, as we all know;
  • After deleting the file, execute commit() again. The deleted file will be reborn, and the data of the reborn file is the same as that before deletion;
  • After deleting the file, the content stored in the Preferences object remains unchanged without the program completely quitting and stopping. Although the file is gone, the data still exists; The data will not be lost until the program completely exits and stops;
  • When clearing SharedPreferences data, be sure to execute editor clear(),editor.commit() cannot simply delete the file. This is the final conclusion and should be paid attention to

    2, SharedPreferences source code analysis

1. Create

SharedPreferences preferences = getSharedPreferences("test", Context.MODE_PRIVATE);

In fact, the real implementation class of context is ContextImp, so go to the getSharedPreferences method of ContextImp to see:

@Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        ......
        File file;
        synchronized (ContextImpl.class) {
            if (mSharedPrefsPaths == null) {
            //Definition type: arraymap < string, file > msharedprefspaths;
                mSharedPrefsPaths = new ArrayMap<>();
            }
            //Can I get the file from mSharedPrefsPaths
            file = mSharedPrefsPaths.get(name);
            if (file == null) {//If the file is null
            //Create a file
                file = getSharedPreferencesPath(name);
                take name,file Key value pairs are stored in the set
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

ArrayMap<String, File> mSharedPrefsPaths; Object is used to store the SharedPreference file name and the corresponding path. The path is obtained in the following methods: obtain data/data / package name / shared_ In prefs / directory

@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
}

Object creation starts after the path

@Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
    //Focus 1
        checkMode(mode);
    .......
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
        //Get cache object (or create cache object)
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            //Get the Sp object from the cache object with the key file
            sp = cache.get(file);
            //If it is null, it means that there is no sp object of the file in the cache
            if (sp == null) {
            //Key 2: read files from disk
                sp = new SharedPreferencesImpl(file, mode);
                //Add to memory
                cache.put(file, sp);
                //Return sp
                return sp;
            }
        }
        //If set to MODE_MULTI_PROCESS mode, the SP's startReloadIfChangedUnexpectedly method will be executed.
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

It is the method before overloading, but the input parameter is changed from File name to File, and the creation process is locked. All package names and corresponding files stored in the system are obtained through the method getSharedPreferencesCacheLocked(), which is why each sp File has only one corresponding SharedPreferencesImpl implementation object

technological process:

  • Get the cache and get data from the cache to see if there is an sp object. If there is an sp object, it will be returned directly
  • If it doesn't exist, get the data from the disk,
  • After the data obtained from the disk is added to the memory,
  • Return sp;

getSharedPreferencesCacheLocked

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;
    }
  • In the getsharedpreferences (File, int mode) method, obtain the - SharedPreferences impl object from the branch File in the above system cache. If it has not been used before, you need to create an object through the method checkMode(mode);
  • First check whether the mode is three modes, and then use sp = new SharedPreferencesImpl(file, mode);
  • Create objects and put the created objects into the system's packagePrefs to facilitate direct acquisition in the future;

    SharedPreferencesImpl(File file, int mode) {
          mFile = file; //Storage file
          //Backup file (disaster recovery file)
          mBackupFile = makeBackupFile(file);
          //pattern
          mMode = mode;
          //Has it been loaded
          mLoaded = false;
          // Store key value pair information in the file
          mMap = null;
          //You can know from the name: start loading data from disk
          startLoadFromDisk();
      }
  • It mainly sets several parameters. mFile is the original file; mBackupFile is the suffix Backup files of bak;
  • mLoaded identifies whether the modified file is being loaded;
  • mMap is used to store the data in sp files. It is also in the form of key value pairs during storage, and it is also obtained through this. This means that every time sp is used, the data is written into memory, that is, the reason why sp data stores data quickly. Therefore, sp files cannot store a large amount of data, otherwise it will easily lead to OOM during execution;
  • The error reported when mThrowable loads the file;
  • The following is the method of loading data: startLoadFromDisk(); Load data from sp file into mMap

2,startLoadFromDisk()

 private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        //Start the sub thread to load disk data
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }
    private void loadFromDisk() {
        synchronized (mLock) {
        //If it is loaded, return directly
            if (mLoaded) {
                return;
            }
            //Whether the backup file exists,
            if (mBackupFile.exists()) {
            //Delete original file
                mFile.delete();
                //Name the backup file: xml file
                mBackupFile.renameTo(mFile);
            }
        }
        .......
        Map map = null;
        StructStat stat = null;
        try {
        //The following is to read the data
            stat = Os.stat(mFile.getPath());
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }
        synchronized (mLock) {
        //Has been loaded,
            mLoaded = true;
            //The data is not null
            if (map != null) {
            //Assign map to the mMap object of the global storage file key value pair
                mMap = map;
                //Update the modification time and file size of memory
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            //Important: wake up all waiting threads with mLock lock
            mLock.notifyAll();
        }
    }
  • First, judge whether the backup file exists. If so, change the suffix of the backup file; Then start reading the data, assign the read data to the mMap object where the global variable stores the file key value pair, and update the modification time and file size variables;
  • Wake up all waiting threads with mLock as lock;
  • So far, even if the initialization of SP objects is completed, it can be seen that it is a secondary cache process: disk to memory;

3. get gets the key value pair in the SP

@Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) { Lock judgment
            awaitLoadedLocked(); //Waiting mechanism
            String v = (String)mMap.get(key); //Get data from key value pairs
            return v != null ? v : defValue;
        }
    }
 private void awaitLoadedLocked() {
        .......
        while (!mLoaded) { //When the data is loaded, the value is true
            try {
            //Thread waiting
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

If the data is not loaded (that is, mLoaded=false), the thread will wait;

4. putXXX and apply source code

public Editor edit() {
        //Same principle as getXXX
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        //Return EditorImp object
        return new EditorImpl();
    }
 public Editor putBoolean(String key, boolean value) {
      synchronized (mLock) {
           mModified.put(key, value);
           return this;
         }
 }
       public void apply() {
            final long startTime = System.currentTimeMillis();
            //According to the name, you can know: submit data to memory
            final MemoryCommitResult mcr = commitToMemory();
           ........
//Submit data to disk
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            //Key: call listener
            notifyListeners(mcr);
        }
  • First, execute commitToMemory and submit data to memory; Then submit the data to the disk;
  • Then the listener is called;

5,commitToMemory

private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            //Data set written to disk
            Map<String, Object> mapToWriteToDisk;
            synchronized (SharedPreferencesImpl.this.mLock) {
                if (mDiskWritesInFlight > 0) {
                    mMap = new HashMap<String, Object>(mMap);
                }
                //Assign the cache set to mapToWriteToDisk 
                mapToWriteToDisk = mMap;
                .......
                synchronized (mLock) {
                    boolean changesMade = false;
                    //Key: clear data
                    if (mClear) {
                        if (!mMap.isEmpty()) {
                            changesMade = true;
                            //Clear the key value pair information in the cache
                            mMap.clear();
                        }
                        mClear = false;
                    }
                    //Loop mModified to update the data in mModified to mMap
                    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 (!mMap.containsKey(k)) {
                                continue;
                            }
                            mMap.remove(k);
                        } else {
                            if (mMap.containsKey(k)) {
                                Object existingValue = mMap.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            //Note: at this time, the key value pair information is written to the cache set
                            mMap.put(k, v);
                        }
.........
                    }
                    //Empty temporary collection
                    mModified.clear();
                   ......
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }
  • mModified is the set of key value pairs added in this update;
  • mClear is assigned when we call the clear() method;
  • The general process is as follows: first judge whether it is necessary to empty the memory data, then cycle the mModified set and add the updated data to the key value pair set in memory;

6. commit method

public boolean commit() {
            .......
            //Update data to memory
            MemoryCommitResult mcr = commitToMemory();
            //Update data to disk
            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
            //Wait: wait for the disk update data to complete
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } finally {
                if (DEBUG) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " committed after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
            //Execute listener callback
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
  • First, apply has no return value, and commit has a return value;
  • In fact, the callback executed by the apply method is executed in parallel with the data written to the disk, while the callback executed by the commit method waits for the completion of the data written to the disk;

2, Detailed explanation of QueuedWork

1,QueuedWork

The QueuedWork class is used after sp initialization. As we saw earlier, both the apply and commit methods are implemented through QueuedWork;

QueuedWork is a management class. As the name suggests, there is a queue to manage and schedule all queued work;

One of the most important is to have a HandlerThread

private static Handler getHandler() {
        synchronized (sLock) {
            if (sHandler == null) {
                HandlerThread handlerThread = new HandlerThread("queued-work-looper",
                        Process.THREAD_PRIORITY_FOREGROUND);
                handlerThread.start();
                sHandler = new QueuedWorkHandler(handlerThread.getLooper());
            }
            return sHandler;
        }
    }

2. queue

// If it is commit, it cannot be delayed. If it is apply, it can be delayed
    public static void queue(Runnable work, boolean shouldDelay) {
        Handler handler = getHandler();
        synchronized (sLock) {
            sWork.add(work);
            if (shouldDelay && sCanDelay) {
                // The default delay time is 100ms
                handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
            } else {
                handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
            }
        }
    }

3. Message processing

private static class QueuedWorkHandler extends Handler {
        static final int MSG_RUN = 1;
        QueuedWorkHandler(Looper looper) {
            super(looper);
        }
        public void handleMessage(Message msg) {
            if (msg.what == MSG_RUN) {
                processPendingWork();
            }
        }
    }
    private static void processPendingWork() {
        synchronized (sProcessingWork) {
            LinkedList<Runnable> work;
            synchronized (sLock) {
                work = (LinkedList<Runnable>) sWork.clone();
                sWork.clear();
                getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            if (work.size() > 0) {
                for (Runnable w : work) {
                    w.run();
                }
            }
        }
    }
  • It can be seen that scheduling is very simple. There is a sWork inside, which traverses all runnable execution when it needs to be executed;
  • For the apply operation, there will be a certain delay before executing the work, but for the commit operation, the scheduling will be triggered immediately, and it is not just the task transmitted by the commit, but all the work in the queue will be scheduled immediately;

4,waitToFinish

Many places in the system will wait for sp to write files. The waiting method is to call queuedwork waitToFinish();

public static void waitToFinish() {
        Handler handler = getHandler();
        synchronized (sLock) {
            // Remove all messages and start scheduling all work directly
            if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
                handler.removeMessages(QueuedWorkHandler.MSG_RUN);
            }
            sCanDelay = false;
        }
        StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
        try {
            // If waitto finish is called, all work will be executed immediately
            processPendingWork();
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
        }
        try {
            // After all the work has been executed, finish needs to be executed
            // In the previous step of apply ing, queuedwork addFinisher(awaitCommit);
            // The implementation is to wait for the sp file to be written
            // If you do not use msg to schedule but wait to finish, the runnable will be executed here
            while (true) {
                Runnable finisher;
                synchronized (sLock) {
                    finisher = sFinishers.poll();
                }
                if (finisher == null) {
                    break;
                }
                finisher.run();
            }
        } finally {
            sCanDelay = true;
        }
        ...
    }

The processing logic of the four components in the system is implemented in ActivityThread. During the execution of the service/activity life cycle, it will wait for the writing of sp to complete, just by calling queuedwork Waitto finish(), ensure that the app data is correctly written to the disk;

5. Suggestions for sp use

  • The requirement for real-time data is not high. Try to use apply
  • If the business requires that the data must be written successfully, use commit
  • Reduce the frequency of sp operations and try to write all data in one commit
  • Consider not accessing sp on the main thread
  • The data written to sp should be as lightweight as possible

    Summary:

The implementation of SharedPreferences itself is divided into two steps. One is memory and the other is disk, and the main thread depends on the writing of SharedPreferences. Therefore, when io becomes a bottleneck, App will become stuck due to SharedPreferences, and ANR will occur in severe cases. To sum up, there are the following points:

  • The data stored in the xml file will be loaded into memory, so the data can be obtained quickly
  • apply is an asynchronous operation. Data is submitted to memory and will not be submitted to disk immediately
  • commit is a synchronous operation that waits for data to be written to disk and returns results
  • If the same thread commit s multiple times, the subsequent thread will wait for the previous execution to end
  • If multiple threads commit to the same sp concurrently, all subsequent tasks will enter the QueuedWork for execution and wait until the first execution is completed

Keywords: Android source code

Added by DotSPF on Tue, 01 Mar 2022 11:07:28 +0200