当前位置:网站首页>SharedPreferences source code analysis
SharedPreferences source code analysis
2022-07-06 04:21:00 【jthou20121212】
SharedPreferences It is a persistent storage scheme for storing a small number of key value pairs provided by the system , Characteristic is API It is relatively simple and convenient to use , But it also has some defects. If it is not used properly, it may lead to jamming and even ANR also SharedPreferences Multi process is not supported
Have a look first SharedPreferences The acquisition process of
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
private ArrayMap<String, File> mSharedPrefsPaths;
// android.app.ContextImpl#getSharedPreferences(java.lang.String, int)
// SharedPreferences According to the incoming name Generate a corresponding xml file
// The data stored according to the key value pair is here xml In the document
public SharedPreferences getSharedPreferences(String name, int mode) {
// Code ellipsis ..
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
// name And file Mapping
file = mSharedPrefsPaths.get(name);
if (file == null) {
// Create a file by name , The file name is passed in name
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// See if there is
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
// Code ellipsis ..
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
// If used MODE_MULTI_PROCESS Mark every time you get SharedPreferences The file will be re read when the instance
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
// Package name and App All documents and SharedPreferences Mapping of interface instances
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
SharedPreferences It's an interface. What we get is its implementation class SharedPreferencesImpl The process is also relatively simple, first through the incoming name Get the file and create one through the file and the incoming operation mode of the file SharedPreferencesImpl And cache it , stay SharedPreferencesImpl In the construction method of, the data in the file will be read and mapped to a map in , and sSharedPrefsCache It is static and does not provide a release method, so App All data in the memory will always be in the memory after reading
SharedPreferencesImpl(File file, int mode) {
// Source file for storing data
mFile = file;
// Backup file
mBackupFile = makeBackupFile(file);
mMode = mode;
// Mark whether the read file operation is completed
mLoaded = false;
mMap = null;
mThrowable = null;
// Start a thread to read the file
startLoadFromDisk();
}
private void startLoadFromDisk() {
// The flag bit indicates whether the loading is complete
// The lock used is mLock
synchronized (mLock) {
// Set to false
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
// Every time the instance object is obtained, if the backup file exists, it indicates that an exception occurred when saving the data last time, and the file has been damaged
// Then delete the source file and use the backup file. If an exception occurs, only the last saved data will be lost
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
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);
// Read the file and save the data to map According to the key When getting data, it is from this map Instead of reading files every time
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;
try {
// If there is no exception in the process of reading the file
// The saved file information is used to compare whether the file has been updated since the last time the file was read
if (thrown == null) {
if (map != null) {
mMap = map;
// File update time
mStatTimestamp = stat.st_mtim;
// file size
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 {
// Notify all waiting threads of the end of file reading
mLock.notifyAll();
}
}
}
Creating SharedPreferences Object will open a thread to read the file , The content will be saved to a map According to key The data is obtained from this map Instead of reading files every time
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
while (!mLoaded) {
try {
// If the reading of the file is not finished yet, wait for the completion
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
SharedPreferences When the object is created, it will start a sub thread to read the file data to map in , Reading files is a time-consuming operation , If the main thread does not finish reading the file, it will fetch the data , The main thread will block and wait for the file reading thread to end . So a little trick can save waiting time , Since the read operation will wait for the end of the file reading process and the file reading is performed on another thread , So you can read the file first, then do something else, and then get the data
// First, let sp Go to another thread to load SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE); // Do a bunch of other things setContentView(testSpJson); // ... // OK, At this time, it is estimated that it has been loaded , Even if it's not over , We also did something when we should have waited ! String testValue = sp.getString("testKey", null);
This part of the code comes from Please don't abuse SharedPreference
The read operation is read directly map The write operation is more complex than the read operation , Need to go through first edit Method to get Editor object , It is also an interface implementation class EditorImpl Every time you call edit Method creates a EditorImpl object , Multiple write operations will first record the operation and call commit/apply Method and submit again , So try to write more than once before commit/apply Avoid writing every time commit/apply therefore SPUtils This kind of tool class is relatively simple, but it is actually not recommended
public final class EditorImpl implements Editor {
// Operations on data will be saved here
private final Object mEditorLock = new Object();
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
// The removal operation is quite special, which will put value Set to judge when writing to the disk
mModified.put(key, this);
return this;
}
}
@Override
public Editor clear() {
synchronized (mEditorLock) {
// Tag called clear Method
mClear = true;
return this;
}
}
}
about putXxx All write operations are operational mModified about remove The operation is to put value Set to this about clear The operation is to set the flag bit mClear by true And then call commit/apply Method to write the changes to the file , The difference lies in commit It's synchronous apply It's asynchronous
public boolean commit() {
// Commit changes to memory
MemoryCommitResult mcr = commitToMemory();
// Write memory changes to disk
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
// Wait for the disk write to complete
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
// Callback monitor
notifyListeners(mcr);
// Return the write result
return mcr.writeToDiskResult;
}
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// If there are other threads writing, copy a copy of the data
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
// Number of threads performing write operations
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
// If called clear Method
if (mClear) {
// Data is not empty, clear data
if (!mapToWriteToDisk.isEmpty()) {
// Indicates that the data has changed
changesMade = true;
mapToWriteToDisk.clear();
}
keysCleared = true;
// Reset mClear Mark
mClear = false;
}
// Traverse mModified Judge whether there is any modification
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// If value yes this That is the remove operation
// If you will value yes null Will perform remove operation
// So if you call putXxx Will be a key Corresponding value Set to null This key It will also be removed
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
// Only when the new value is different from the old value can it be replaced
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
// mModified If it is not empty, it is considered that the data has changed
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
// Empty mModified
mModified.clear();
// If the data changes, add one to the data version number in memory
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
// Create a MemoryCommitResult Object, which is the result of writing data changes to memory
// memoryStateGeneration Indicates the version number of the current memory data
// keysCleared Indicates whether the emptying operation has been performed
// keysModified It's all value Something has changed key
// listeners It's listening.
// mapToWriteToDisk Is the data in memory after a series of change operations
// stay MemoryCommitResult The constructor of will initialize an initial value of 1 Of CountDownLatch object
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
// commit/apply Will call this method
// commit Invocation time postWriteRunnable by null
// apply Invocation time postWriteRunnable Not empty
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// Indicates from commit still apply Call to
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Write data in memory to a file
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
// Write data in memory to disk
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// Write operation is completed, and the number of operation threads is reduced by one
mDiskWritesInFlight--;
}
// If postWriteRunnable Not for null perform postWriteRunnable
// from commit Call in postWriteRunnable by null
// from apply Call in postWriteRunnable Not for null
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// If it's from commmit Call to
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
// If only the current thread is performing write operations
wasEmpty = mDiskWritesInFlight == 1;
}
// Then write the file directly in the current thread
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// If it is finished after the current thread executes file writing
// If it is apply Method or commit Method, but if multiple threads are writing files, they are added to the queue to execute
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
public static void queue(Runnable work, boolean shouldDelay) {
// QueuedWorkHandler object , its looper Is in HandlerThread Of run Created in the , So the message sent to it will be executed in this thread
Handler handler = getHandler();
synchronized (sLock) {
// Join the queue
sWork.add(work);
// commit There is no need to delay entering apply Entering requires a delay
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
// Send a message to handler
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
// QueuedWorkHandler received MSG_RUN The message will be executed immediately after processPendingWork Method
private static void processPendingWork() {
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
// Execute all... In sequence runnable
for (Runnable w : work) {
w.run();
}
}
}
}
stay processPendingWork Method will execute the... In the queue in turn runnable That's what's coming in from above writeToDiskRunnable In this method, the real operation of writing to the disk will be performed writeToFile
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
// Code ellipsis ..
boolean fileExists = mFile.exists();
// Source file exists
if (fileExists) {
boolean needsWrite = false;
// Compare the disk data version with the memory data version. Only when the disk data version is lower than the memory data version, you need to write
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
// If it is commit You need to write
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
// If it is apply Comparing the current memory data version with the memory data version to be written to the file requires writing
// Inconsistency indicates that there are new data changes waiting for the next write
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
// Write only when the data really changes. Avoid meaningless writing
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
// If the backup file does not exist
if (!backupFileExists) {
// Name the source file backup file , Return if failed
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
// If there is a backup file, delete the source file because a new file will be created immediately to write new data
mFile.delete();
}
}
try {
// Omit writing data in memory to source file code ..
// Write file successfully delete backup file
mBackupFile.delete();
if (DEBUG) {
deleteTime = System.currentTimeMillis();
}
// Record the disk data version number
mDiskStateGeneration = mcr.memoryStateGeneration;
// Write successfully
mcr.setDiskWriteResult(true, true);
// Code ellipsis ..
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// If the write is successful, the above will return the write failure to delete the source file
// If an exception occurs in the next construction SharedPreferencesImpl Object will rename the backup file to the source file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
Whether the write succeeds or fails, it will call setDiskWriteResult Method
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
// Release cause call writtenToDiskLatch.await Blocking waiting threads
writtenToDiskLatch.countDown();
}
call setDiskWriteResult Later, because of the call commit And the blocked thread is released , So let's see apply
public void apply() {
// commit We have analyzed
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
// take awaitCommit Join the queue awaitCommit One thing is to wait for the file to be written
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
// perform awaitCommit Wait for the file to be written
awaitCommit.run();
// remove awaitCommit
QueuedWork.removeFinisher(awaitCommit);
}
};
// And commit Same call enqueueDiskWrite But the second parameter passed in here is not empty
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Callback listening before writing to disk
notifyListeners(mcr);
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Code ellipsis ..
// Join the queue at HandlerThread In turn
// writeToDiskRunnable After writing to the disk in, it will be executed immediately postWriteRunnable
// and postWriteRunnable Just wait for the disk write to complete. Now it is finished, so it is directly removed from the queue
QueuedWork.queue(writeToDiskRunnable, true);
}
stay Activity 、service The lifecycle methods of these components will call android.app.QueuedWork#waitToFinish Wait for all write operations to disk to complete , If there are multiple write operations or the file is relatively large, the waiting time will be relatively long, and a serious situation may be caused ANR
public static void waitToFinish() {
// Code ellipsis ..
try {
while (true) {
Runnable finisher;
// Take out each task in turn to perform
synchronized (sLock) {
finisher = sFinishers.poll();
}
// All tasks are completed and exit
if (finisher == null) {
break;
}
// Every task is waiting for the completion of the disk write operation, so the completion of the execution means that all the disk write operations are completed
finisher.run();
}
} finally {
sCanDelay = true;
}
// Code ellipsis ..
}
For data changes, first pass edit How to get one EditorImpl object , The change operation is recorded in a map in , Emptying is using a boolean Mark , Calling commit/apply Then judge whether the emptying method is called. If yes, empty the data and then traverse map Record all changed data into memory , Now the latest data is in memory . about commit Method will determine whether there is only one thread changing the file. If there is only one thread, the disk write operation will be performed in the current thread , If there are multiple threads changing the file, then HandlerThread Perform disk write operations in , No matter which thread is executing the disk write, it will wait for the disk write to complete, so it is a synchronous write , Because it's in commit Method, wait for the disk write to complete . about apply The method is called awaitCommit Of Runnable Wait for the disk write to complete , And it will be added to a queue before it is executed , Remove after execution , And this awaitCommit The implementation and removal of is in another called postWriteRunnable Of Runnable Implemented in , This postWriteRunnable Another one is called writeToDiskRunnable Medium Runnable Implemented in , writeToDiskRunnable Is in HandlerThread Implemented in , It will wait for the disk write to complete before executing postWriteRunnable Will perform again awaitCommit and awaitCommit Only one thing I did was block the disk before it was written, so in apply In the execution logic of the method, after the disk is written runnable It's all finished , And in the Activity Service Some components will wait in the life cycle method awaitCommit The added queue waits for all disk write operations to complete , So although apply It is asynchronous operation, but it can also be done ANR, In a nutshell commit Is waiting for the write operation to complete in the current thread apply The asynchronous thread waits for the write operation to complete and apply Will wait for the write operation to complete runnable Join a queue , stay Activity、Service The component life cycle method will execute these queued waiting write operations in turn ruannble Execution completed . Finally, I will summarize some points :
- Every time I read / Write operations are all full and the data will always be in memory and will not be released
- Multi process is not supported MODE_MULTI_PROCESS The function of is to get SharedPreferences When the object is instantiated, it is judged that the file has changed and the data is re read to the memory
- In the first acquisition SharedPreferences Object will read the contents of the file into memory. If you read the data before this action is completed, it will block waiting for the file to be read, which may cause a jam
- stay Activity Service In the life cycle method of some components, waiting for all disk write operations to complete may cause ANR
- adopt registerOnSharedPreferenceChangeListener Method registered listener for commit Method will call back after the disk is written, and apply Methods will call back after the data is written to memory, and they are all called back in the main thread
- By successfully writing the backup source file before writing the file every time, deleting the backup file when writing fails, deleting the source file and naming the backup file as the source file, we can ensure that even if the file is damaged and the data is lost, only the last saved data will be lost
Reference and thanks
Please don't abuse SharedPreference
Android Source code analysis SharedPreferences
analyse SharedPreference apply Caused by the ANR problem
Android Don't abuse SharedPreferences( On )
Android Don't abuse SharedPreferences( Next )
Article to read SharedPreferences And a little thought
边栏推荐
- Comprehensive ability evaluation system
- P3500 [poi2010]tes intelligence test (two points & offline)
- Solution to the problem that the root account of MySQL database cannot be logged in remotely
- 1008 circular right shift of array elements (20 points)
- Query the number and size of records in each table in MySQL database
- pd. to_ numeric
- 绑定在游戏对象上的脚本的执行顺序
- P2648 make money
- Jd.com 2: how to prevent oversold in the deduction process of commodity inventory?
- [leetcode question brushing day 33] 1189 The maximum number of "balloons", 201. The number range is bitwise AND
猜你喜欢
Certbot failed to update certificate solution
CADD course learning (8) -- virtual screening of Compound Library
How to realize automatic playback of H5 video
Solution of storage bar code management system in food industry
The most detailed and comprehensive update content and all functions of guitar pro 8.0
CADD课程学习(7)-- 模拟靶点和小分子相互作用 (柔性对接 AutoDock)
Understanding of processes, threads, coroutines, synchronization, asynchrony, blocking, non blocking, concurrency, parallelism, and serialization
coreldraw2022新版本新功能介绍cdr2022
CertBot 更新证书失败解决
How does technology have the ability to solve problems perfectly
随机推荐
Recommendation | recommendation of 9 psychotherapy books
[network] channel attention network and spatial attention network
User datagram protocol UDP
How does technology have the ability to solve problems perfectly
Yyds dry goods inventory hcie security Day11: preliminary study of firewall dual machine hot standby and vgmp concepts
80% of the diseases are caused by bad living habits. There are eight common bad habits, which are both physical and mental
[tomato assistant installation]
BOM - location, history, pop-up box, timing
The global and Chinese market of negative pressure wound therapy unit (npwtu) 2022-2028: Research Report on technology, participants, trends, market size and share
2/13 qaq~~ greed + binary prefix sum + number theory (find the greatest common factor of multiple numbers)
电脑钉钉怎么调整声音
Comprehensive ability evaluation system
How to realize automatic playback of H5 video
Recommendation system (IX) PNN model (product based neural networks)
729. My schedule I (set or dynamic open point segment tree)
Unity中几个重要类
HotSpot VM
R note prophet
Lombok原理和同时使⽤@Data和@Builder 的坑
题解:《单词覆盖还原》、《最长连号》、《小玉买文具》、《小玉家的电费》