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