1. List of questions
- Initialization of SharedPreferences
- How is SharedPreferences initialized?
- Does initialization block the main thread? If so, how is the blockage caused?
- SharedPreferences Read and write operations
- Why are read and write operations for SharedPreferences thread-safe?
- Must Commit be performed by the current thread? If not, how is the synchronization implemented?
- Doesn’t Apply block the main thread if it writes to disk on the child thread?
SPImpl SharedPreferences SPImpl SharedPreferences 2) All the following analyses exclude the MODE_MULTI_PROCESS pattern
2. Initialize SharedPreferences
2.1 How is SharedPreferences initialized?
Whether we use getSharedPreferences(fileName,mode) in an Activity or a Service to get a SharedPreferences object, we call the following methods of the ContextImpl class: public SharedPreferences getSharedPreferences(String name, int mode) {*}
So let’s analyze the initialization process from the ContextImpl class.
First let’s look at the code related to SharedPreferences in the ComtextImpl class:
--> ContextImpl.java /** * Map from package name, to preference name, * * Since only one Contextimpl. class object exists for a process, all sharedPreferences within the same process are stored in the static list. * 1) String: packageName * 2) String: SharedPreferences file name * 3) SharedPreferenceImpl: Private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs; @Override public SharedPreferences getSharedPreferences(String name, int mode) { SharedPreferencesImpl sp; synchronized (ContextImpl.class) {if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if(packagePrefs == null) { packagePrefs = new ArrayMap<String, SharedPreferencesImpl>(); sSharedPrefs.put(packageName, packagePrefs); }... sp = packagePrefs.get(name);if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
returnsp; }}...return sp;
}
Copy the code
In the above code, the ContextImpl class defines a static member variable, sSharedPrefs, of type ArrayMap, which holds the SharedPreferences object loaded into memory. If not, a new SP object is created. When creating this new object, the disk file is read in the child thread and saved in the form of a Map in the newly created SP object. Let’s take a look at a few points to note here:
First, there is only one ContextImpl Class object for the same process, so all SharedPreferences objects in the current process are stored in sSharedPrefs. SSharedPrefs is an ArrayMap object whose generic definition tells us that the SharedPreferences object is stored in memory in two dimensions: 1) package name and 2) file name.
In addition, because the ContextImpl class does not define a method to remove the SharedPreferences object from the sSharedPrefs, once it is loaded into memory, it exists until the process destroys it. In contrast, that is to say, once the SP object is loaded into memory, any subsequent use, is directly from memory, will not be read disk situation.
The process of initializing the SharedPreferences object is fairly straightforward, but there is one issue that needs attention, which we’ll examine in the next section.
2.2 Does initialization block the main thread? If so, how is the blockage caused?
In the previous section, we mentioned that the SP disk file is read in the child thread, so it should not cause the main thread to block, but what is the fact? Let’s first look at the code that reads the file at initialization,
// SharedPreferences is itself an interface implemented as the SharedPreferences class. // constructor SharedPreferencesImpl(File File, int mode) {... startLoadFromDisk(); }Copy the code
The code to read the disk file when creating the SP object is in the constructor of the SharedPreferencesImpl class. There is an important method called startLoadFromDisk(). Let’s look at this method in detail.
-->SharedPreferencesImpl.java
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
Copy the code
From the above method, it is true that the disk file is read by the child thread, so the SP object initialization process itself does not block the main thread. But does this really prevent blocking? Let’s look at the code that gets the specific preference value,
-->SharedPreferencesImpl.java
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
Copy the code
AwaitLoadedLocked (), what is this 👻, to block the current thread,
-->SharedPreferencesImpl.java
private void awaitLoadedLocked() {...while(! mLoaded) { try {wait(a); } catch (InterruptedException unused) { } } }Copy the code
If mLoaded==false wait(), what is mLoaded?
private void loadFromDiskLocked() {... Read disk file code (omitted)... mLoaded =true; . notifyAll(); }Copy the code
As can be seen from the above code, mLoaded is set to true only after the child thread has finished loading data from disk. Therefore, although reading data from disk is done in the child thread and does not block other threads, if a specific preference value is obtained before the file is finished reading, The thread is blocked until the child thread finishes loading the file. So, if you get a preference on the main thread, you might block the main thread.
3. SharedPreferences Read and write operations
After SharedPreferences are initialized, all read operations are performed in memory, and write operations are divided into memory operations and disk operations. The following is a simple analysis of read and write operations with three questions as clues.
3.1 Why are read and write operations of SharedPreferences thread-safe?
SP reads are key-value pairs from the SharedPreferencesImpl member mMap, and writes, either through the Commit () or apply() methods, commit the modified data to mMap in the current thread. It then continues writing to disk on the current thread or another thread. Let’s look at the code that reads and writes to memory,
-->SharedPreferencesImpl.java
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
Copy the code
-->SharedPreferencesImpl$EditorImpl.java
public boolean commit() { MemoryCommitResult mcr = commitToMemory(); . Commit disk write} public voidapply() { final MemoryCommitResult mcr = commitToMemory(); . Write to disk} private MemoryCommitResult for ApplycommitToMemory() {... Synchronized (SharedPreferencesImpl. This will be a new data save people mMap} {... }Copy the code
As you can see from the above code, SP adds the same lock to mMap’s read and write operations, so it is indeed thread-safe to operate on memory. Considering that the life cycle of SP objects is the same as that of processes, once loaded into memory, there is no need to read disk files, so as long as the state in memory is consistent, the consistency of read and write can be guaranteed. This consistency also ensures that SP reads are thread-safe. As for writing to disk, take your time. No one will read the files on disk anyway.
3.2 Must the Commit operation be performed by the current thread? If not, how is the synchronization achieved?
Commit () is performed in two steps. The first step is to insert data into mMap through commitToMemory(), which updates the data in memory, and the second step is to write the mMap to a disk file through enqueueDiskWrite(MCR, NULL). The commit() method is indeed a synchronous operation from the perspective of the calling thread, that is, it blocks the current thread. But there are a few subtleties to this method that need to be analyzed. Let’s look at the code.
--> SharedPreferencesImpl$EditorImpl.java
public boolean commit() {// Save data to mMap on the current thread. MemoryCommitResult MCR = commitToMemory(); SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null); Try {// If you are writing in singleThreadPool, pause the main thread with await() until the write is complete. // Commit synchronization is done here. mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) {return false; } /* * Timing of call-back: * 1. Note that the call-back is made when both memory and disk operations end */ notifyListeners(MCR);return mcr.writeToDiskResult;
}
Copy the code
The first is commitToMemory(), which is nothing more than updating new data to mMap. Then we look at the enqueueDiskWrite(MCR, NULL) method, which is responsible for writing data to disk files, where the magic happens.
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() { synchronized (mWritingToDiskLock) { writeToFile(mcr); } synchronized (SharedPreferencesImpl.this) { mDiskWritesInFlight--; }... }}; final boolean isFromSyncCommit = (postWriteRunnable == null);if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
Copy the code
The commit() method is executed either on the current thread or in the thread pool of QueueWork. QueueWork is a QueueWork thread.
if(isFromSyncCommit) {/* * If editor.mit () is called, writeToFile() will be executed directly in the current thread if there is no other writeToDisk task to complete * before this commit. However, if there are other writeToDisk tasks * that have not been completed before this commit, then even the commit needs to be executed in a child thread. * * So the commit may be in the current thread, or it may be in a child thread. If the current thread is the main thread, it is possible to perform IO operations on the main thread. * * The purpose of this method is that if you do not apply data to a thread before committing it, it is possible that * apply data will be written to disk after committing, and the data on disk will be incorrect and inconsistent with the data in memory. * * How is commit synchronization guaranteed if a child thread is thrown? * There is a CountDownLatch in MCR, which waits by countdownlatch.await (). */ boolean wasEmpty =false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
Copy the code
IsFromSyncCommit ==true indicates that the commit() method is currently called. There is logic in this code to determine whether to write to disk from the current thread or from QueueWork’s thread pool. The key variable is mDiskWritesInFlight, which indicates how many pending writes are on the SP object. This variable is +1 on commitToMemory() and -1 on successful writes.
Writetodiskrunnable.run () when mDiskWritesInFlight == 1, writeToDiskRunnable.run() is called directly in the current thread, that is, write to disk in the current thread. When mDiskWritesInFlight > 1, it is inserted into QueueWork’s thread pool for execution.
3.3 Doesn’t the main thread block when Apply writes to disk on child threads?
AcitivtyThread calls handlePauseActivity() with one line of code:
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
Copy the code
As you can see from this code, it is possible to block the main thread even if you commit changes using Apply. However, after 4.0, there is no such limitation. Maybe Google also thinks that it affects the fluency too much. This needs to be confirmed.