This is the first day of my participation in Gwen Challenge
1. ContextImpl role
SharedPreferences is an interface implemented by the SharedPreferences class. In The Android source code, this class is a hidden class, so specific objects need to be retrieved through the Context. The Application, Service, and Activity all inherit indirectly from the Context, passing the operations to the ContextImpl object through the decorator pattern. ContextImpl provides the getSharedPreferences method to get a SharedPreferences object. Now that we know that the SharedPreferences object is retrieved via ContextImpl, how do we ensure that the retrieved objects are the same in the App? In each App, the number of ContextImpl is the number of activities + the number of Services + the number of applications. To ensure the uniqueness of SharedPreferencesd, The SharedPreferences object is stored through a static collection in ContextImpl.
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
Copy the code
The SharedPreferences of the application are broken down by package name in ArrayMap. The specific SharedPreferences object is then obtained from the file address of SharedPreferences. The storage path is as follows:
/data/user/0/com.mdy.sp/shared_prefs/${name}.xml
Copy the code
2. Data Load and GET
The concrete implementation of SharedPreferences is handed over to the SharedPreferencesImpl class. When the SharedPreferencesImpl object is created, the startLoadFromDisk method is called to load the disk data into memory, and internally a separate thread is created to perform the loadFromDisk operation.
private void loadFromDisk(a) {
synchronized (mLock) {
// mLoaded = true
if (mLoaded) {
return;
}
//mBackupFile Indicates the backup file. If the backup file exists, delete the source file and replace it
if(mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); }}// Read the disk XML file by IO and parse it into map collection
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;
}
// If the parse succeeds, set mLoaded to true and assign the map reference address to mMap
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
try {
if (thrown == null) {
if(map ! =null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = newHashMap<>(); }}}catch (Throwable t) {
mThrowable = t;
} finally {
Wake up the main threadmLock.notifyAll(); }}}Copy the code
The loadFromDisk method loads and parses the data, and finally assigns the reference address to the mMap of the SharedPreferencesImpl. When you call the get family of methods to get cached values, you actually get cached values from mMap.
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
returnv ! =null? v : defValue; }}Copy the code
The get methods call awaitLoadedLocked to determine whether the disk is finished reading, based on the Boolean value of mLoaded (true), or call mlock.wait () to suspend the main thread. This corresponds to the notifyAll method called at the end of the loadFromDisk method above, which wakes up the main thread when the disk reads.
3. Data caching
Caching of data in SharedPreferences is implemented through Editor’s implementation class, EditorImpl. Cache the data using the corresponding GET method and call commit or apply.
3.1 Memory Synchronization
Before data is stored to disk in SharedPreferences, the commitToMemory method is first called to synchronize it to memory.
private MemoryCommitResult commitToMemory(a) {
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);
}
// The mMap reference is assigned to mapToWriteToDisk
mapToWriteToDisk = mMap;
/ / mDiskWritesInFlight value + 1
mDiskWritesInFlight++;
synchronized (mEditorLock) {
boolean changesMade = false;
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// Value belongs to Editor or is null
if (v == this || v == null) {
if(! mapToWriteToDisk.containsKey(k)) {continue;
}
mapToWriteToDisk.remove(k);
} else {
// If there is a key, determine whether to add the key according to whether the value is consistent
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if(existingValue ! =null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
}
/ / mCurrentMemoryStateGeneration value + 1
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
// The memoryStateGeneration value is passed to the MemoryCommitResult and is used to determine whether a disk is being written while the disk is being writtenmemoryStateGeneration = mCurrentMemoryStateGeneration; }}return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
Copy the code
The commitToMemory method is relatively simple. It adds the new data to the mMap collection and generates a MemoryCommitResult object, which is later written to disk using the writeToFile method.
3.2 commit to submit
The exact difference between commit and apply is that commit is a synchronous commit and apply is a single thread. What’s going on, take a look at the source code:
public boolean commit(a) {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
Copy the code
The commitToMemory method writes to memory and returns an MCR object. Called after enqueueDiskWrite method is written to disk, and then call the MCR. WrittenToDiskLatch. Await () method waits can write, finally returns the MCR. WriteToDiskResult. Note here that the postWriteRunnable argument passed in when the enqueueDiskWrite method is called is a NULL object.
3.3 apply to submit
The Apply method first calls commitToMemory to write data to memory, returning an MCR object. A postWriteRunnable object is then constructed and passed in when enqueueDiskWrite is called. Commit passes postWriteRunnable as a NULL object, while Apply passes a concrete Runnable object.
3.4 enqueueDiskWrite
The enqueueDiskWrite method distinguishes commit from apply based on whether the postWriteRunnable parameter passed is null.
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
// commit Commit postWriteRunnable==null
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run(a) {
synchronized (mWritingToDiskLock) {
// The specific write operation
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
MDiskWritesInFlight Value -1
mDiskWritesInFlight--;
}
if(postWriteRunnable ! =null) { postWriteRunnable.run(); }}};// isFromSyncCommit==true Indicates that the commit is committed
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
/ / wasEmpty mDiskWritesInFlight = = 1 = = true said
if (wasEmpty) {
writeToDiskRunnable.run();
return; }}// wasEmpty == falseQueuedWork.queue(writeToDiskRunnable, ! isFromSyncCommit); }Copy the code
If isFromSyncCommit is true, the commit is committed. The value of mDiskWritesInFlight is 1 to determine whether disk write operations are performed. This value is +1 when the commitToMemory method was previously called to write to memory. If a disk is being written, the value of mDiskWritesInFlight must be greater than 1. Then the commit task is added to the Queue of QueuedWork. After the disk write operation is complete, mDiskWritesInFlight-1 will be inflight, so we can draw a conclusion: The commit method commits data based on whether a disk write is being performed. If not, the write is performed on the main thread, and if so, the task is added to the QueuedWork and then executed on the HandlerThread.
When isFromSyncCommit is false, apply commits. Tasks submitted by the Apply method are added directly to QueuedWork. Note that quene’s last parameter, according to! IsFromSyncCommit.
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
// The task is added to sWork
sWork.add(work);
if (shouldDelay && sCanDelay) {
//apply call delay method, 100ms
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
/ / the commit callhandler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); }}}Copy the code
The queue handler gets the Looper object from the HandlerThread. While apply commits are deferred, commit commits immediately send a Message to The handleMessage method of QueuedWorkHandler, which eventually leads to the processPendingWork method.
private static void processPendingWork(a) {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
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);
}
// Execute the tasks in the queue
if (work.size() > 0) {
for(Runnable w : work) { w.run(); }}}}Copy the code
The processPendingWork method is used to perform the added task in sWork, which is to execute the writeToDiskRunnable object we passed and eventually go to the internal writeToFile method to write to disk.
Source code analysis to here, found that SharedPreferences seems to be no big problem, so what is the problem? In the following:
public static void waitToFinish(a) {
processPendingWork();
}
Copy the code
QueuedWork knows that this method is called specifically in ActivityThread after Acitivity executes OnPause, BroadcastReceiver onReceive. The processPendingWork method is called internally, but note that this is done on the main thread, and the disk write is performed internally, which results in ANR if waitToFinish times out.
4. To summarize
- If no results are required, submit using Apply. The XML file for each SharedPreferences should be as small as possible so that disk writing is faster.
- SharedPreferences supports multithreading (see synchronized), but not multiprocessing.
5. How does SharedPreferences support multiple processes?
First, make clear the following two points:
- SharedPreferences does not support multiple processes and cannot synchronize data with multiple processes.
- Set up the
Mode = context.mode_multi_process for SharedPreferences
At the right time. And every time I callgetSharedPreferences
Method is calledstartLoadFromDisk
The method loads data from disk to memory. The disadvantage of this method is that it still cannot synchronize data, and this mode has been deprecated.
It is not recommended to use SharedPreferences for inter-process communication, but what if you do? So we can use ContentProvider to do this. When ContentProvider is implemented, internal data storage is implemented by SharedPreferences, and the CRUD method of ContentProvider is used to operate the SharedPreferences method. Other processes use URIs to access SharedPreferences and perform CRUD of data. However, using the SharedPreferences object in other processes may require us to encapsulate one more layer to achieve an experience similar to normal SharedPreferences.