current position:Home>In depth analysis of Android SharedPreferences source code

In depth analysis of Android SharedPreferences source code

2022-01-27 00:11:38 Manon Xiaofeng

summary

SharedPreferences( abbreviation SP) yes Android Common data storage methods in ,SP use key-value( Key value pair ) form , It is mainly used for lightweight data storage , It is especially suitable for saving the configuration parameters of the application , But not recommended SP To store large-scale data , It can degrade performance .

SP use XML File format to save data , The file is located at /data/data/<packageName>/shared_prefs/.

Examples of use

//  load SP File data ,“my_prefs” For the file name 
SharedPreferences sp = getSharedPreferences("my_prefs", Context.MODE_PRIVATE);
//  Save the data 
Editor editor = sp.edit();
editor.putString("blog", "www.xiaox.com");
//  Submit data : Synchronization mode , A return value indicates whether the data is saved successfully 
boolean result = editor.commit(); 
//  Submit data : Asynchronous way , no return value // 
editor.apply()
//  Reading data 
String blog = sp.getString("blog", "");

my_prefs.xml The contents of the document :

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>    
    <string name="blog">www.xiaox.com</string>
</map>

framework

Class diagram

img

explain :SharedPreferences And Editor Just two interfaces ,SharedPreferencesImpl and EditorImp The corresponding interfaces are implemented respectively . in addition ,ContextImpl Record SharedPreferences Important data , as follows :

  • sSharedPrefsCache: By package name key, second level key yes SP file , With SharedPreferencesImp by value Nesting of map structure ,sSharedPrefsCache Is a static member variable , There is only one copy of each process , And by the ContextImpl.class Lock protection .
  • mSharedPrefsPaths: Record all SP file , Named by file name key, The specific document is value Of map structure .
  • mPreferencesDir: Is the value SP In the directory , namely /data/data/<packageName>/shared_prefs/

Workflow

img

explain

  1. putXxx() operation : Write data to EditorImpl.mModified;
  2. apply() perhaps commit() operation : a. First call commitToMemory(), Synchronize data to SharedPreferencesImpl Of mMap, And save to MemoryCommitResult Of mapToWriteToDisk; b. Call again enqueueDiskWrite(), Write to disk file ; Before that, save the original data to .bak Postfix file , Used to recover any abnormal data in the process of writing to the disk .
  3. getXxx() operation : from SharedPreferencesImpl.mMap Reading data .

Source code analysis (API 28)

obtain SharedPreferences

Can pass Activity.getPreferences(mode)PreferenceManager.getDefaultSharedPreferences(context) perhaps Context.getSharedPreferences(name,mode) To get SharedPreferences example , The final call is ContextImpl Of getSharedPreferences(name, mode).

ContextImpl#getSharedPreferences(name, mode)

class ContextImpl extends Context {
    
	@GuardedBy("ContextImpl.class")    
		     private ArrayMap<String, File> mSharedPrefsPaths;
	// ...
	@Override    
		    public SharedPreferences getSharedPreferences(String name, int mode) {
    
		// ...
		File file;
		synchronized (ContextImpl.class) {
    
			if (mSharedPrefsPaths == null) {
    
				mSharedPrefsPaths = new ArrayMap<>();
			}
			//  First from mSharedPrefsPaths Query whether the corresponding file exists  
			file = mSharedPrefsPaths.get(name);
			if (file == null) {
    
				//  If the file doesn't exist , Create a new file  
				file = getSharedPreferencesPath(name);
				//  Save the newly created file to mSharedPrefsPaths, Named by file name key 
				mSharedPrefsPaths.put(name, file);
			}
		}
		return getSharedPreferences(file, mode);
	}
	@Override    
	    public File getSharedPreferencesPath(String name) {
    
		return makeFilename(getPreferencesDir(), name + ".xml");
	}
	private File getPreferencesDir() {
    
		synchronized (mSync) {
    
			if (mPreferencesDir == null) {
    
				//  Create directory /data/data/<packageName>/shared_prefs/ 
				mPreferencesDir = new File(getDataDir(), "shared_prefs");
			}
			return ensurePrivateDirExists(mPreferencesDir);
		}
	}
}

ContextImpl#getSharedPreferences(file, mode)

@Override
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");
				}
			}
			//  establish SharedPreferencesImpl 
			sp = new SharedPreferencesImpl(file, mode);
			cache.put(file, sp);
			return sp;
		}
	}
	//  Specify multi process mode , When the file is changed by another process , It will be reloaded  
	if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||        
		      getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
    
		// If somebody else (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;
}

SharedPreferencesImpl initialization

SharedPreferencesImpl.java

SharedPreferencesImpl(File file, int mode) {
    
	mFile = file;
	//  establish .bak Suffix backup file , Used when an exception occurs , You can restore data by backing up files  
	mBackupFile = makeBackupFile(file);
	mMode = mode;
	mLoaded = false;
	mMap = null;
	mThrowable = null;
	startLoadFromDisk();
}

SharedPreferencesImpl#startLoadFromDisk()

private void startLoadFromDisk() {
    
	synchronized (mLock) {
    
		mLoaded = false;
	}
	//  Read the file data to through the worker thread mMap 
	new Thread("SharedPreferencesImpl-load") {
    
		public void run() {
    
			loadFromDisk();
		}
	}
	.start();
}
private void loadFromDisk() {
    
	synchronized (mLock) {
    
		if (mLoaded) {
    
			return;
		}
		if (mBackupFile.exists()) {
    
			mFile.delete();
			mBackupFile.renameTo(mFile);
		}
	}
	// Debugging 
	if (mFile.exists() && !mFile.canRead()) {
    
		Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
	}
	Map<String, Object> map = null;
	StructStat stat = null;
	Throwable thrown = null;
	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 stat failed. Treat as empty/non-existing by 
		// ignoring.
	}
	catch (Throwable t) {
    
		thrown = t;
	}
	synchronized (mLock) {
    
		mLoaded = true;
		mThrowable = thrown;
		// It's important that we always signal waiters, even if we'll make 
		// them fail with an exception. The try-finally is pretty wide, but 
		// better safe than sorry. 
		try {
    
			if (thrown == null) {
    
				if (map != null) {
    
					mMap = map;
					mStatTimestamp = stat.st_mtim;
					mStatSize = stat.st_size;
				} else {
    
					mMap = new HashMap<>();
				}
			}
			// In case of a thrown exception, we retain the old map. That allows 
			// any open editors to commit and store updates.
		}
		catch (Throwable t) {
    
			mThrowable = t;
		}
		finally {
    
			mLock.notifyAll();
		}
	}
}

obtain SharedPreferences summary

  1. For the first use, create the corresponding xml file ;
  2. Load file contents into memory asynchronously , Execute at this time getXxx() and edit() Methods are blocking and waiting , Until all the file data is loaded into memory ;
  3. Once the data is fully loaded into memory , Follow up getXxx() Direct access to memory .

get data

SharedPreferencesImpl#getString(key, defValue)

public String getString(String key, @Nullable String defValue) {
    
	synchronized (mLock) {
    
		//  Check whether the data is loaded  
		awaitLoadedLocked();
		String v = (String)mMap.get(key);
		return v != null ? v : defValue;
	}
}
private void awaitLoadedLocked() {
    
	if (!mLoaded) {
    
		BlockGuard.getThreadPolicy().onReadFromDisk();
	}
	while (!mLoaded) {
    
		try {
    
			//  When loading is not complete , Then enter the waiting state  
			mLock.wait();
		}
		catch (InterruptedException unused) {
    
		}
	}
	if (mThrowable != null) {
    
		throw new IllegalStateException(mThrowable);
	}
}

Edit the data

obtain Editor Editor instance :SharedPreferencesImpl#edit()

public Editor edit() {
    
	synchronized (mLock) {
    
		//  Wait for data loading to complete  
		awaitLoadedLocked();
	}
	//  establish EditorImpl example  
	return new EditorImpl();
}

EditorImpl#putString(key, value)

public final class EditorImpl implements Editor {
    
	@GuardedBy("mEditorLock")    
	private final Map<String, Object> mModified = new HashMap<>();
	@GuardedBy("mEditorLock")    
	private Boolean mClear = false;
	// ...
	//  insert data  
	public Editor putString(String key, @Nullable String value) {
    
		synchronized (mEditorLock) {
    
			//  insert data , Staging to mModified 
			mModified.put(key, value);
			return this;
		}
	}
	//  Remove data  
	public Editor remove(String key) {
    
		synchronized (mEditorLock) {
    
			mModified.put(key, this);
			return this;
		}
	}
	//  Clear all the data  
	public Editor clear() {
    
		synchronized (mEditorLock) {
    
			mClear = true;
			return this;
		}
	}
}

Save the data

Save the data , Mainly called commit() and apply() Method to complete .

EditorImpl#commit()

public Boolean commit() {
    
	// ...
	//  Update data to memory  
	MemoryCommitResult mcr = commitToMemory();
	//  Synchronize memory data to file  
	SharedPreferencesImpl.this.enqueueDiskWrite(        
      mcr, null /* sync write on this thread okay */
	);
	try {
    
		//  Enter the waiting state , Until the operation of writing to the file is completed  
		mcr.writtenToDiskLatch.await();
	}
	catch (InterruptedException e) {
    
		return false;
	}
	//  Notification listener , And call back in the main thread onSharedPreferenceChanged() Method  
	notifyListeners(mcr);
	//  Returns the result of the file operation  
	return mcr.writeToDiskResult;
}

EditorImpl#commitToMemory()

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;
			//  When mClear by true, Then empty it directly mMap 
			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 It's a special value , When v It's empty , amount to remove The data  
				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);
				}
				//  Indicates that the data has changed  
				changesMade = true;
				if (hasListeners) {
    
					keysModified.add(k);
				}
			}
			//  Empty mModified The data of  
			mModified.clear();
			if (changesMade) {
    
				mCurrentMemoryStateGeneration++;
			}
			memoryStateGeneration = mCurrentMemoryStateGeneration;
		}
	}
	return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,                                  
	                                  mapToWriteToDisk);
}

EditorImpl#enqueueDiskWrite()

private void enqueueDiskWrite(final MemoryCommitResult mcr,                              
                              final Runnable postWriteRunnable) {
    
	final Boolean isFromSyncCommit = (postWriteRunnable == null);
	final Runnable writeToDiskRunnable = new Runnable() {
    
		@Override        
		        public void run() {
    
			synchronized (mWritingToDiskLock) {
    
				//  Perform file write operations  
				writeToFile(mcr, isFromSyncCommit);
			}
			synchronized (mLock) {
    
				mDiskWritesInFlight--;
			}
			if (postWriteRunnable != null) {
    
				postWriteRunnable.run();
			}
		}
	};
	//  Use commit Method , Will enter this branch , Execute on current thread  
	if (isFromSyncCommit) {
    
		Boolean wasEmpty = false;
		synchronized (mLock) {
    
			wasEmpty = mDiskWritesInFlight == 1;
		}
		if (wasEmpty) {
    
			writeToDiskRunnable.run();
			return;
		}
	}
	//  Use apply Method , Will execute the sentence , Put the task into a single threaded thread pool to execute  
	QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

EditorImpl#writeToFile()

private void writeToFile(MemoryCommitResult mcr, Boolean isFromSyncCommit) {
    
	// ...
	Boolean fileExists = mFile.exists();
	if (fileExists) {
    
		Boolean needsWrite = false;
		// Only need to write if the disk state is older than this commit 
		if (mDiskStateGeneration < mcr.memoryStateGeneration) {
    
			if (isFromSyncCommit) {
    
				needsWrite = true;
			} else {
    
				synchronized (mLock) {
    
					// No need to persist intermediate states. Just wait for the latest state to 
					// be persisted. 
					if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
    
						needsWrite = true;
					}
				}
			}
		}
		//  There is no change , Go straight back to  
		if (!needsWrite) {
    
			mcr.setDiskWriteResult(false, true);
			return;
		}
		Boolean backupFileExists = mBackupFile.exists();
		if (!backupFileExists) {
    
			//  When the backup file does not exist , Then put mFile Rename to backup file  
			if (!mFile.renameTo(mBackupFile)) {
    
				Log.e(TAG, "Couldn't rename file " + mFile                      
                      + " to backup file " + mBackupFile);
				mcr.setDiskWriteResult(false, false);
				return;
			}
		} else {
    
			//  otherwise , Delete directly mFile 
			mFile.delete();
		}
	}
	try {
    
		FileOutputStream str = createFileOutputStream(mFile);
		if (str == null) {
    
			mcr.setDiskWriteResult(false, false);
			return;
		}
		//  take mMap All information is written to the file  
		XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
		writeTime = System.currentTimeMillis();
		FileUtils.sync(str);
		fsyncTime = System.currentTimeMillis();
		str.close();
		ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
		try {
    
			final StructStat stat = Os.stat(mFile.getPath());
			synchronized (mLock) {
    
				mStatTimestamp = stat.st_mtim;
				mStatSize = stat.st_size;
			}
		}
		catch (ErrnoException e) {
    
			// Do nothing
		}
		if (DEBUG) {
    
			fstatTime = System.currentTimeMillis();
		}
		//  Write successfully , Delete backup file  
		mBackupFile.delete();
		mDiskStateGeneration = mcr.memoryStateGeneration;
		//  Return write success , Wake up waiting thread  
		mcr.setDiskWriteResult(true, true);
		return;
	}
	catch (XmlPullParserException e) {
    
		Log.w(TAG, "writeToFile: Got exception:", e);
	}
	catch (IOException e) {
    
		Log.w(TAG, "writeToFile: Got exception:", e);
	}
	//  If the file write operation fails , Delete the file that was not successfully written  
	if (mFile.exists()) {
    
		if (!mFile.delete()) {
    
			Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
		}
	}
	//  Return write failure , Wake up waiting thread  
	mcr.setDiskWriteResult(false, false);
}

EditorImpl#apply()

public void apply() {
    
	final long startTime = System.currentTimeMillis();
	//  Update data to memory  
	final MemoryCommitResult mcr = commitToMemory();
	final Runnable awaitCommit = new Runnable() {
    
		@Override        
				public void run() {
    
			try {
    
				//  Enter the waiting state  
				mcr.writtenToDiskLatch.await();
			}
			catch (InterruptedException ignored) {
    
			}
		}
	};
	QueuedWork.addFinisher(awaitCommit);
	Runnable postWriteRunnable = new Runnable() {
    
		@Override        
		        public void run() {
    
			awaitCommit.run();
			QueuedWork.removeFinisher(awaitCommit);
		}
	};
	//  Write data to file asynchronously  
	SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
	notifyListeners(mcr);
}

performance optimization

IO bottleneck

IO The bottleneck is caused by SP The biggest reason for poor performance , solve IO bottleneck ,80% The performance problem is solved .

SP Of IO Bottlenecks include Read data to memory And Data written to disk Two parts .

1. There are two scenarios to trigger when reading data into memory :

  • SP When the file is not loaded into memory , call getSharedPreferences Method initializes the file and reads it into memory .

  • Version below Android-H Or use MULTI_PROCESS When the tag , Every time you call getSharedPreference Method will be read in .

Optimize

What we can optimize is b 了 , Too much data is loaded into memory every time, which will affect the efficiency , but H The following versions are already very low , Basically negligible . about MULTI_PROCESS, May adopt ContentProvider And so on , More efficient , And can avoid SP Data loss .

2. There are also two scenarios for writing data to disk :

  • Editor Of commit Method , Write to disk synchronously every time .

  • Editor Of apply Method , Write to the disk in the single thread pool every time , Asynchronous write .

Optimize

commit and apply The difference between synchronous writing and asynchronous writing , And whether the return value is required . Without the need to return a value , Use apply This method can greatly improve the performance . meanwhile , Multiple write operations can be combined into one commit/apply, Combining multiple write operations can also improve IO performance .

Poor lock performance

  1. SP Of get operation , Will lock SharedPreferences object , Mutually exclusive other operations .

  2. SP Of put operation ,edit() And commitToMemory Will lock SharedPreferences object ,put Operation will lock Editor object , Writing to the disk will lock a write lock .

Optimize

Because of the lock ,SP When the operation is concurrent , Time consuming will increase . Reduce lock time , Is an optimization point . Because the locks of read and write operations are SP Of instance objects , Split the data into different sp In file , This is a direct solution to reduce lock time . Reduce the frequency of single file access , Multi file sharing access , To reduce lock time .

Optimization summary

  • It is strongly recommended not to be in sp It stores very large key/value, It helps to reduce the number of jams /anr;
  • Please do not use high frequency apply, Submit as many batches as possible ;commit Operate directly on the main thread , Pay more attention to ;
  • Do not use MODEMULTIPROCESS;
  • High frequency write operation key With high frequency read operation key You can split the file properly , Due to the reduction of synchronous lock competition ;
  • Don't do it right away getSharedPreferences().edit(), It should be done in two major steps , Other code can be executed in the middle ;
  • Don't repeat it many times in a row edit(), You should get it once edit(), And then do it many times putxxx(), Reduce memory fluctuations ; We often see that people like packaging methods , The result is this ;
  • Every time commit A file that will update all the data when , So the whole file should not be too large , Affect overall performance .

copyright notice
author[Manon Xiaofeng],Please bring the original link to reprint, thank you.
https://en.cdmana.com/2022/01/202201270011338539.html

Random recommended