# SharedPreferences

{% hint style="info" %}
公众号已发表文章：

[SharedPreferences 源码解析(上） ](https://mp.weixin.qq.com/s/RNWcE9TZl2cfEcGjS-GQAw)

[SharedPreferences 源码解析(下）](https://mp.weixin.qq.com/s/AE7QcqYiwmrYCMiOJMSsBA)
{% endhint %}

## getSharedPreferences

通过 Context 的 getSharedPreferences 方法获取 Sp 实例，其具体的实现在 ContextImpl 类中，方法代码如下：

```
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // 处理 name 为 null 的情况
    // At least one application in the world actually passes in a null
    // name.  This happened to work because when we generated the file name
    // we would stringify it to "null.xml".  Nice.
    if (mPackageInfo.getApplicationInfo().targetSdkVersion <
            Build.VERSION_CODES.KITKAT) {
        if (name == null) {
            name = "null";
        }
    }

    File file;
    synchronized (ContextImpl.class) {
        if (mSharedPrefsPaths == null) {
            //创建缓存的 map
            mSharedPrefsPaths = new ArrayMap<>();
        }
        //先从缓存中获取 File 
        file = mSharedPrefsPaths.get(name);
        if (file == null) {
            //缓存中没有，先生成file
            file = getSharedPreferencesPath(name);
            //加入缓存
            mSharedPrefsPaths.put(name, file);
        }
    }
    //通过文件生成 SharedPreferences
    return getSharedPreferences(file, mode);
}
```

逻辑就是根据名称找到或者生成对应的文件，生成文件时调用了 `getSharedPreferencesPath`，该方法代码如下：

```
@Override
public File getSharedPreferencesPath(String name) {
    return makeFilename(getPreferencesDir(), name + ".xml");
}
```

调用了makeFileName方法，第一个参数是文件目录，通过getPreferencesDir方法获取；第二个是文件名，通过这里可以看出存储sp使用的xml文件。

getPreferencesDir 代码如下，其实就是获取 data 下的 "shared\_prefs"目录：

```
private File getPreferencesDir() {
    synchronized (mSync) {
        if (mPreferencesDir == null) {
            //应用的data 目录下 shared_prefs 文件夹
            mPreferencesDir = new File(getDataDir(), "shared_prefs");
        }
        return ensurePrivateDirExists(mPreferencesDir);
    }
}
```

ensurePrivateDirExists 方法主要目的是在目录不存在时进行创建，源码如下，细节就不再分析了。

```
private static File ensurePrivateDirExists(File file) {
    return ensurePrivateDirExists(file, 0771, -1, null);
}


private static File ensurePrivateDirExists(File file, int mode, int gid, String xattr) {
    if (!file.exists()) {
        final String path = file.getAbsolutePath();
        try {
            Os.mkdir(path, mode);
            Os.chmod(path, mode);
            if (gid != -1) {
                Os.chown(path, -1, gid);
            }
        } catch (ErrnoException e) {
            if (e.errno == OsConstants.EEXIST) {
                // We must have raced with someone; that's okay
            } else {
                Log.w(TAG, "Failed to ensure " + file + ": " + e.getMessage());
            }
        }

        if (xattr != null) {
            try {
                final StructStat stat = Os.stat(file.getAbsolutePath());
                final byte[] value = new byte[8];
                Memory.pokeLong(value, 0, stat.st_ino, ByteOrder.nativeOrder());
                Os.setxattr(file.getParentFile().getAbsolutePath(), xattr, value, 0);
            } catch (ErrnoException e) {
                Log.w(TAG, "Failed to update " + xattr + ": " + e.getMessage());
            }
        }
    }
    return file;
}
```

makeFilename 方法实现如下：

```
private File makeFilename(File base, String name) {
    //文件名包含分隔符时抛出异常
    if (name.indexOf(File.separatorChar) < 0) {
        final File res = new File(base, name);
           return res;
    }
    throw new IllegalArgumentException(
            "File " + name + " contains a path separator");
}
```

可以看出主要逻辑就是生成 File 对象。

以上是生成 file 对象的过程。

## 创建 SharedPreferences

有了文件后，就可以创建 Sp 实例了，这个过程通过 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");
                }
            }
            //创建 SharedPreferences 实例
            sp = new SharedPreferencesImpl(file, mode);
            // 放入缓存
            cache.put(file, sp);
            return sp;
        }
    }
    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;
}
```

方法会先存缓存中获取，如果缓存没有，那么会执行创建，并把新创建的sp实例放入缓存。

创建前先进行了一些权限检查，checkMode 用于检查 sp 的模式，MODE\_WORLD\_READABLE 和 MODE\_WORLD\_WRITEABLE 在7.0及以后会抛出异常。

```
private void checkMode(int mode) {
    if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
        if ((mode & MODE_WORLD_READABLE) != 0) {
            throw new SecurityException("MODE_WORLD_READABLE no longer supported");
        }
        if ((mode & MODE_WORLD_WRITEABLE) != 0) {
            throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
        }
    }
}
```

创建 sp 时直接实例化了`SharedPreferencesImpl，`它是 SharePreferences 的实现类，构造方法的源码如下：

```
SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        mThrowable = null;
        //从文件中读取
        startLoadFromDisk();
}
```

在构造方法中，调用了 startLoadFromDisk 方法，从名字可以看出来，这个方法的任务主要是从文件中读取内容，其源码如下：

```
private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    //开启一个线程读取文件
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}
```

在该方法中，先将读取状态标识置为false，然后开启了一个线程执行文件读取任务，文件读取任务由 loadFromDisk 方法定义，代码为：

```
private void loadFromDisk() {
    synchronized (mLock) {
        //已经读取成功，直接返回
        if (mLoaded) {
            return;
        }
        //如果有备份文件，优先从备份文件恢复
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    Map<String, Object> map = null;
    Throwable thrown = null;
    try {
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                //创建文件输入流
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                //将读取的文件解析成Map        
                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) {
        //读取完成后设置读取状态为true
        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 赋值
                    mMap = map;
                } 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();
        }
    }
}
```

主要逻辑是读取XML文件并将其解析为 Map 对象，然后将读取标识置为 true。sp创建后，就可以进行读取和写入了。

## 读取值

以 `getString` 方法为例，在 `SharedPreferencesImpl`中代码：

```
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}
```

在读取前，先调用了 awaitLoadedLocked 方法，在还没有完成文件读取时进行等待：

```
private final Object mLock = new Object();

private void awaitLoadedLocked() {
    while (!mLoaded) {
        try {
            //没有读取成功，先等待
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}
```

而前面 loadFromDisk 方法在读取成功后会调用 mLock.notifyAll 方法，从而通知读取操作可以继续。

值的读取就是从生成的 HashMap 中根据 key 获取对应的值。

## 写入值

相比读取，写入过程就复杂很多了。在写入时，需要先调用 `edit` 方法，获取一个 `Editor` 对象。

### 创建 Editor

edit 方法源码如下：

```
@Override
public Editor edit() {
    synchronized (mLock) {
        awaitLoadedLocked();
    }
    return new EditorImpl();
}
```

当 Sp 可用时，创建了一个 `EditorImpl` 对象并返回。`EditorImpl` 是`Editor`的实现类，同时是`SharedPreferencesImpl`的内部类。

`EditorImpl` 只有三个属性：

```
public final class EditorImpl implements Editor {
    //写锁
    private final Object mEditorLock = new Object();
    //新增或者发生变更的键值对
    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();
    //是否需要清空之前的值
    @GuardedBy("mEditorLock")
    private boolean mClear = false;
    
    //....
}
```

执行修改主要是一些重载的 put 方法，remove 方法用于移除一个键值对，clear 方法用于清空修改。

### putXxx

EditorImpl 中的对应的修改方法代码如下(put 方法以 putString 为例)：

```
@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) {
        mModified.put(key, this);
        return this;
    }
}

@Override
public Editor clear() {
    synchronized (mEditorLock) {
        mClear = true;
        return this;
    }
}
```

put 和 remove 操作的是 EditorImpl 的 mModified，而 clear 方法只是将清除标记置为 true。

另外这些方法都返回 Editor 对象，方便链式调用。

通过对 EditorImpl 操作后，所有要新增或发生修改的键值对都被记录在了 mModified 这个 map 中，而要使这些更改生效，就需要调用 `apply` 或者 `commit` 进行提交。

先来看下 apply 方法。

### EditorImpl#apply()

apply 方法源码如下，略去了一些 log 信息：

```
@Override
public void apply() {
    //先提交到内存
    final MemoryCommitResult mcr = commitToMemory();
    
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    //等待,确保写入被执行
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                
                }
           }
    };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // 写入内存后就可以通知监听者了
    notifyListeners(mcr);
}
```

apply 逻辑是：

1. 通过 `commitToMemory` 方法把修改提交至内存中
2. 通过 `enqueueDiskWrite` 将磁盘写入任务提交至任务队列
3. 通知监听者

> awaitCommit 可能在文件写入时被执行，也可能会在 QueuedWork 执行完所有任务后再执行。如果通过文件写入过程执行，会从QueuedWork的finisher中把它移除。

### EditorImpl#commitToMemory()

这个方法用于将在 Editor 上执行所有更改提交到内存的Sp中，并返回一个 MemoryCommitResult 对象提交信息，代码如下：

```
private MemoryCommitResult commitToMemory() {
    // 当前内存中sp的版本号
    long memoryStateGeneration;
    
    //所有发生修改的key，用于通知监听者
    List<String> keysModified = null;
    
    Set<OnSharedPreferenceChangeListener> listeners = null;
    
    //要写入磁盘的map
    Map<String, Object> mapToWriteToDisk;

    synchronized (SharedPreferencesImpl.this.mLock) {
        if (mDiskWritesInFlight > 0) {
            // 此时有写入任务正在执行，所以不能直接修改 mMap，而是克隆它
            mMap = new HashMap<String, Object>(mMap);
        }
        //mapToWriteToDisk 为所有键值对
        mapToWriteToDisk = mMap;
        
        //写入任务+1
        mDiskWritesInFlight++;

        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            //如果有监听者，则需要记录发生更改的 key
            keysModified = new ArrayList<String>();
            listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
        }

        synchronized (mEditorLock) {
            boolean changesMade = false;

            if (mClear) {
                //如果调用过 clear 先执行清空操作
                if (!mapToWriteToDisk.isEmpty()) {
                    changesMade = true;
                    mapToWriteToDisk.clear();
                }
                mClear = false;
            }
            //遍历通过 Editor 做出修改的键值对
            for (Map.Entry<String, Object> e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                //在 remove 方法中，移除一个key时，将它的值设为 EditorImpl.this
                //所以如果 v==this,就代表移除一个key
                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;
                        }
                    }
                    //将新增加或者放生变更的key和value放到即将写入的map中
                    mapToWriteToDisk.put(k, v);
                }

                changesMade = true;
                if (hasListeners) {
                    //记录真正发生变更的key
                    keysModified.add(k);
                }
            }

            mModified.clear();

            if (changesMade) {
                //将内存中的 Sp 版本号自增
                mCurrentMemoryStateGeneration++;
            }
            //记录当前内存中的版本
            memoryStateGeneration = mCurrentMemoryStateGeneration;
        }
    }
    //创建 MemoryCommitResult 对象
    return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
}
```

该方法的主要逻辑是，根据 Editor 的 mModified 中记录的发生修改的key，来更新内存中的 map 对应的 key。如果Sp有监听者，还需要记录发生修改的key，以便通知监听者。最后返回一个`MemoryCommitResult` 对象，它记录了当前内存中Sp版本号、发生变更的key、监听者和最要写入文件的map（即内存中Sp的所有键值对）。

需要注意的点：

1. 对于 Editor#clear() 方法，如果执行过，会先将所有键值对清空，然后写入 mModified 中记录的键值对
2. 内存中的Sp 有一个版本号标识，每次提交新的改动到内存后，该版本号会加1

该方法返回的是一个 `MemoryCommitResult` 对象，`MemoryCommitResult` 是 SharedPreferencesImpl 的静态内部类，它的定义如下：

```
// Return value from EditorImpl#commitToMemory()
private static class MemoryCommitResult {
    //标识当前内存的版本
    final long memoryStateGeneration;
    //发生修改的key集合
    @Nullable final List<String> keysModified;
    //sp 的监听者
    @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
    //要写入文件的map
    final Map<String, Object> mapToWriteToDisk;
    //lock
    final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

    @GuardedBy("mWritingToDiskLock")
    volatile boolean writeToDiskResult = false;
    
    boolean wasWritten = false;

    private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
            @Nullable Set<OnSharedPreferenceChangeListener> listeners,
            Map<String, Object> mapToWriteToDisk) {
        this.memoryStateGeneration = memoryStateGeneration;
        this.keysModified = keysModified;
        this.listeners = listeners;
        this.mapToWriteToDisk = mapToWriteToDisk;
    }

    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        //countDown
        writtenToDiskLatch.countDown();
    }
}
```

其中 setDiskWriteResult 方法用来设置文件写入结果，后面会讲到。

### SharedPreferencesImpl#enqueDiskWrite

将变更提交的内存后得到 MemoryCommitResult 对象后，调用了 enqueDiskWrite 进行文件写入，这个方法源码如下：

```
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //是否是 commit                          
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    //写入文件
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    //文件写入任务数减一
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    //执行写入后的任务
                    postWriteRunnable.run();
                }
            }
        };

    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            // mDiskWritesInFlight 在提交至内存时会自增，
            // 如果是 1 说明此时没有其他的任务要写入
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //在当前线程中直接执行写入
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
```

该方法中第二个参数是一个 Runnable ，用于在文件写入后执行。如果这个参数为null，那么就说明此次调用来自 commit() 方法，否则此次调用就来自 apply ()方法，在上面的 apply 方法中，我们也看到它确实传了一个 Runnable。

上面方法中的 writeToDiskRunnable 定义了文件写入的过程：**先调用 writeToFile 将内存的Sp写入文件，然后将 mDiskWritesInFlight 减一，最后在执行 postWriteRunnable。**

不过 writeToDiskRunnable  的执行时机却暗藏玄机。如果此调用来自 commit() 方法，并且目前只有这次写入需要执行，那么就会在当前线程执行这次文件写入(如果我们的调用来自主线程，就会直接在主线程中执行文件写入，这就可能导致性能问题)；其他情况都会通过 QueuedWork 把写入任务加入队列中，关于 QueuedWork 稍后再介绍，先来看看 writeToFile 方法如何执行文件写入的。

### SharedPreferencesImpl#writeToFile()

writeToFile 是执行文件写入的方法，代码如下，我省略的一些日志输出代码：

```
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    // 省略了一些代码

    boolean fileExists = mFile.exists();

    // Rename the current file so it may be used as a backup during the next read
    if (fileExists) {
        boolean needsWrite = false;

        // 只有文件版本小于内存版本时才写入
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                needsWrite = true;
            } else {
                synchronized (mLock) {
                    //对于 apply，没有必要每次都写入，而是只执行最新一次的提交对应的写入
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }

        if (!needsWrite) {
            //没必要写入文件，直接返回
            mcr.setDiskWriteResult(false, true);
            return;
        }

        boolean backupFileExists = mBackupFile.exists();

        if (!backupFileExists) {
            //备份文件不存在，将正式文件重命名为备份文件
            if (!mFile.renameTo(mBackupFile)) {
                //如果失败，停止写入
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            //备份文件存在，删除正式文件
            mFile.delete();
        }
    }//end if(fileExists)..

    try {
        //创建文件输出流（会自动创建文件）
        FileOutputStream str = createFileOutputStream(mFile);

        if (str == null) {
            mcr.setDiskWriteResult(false, false);
            return;
        }
        //写入数据
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);

        writeTime = System.currentTimeMillis();

        FileUtils.sync(str);

        fsyncTime = System.currentTimeMillis();

        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

        //写入成功，删除备份文件
        mBackupFile.delete();

        //更新文件版本号
        mDiskStateGeneration = mcr.memoryStateGeneration;

        //设置写入结果
        mcr.setDiskWriteResult(true, true);

        long fsyncDuration = fsyncTime - writeTime;
        mSyncTimes.add((int) fsyncDuration);
        mNumSync++;

        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }

    // 发生异常，清理未成功写入的文件
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    mcr.setDiskWriteResult(false, false);
}
```

在写入前，会先比较当前磁盘版本号与内存版本号，只有磁盘版本号小于内存版本号时，才会执行文件写入。

另外，对于 apply ，会将每次 mcr 的版本号与当前内存的最终版本号进行对比，只有相等时才会执行文件写入。这样一来，如果短时间内有多次 apply 文件写入请求，只有最后一次写入会被真正执行。

在执行写入前，会先创建一个备份文件，当写入过程中发生意外，下次读取时可以从备份文件中恢复。在从文件加载Sp 的 loadFromDisk 方法中就有如下代码：

```
synchronized (mLock) {
    if (mLoaded) {
        return;
    }
    //如果备份文件存在，则优先从备份文件中恢复
    if (mBackupFile.exists()) {
        mFile.delete();
        mBackupFile.renameTo(mFile);
    }
}
```

正式的文件写入过程主要分以下几步:

1. 创建文件输出流
2. 将 map 写入到 xml 文件
3. 删除备份文件&#x20;
4. 更新磁盘Sp对应版本号 mDiskStateGeneration
5. 设置写入结果

关于第五步，针对文件写入情况，会调用 MemoryCommitResult#setDiskWriteResult 方法设置结果，这个方法源码：

```
//lock
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

void setDiskWriteResult(boolean wasWritten, boolean result) {
    this.wasWritten = wasWritten;
    writeToDiskResult = result;
    //countDown
    writtenToDiskLatch.countDown();
}
```

设置完结果后，调用了 writtenToDiskLatch.countDown() 以通知正在等待的线程。&#x20;

以上就是 apply 的过程，主要分为两步：

1. 提交修改至内存，生成一个 MemoryCommitResult 对象mcr,这个 mcr 记录着当前内存中sp的版本号、所有键值对、发生修改的键等信息
2. 通过 QueuedWork 将文件写入任务入队，等待执行

在讲解 QueuedWork 之前，我们先对照着看一下 commit 方法。

### EditorImpl#commit()

```
public boolean commit() {
    //1.提交至内存
    MemoryCommitResult mcr = commitToMemory();
    //2.将文件写入任务入队列
    SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null);
    try {
        //3.等待文件写入结果
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
                
    }
    //4. 通知监听者
    notifyListeners(mcr);
    //5. 返回文件写入结果
    return mcr.writeToDiskResult;
}
```

commit 方法也包括提交内存和将文件写入任务入队，但后面还增加了等待文件写入完成的过程，因为commit 方法的返回值就是文件写入的结果。

另外，调用 [SharedPreferencesImpl#enqueDiskWrite](/kx-android/android/data/sharedpreference.md#sharedpreferencesimpl-enquediskwrite)方法时，第二个参数传的是 null,在分析该方法时我们也看到，该方法正是以这个参数是否为 null 来区分是commit 还是 apply 的。

为了方便阅读，再把该方法源码贴一遍：

```
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //是否是 commit                          
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    //写入文件
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    //文件写入任务数减一
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    //执行写入后的任务
                    postWriteRunnable.run();
                }
            }
        };

    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            // mDiskWritesInFlight 在提交至内存时会自增，
            // 如果是 1 说明此时没有其他的任务要写入
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //在当前线程中直接执行写入
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
```

对于 commit 方法提交，如果当前并没有其他文件写入任务要执行，那么会在当前线程执行文件写入。

接下来终于要揭晓 QueuedWork 的神秘面纱了。

## [QueuedWork](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/QueuedWork.java)&#x20;

在看代码细节之前，先来看看官方注释对 QueuedWork 有个初步了解：

> Internal utility class to keep track of process-global work that's outstanding and hasn't been finished yet.&#x20;
>
> New work will be {@link [#queue](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/QueuedWork.java;l=221) queued}. It is possible to add 'finisher'-runnables that are {@link [#waitToFinish](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/QueuedWork.java;l=154) guaranteed to be run}. This is used to make sure the work has been finished.&#x20;

> This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for  other things in the future.&#x20;
>
> &#x20;The queued asynchronous work is performed on a separate, dedicated thread.&#x20;

通过以上注释，可以获取一下信息：

1. 用于维护进程级别的任务
2. 新任务通过queue方法入队
3. 可以增加 finisher 任务，它们肯定会被执行，可以用来确保任务已经被执行完成
4. 目前主要用于执行 Sp 的文件同步并提供等待完成机制
5. 队列的任务会在单独线程中执行。

下面就来看看 QueuedWork 的代码实现，先来看看它的几个重要的静态属性：

### 静态属性

```
private static final long DELAY = 100;
//类的锁
private static final Object sLock = new Object();
//处理工作的锁，确保只有一个线程在处理任务
private static Object sProcessingWork = new Object();

private static Handler sHandler = null;
//存储所有finisher
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
//存储所有任务
private static final LinkedList<Runnable> sWork = new LinkedList<>();
//是否允许延迟
private static boolean sCanDelay = true;

  
public static void addFinisher(Runnable finisher) {
    synchronized (sLock) {
        sFinishers.add(finisher);
    }
}    
```

### QueuedWork#queue()

前面在 SharedPreferencesImpl#enqueDiskWrite()，调用了queue 方法将文件写入工作入队，该方法源码如下：

```
public static void queue(Runnable work, boolean shouldDelay) {
    //1.获取 handler
    Handler handler = getHandler();

    synchronized (sLock) {
        //2. 将work添加到任务列表
        sWork.add(work);
        //3. 发送消息触发执行
        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}
```

主要逻辑就是讲任务加入队列中，然后通过handler发送消息来出发任务的执行。这个方法的第二个参数标识是否需要延后一段时间（DELAY 的值是100），在SharedPreferencesImpl#enqueDiskWrite()中是这样调用的：

```
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
```

也就是说，**对于 commit ，shouldDelay 为 false；对于 apply ，shouldDelay 为true**。shouldDelay 决定了在通过 handler 发送消息时是否启用延时。

那为什么要 apply 进行延时呢？&#x20;

在 SharedPreferencesImpl 的 writeToFile 方法中有如下判断：

```
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
    if (isFromSyncCommit) {
        needsWrite = true;
    } else {
        synchronized (mLock) {
            //对于 apply，没有必要每次都写入，而是只执行最新一次的提交对应的写入
            if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                needsWrite = true;
            }
        }
    }
}
```

DELAY 的值是常量100，如果100ms 内有多次 apply 提交，这个延时可以确保当100ms后，任务队列中的文件写入任务被统一处理时，只有最新的apply提交对应的文件写入任务会真正被执行。因为只有它对应的mcr的版本号是和内存 一致的，其他的mcr版本都低于内存版本。这样可以有效去除冗余的文件写入，提升性能。

### getHandler()

上面方法中 handler 的获取调用了 getHandler() 方法，其源码如下：

```
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;
    }
}
```

可以看到，初始化时，新建了一个 `HanlderThread` ，并基于它创建了一个 QueuedWorkHandler 赋值给 sHandler。

### QueuedWorkHandler

QueuedWorkHandler 是 QueuedWork 的静态内部类，它的定义很简单，在收到MSG\_RUN消息后，调用 processPendingWork() 处理所有待执行的任务。

```
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();
        }
    }
}
```

### processPendingWork

```
private static void processPendingWork() {
    synchronized (sProcessingWork) {
        LinkedList<Runnable> work;

        synchronized (sLock) {
            work = (LinkedList<Runnable>) sWork.clone();
            sWork.clear();

            // 移除所有MSG_RUN消息
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        if (work.size() > 0) {
            //按顺序执行任务
            for (Runnable w : work) {
                w.run();
            }
        }
    }
}
```

processPendingWork 的逻辑也很简单，就是将任务列表中的任务按序执行。正常情况下，这个过程就是在 getHandler 中创建的 HandlerThread 中进行。

那不正常情况呢？

### waitToFinish

```
/**
 * Trigger queued work to be processed immediately. The queued work is processed on a separate
 * thread asynchronous. While doing that run and process all finishers on this thread. The
 * finishers can be implemented in a way to check weather the queued work is finished.
 *
 * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
 * after Service command handling, etc. (so async work is never lost)
 */
public static void waitToFinish() {

    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // Delayed work will be processed at processPendingWork() below
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }

    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        processPendingWork();
    } finally {
        StrictMode.setThreadPolicy(oldPolicy);
    }

    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }
    //...

}
```

waitToFinish 会在当前线程立刻执行所有待执行的任务，任务执行完后会一并执行所有 finisher 来通知任务执行完成。

而通过方法的注释可以看出，这个方法主要在 Activity的onPause 方法中、BroadcastReceiver 的 onReceive 方法后、以及Service 的 onCommand 后调用（具体见下面代码），以确保所有任务都被执行没有丢失，但这回导致在主线程执行文件写入，是有可能造成性能问题的。

```
//Service 启动
private void handleServiceArgs(ServiceArgsData data) {
        Service s = mServices.get(data.token);
        if (s != null) {
            try {
                if (data.args != null) {
                    data.args.setExtrasClassLoader(s.getClassLoader());
                    data.args.prepareToEnterProcess();
                }
                int res;
                if (!data.taskRemoved) {
                    //调用Service的 onStartCommand
                    res = s.onStartCommand(data.args, data.flags, data.startId);
                } else {
                    s.onTaskRemoved(data.args);
                    res = Service.START_TASK_REMOVED_COMPLETE;
                }

                QueuedWork.waitToFinish();
                
                //.....
            }
        }
    }
```

```
//service stop
private void handleStopService(IBinder token) {
        Service s = mServices.remove(token);
        if (s != null) {
            try {
                //调用Service的onDestroy方法
                s.onDestroy();
                s.detachAndCleanUp();
                //.....
                
                QueuedWork.waitToFinish();

                //....
            } catch (Exception e) {
                if (!mInstrumentation.onException(s, e)) {
                    throw new RuntimeException(
                            "Unable to stop service " + s
                            + ": " + e.toString(), e);
                }
                Slog.i(TAG, "handleStopService: exception for " + token, e);
            }
        } 
    }
```

```
//在 Android 11 之前，在 onPause调用后执行 QueuedWork.waitToFinish();
@Override
    public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
            int configChanges, PendingTransactionActions pendingActions, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        if (r != null) {
            if (userLeaving) {
                performUserLeavingActivity(r);
            }

            r.activity.mConfigChangeFlags |= configChanges;
            performPauseActivity(r, finished, reason, pendingActions);

            // Make sure any pending writes are now committed.
            if (r.isPreHoneycomb()) {
                QueuedWork.waitToFinish();
            }
            mSomeActivitiesChanged = true;
        }
    }
```

```
//在Android 11之后，在onStop 后调用
public void handleStopActivity(IBinder token, boolean show, int configChanges,
            PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
        final ActivityClientRecord r = mActivities.get(token);
        r.activity.mConfigChangeFlags |= configChanges;

        final StopInfo stopInfo = new StopInfo();
        performStopActivityInner(r, stopInfo, show, true /* saveState */, finalStateRequest,
                reason);

        //...

        // Make sure any pending writes are now committed.
        if (!r.isPreHoneycomb()) {
            QueuedWork.waitToFinish();
        }
        
        //...
}
```

## 建议

* 尽量使用apply
* 多次 put ，一次apply
* 拆分 sp 文件的大小，避免一个 app 只使用一个 Sp，这样文件将会变得很大，写入时间会变长。如果恰好卡在 waitToFinish 这样的时间点，有可能造成 ANR。
* 不要连续多次edit(), 应该获取一次获取edit(),然后多次执行putxxx(), 减少内存波动
* 不要使用MODE\_MULTI\_PROCESS

## 相关问题

### commit 和 apply 区别？

commit 会等待文件写入完成，并且有可能会在主线程写入文件，并且没有针对短时间内频繁更新做优化，有可能导致每次操作都在主线程写入。

apply 如果短时间内(100ms)有多次提交，只有最后一次会执行文件写入（因为会对比每次提交的版本号是否与当前内存版本号一致），并且是在单独的线程里执行写入，不会影响性能。

## 相关链接

[gityuan:全面剖析SharedPreferences](http://gityuan.com/2017/06/18/SharedPreferences/)

[SharedPreferences灵魂拷问之原理](https://juejin.im/post/5df7af66e51d4557f17fb4f7)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wenhaiz.gitbook.io/kx-android/android/data/sharedpreference.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
