[置顶] Android SharedPreferences源码分析

时间:2022-04-27 00:17:25

我们经常使用SharedPreferences保存一些简单的数据,比如Settings的数据。如果我们只是简单的使用,可能没什么问题,但是如果要用好它还是得明白它的实现方式,下面来从源码上来分析下SharedPreferences的缓存,异步读写实现,多线程,多进程访问。

SharedPreferences简介

SharedPreferences是Android提供的一种使用XML文件保存内容的机制。其内部就是通过xml写入文件的。

SharedPreferences是一个接口类,这是使用它的一个基础,我们可以通过Context的getSharedPreference来获取SharedPreferences。如下所示:


SharedPreferences sp = context.getSharedPreferences("name",Context.MODE_PRIVATE);

第一个参数表示存储的文件名,第二个表示创建文件时的模式。

它提供了getInt, getLong, getFloat,getChar, getString 来读取int, long, float, char, String类型的数据,并且提供了一个Editor接口来用于写入对应的数据类型。Android在API14时又提供了Set类型的数据写入读取。下面看一段简单实用示例:


SharedPreferences sp = context.getSharedPreferences("name",Context.MODE_PRIVATE);

int val1 = sp.getInt("val1",0);

SharedPreferences.Editor editor = sp.edit();

editor.putInt("val1", val1+1);

editor.apply();

// editor.commit(); 跟apply方法是一样的,但是apply是异步写入。

下面就针对上面这段代码流程,分析一下SharedPreferences的源码。

获取SharedPreferences

我们通过context.getSharedPreferences方法获取SharedPreferences,而Context得真正实现者是ContextImpl,所以看看ContextImpl里面的getSharedPreferences方法:


@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);
}

// 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";
}
}

sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name); //根据文件名,获取存储的文件
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, 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(); //在多进程模式或者目标sdk版本在HONEYCOMB以下版本每次读取缓存了的sp,Android会检查xml文件是否已经被重写了。
}
return sp;
}

这段代码首先判断sSharedPrefs是否为空,如果为空则给他初始化。sSharedPrefs是一个用来缓存SharedPreferences的ArrayMap,它的key为包名,它的value为ArrayMap,这个ArrayMap保存的键值对是SharedPreferences文件名和对应的SharedPreferencesImpl(是SharedPreferences的实现类)。如果SharedPreferencesImpl已经存在,它会直接返回已经存在的SharedPreferencesImpl。如果是在多进程模式下,或者目标版本低于HONEYCOMB的时候,会检查是否需要重新从磁盘中加载文件。但是需要说的是MODE_MULTI_PROCESS模式已经被deprecated了,官方建议使用ContentProvider来处理多进程访问,其实我们项目中就遇到这么一个问题导致了一个BUG。

在重新创建SharedPreferencesImpl的时候,getSharedPreferences会调用getSharedPrefsFile来获取存储的xml文件,这个函数对xml文件名进行了组装:


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

通过getPreferencesDir()来获取shared_prefs目录,然后根据文件名加上xml后缀。Android没有提供直接访问shared_prefs目录的API,getPreferencesDir是一个私有类,我们如果想要直接访问这个目录,可以通过下面这段代码访问:


String sharedPrefsDir = context.getCacheDir().getParent().getAbsolutePath()+"/shared_prefs";

SharedPreferencesImpl构造函数

从上面的代码已经知道SharedPreferences具体的实现者是SharedPreferencesImpl。我们都知道Android的SharedPreferences对XML操作是使用DOM方式解析的(一开始就把整个XML给读取出来)。在SharedpreferencesImpl源码中,它的构造函数里面它就把XML文件给读取出来了:


SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}

它的构造函数中startLoadFromDisk就是将xml给读取出来的。下面看看startLoadFromDisk:


private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}

它使用了一个异步线程来读取xml,最终实现的函数是loadFromDiskLocked(),在读取的时候它必须获取SharedPreferencesImpl.this的锁:


private void loadFromDiskLocked() {
...
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
...

mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<String, Object>();
}


这个函数里面省略了一些代码,想看全部的,可以直接去SharedPreferencesImpl文件看。这个函数最终调用了XmlUtils.readMapXml来调用,读取整个xml的内容,放到mMap当中。

读取key对应的值

SharedPreferencesImpl的读取是非常简单的,因为在构造函数当中就已经读取整个xml文件的内容到mMap当中了,所以再次读取的时候直接从mMap当中读取就好了,但是得注意同步的问题:


public int getInt(String key, int defValue) {
synchronized (this) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}

函数awaitLoadedLocked就是等待读取文件完成。因为如果读取具体元素的时候,读取文件线程却没有完成,那么必须等待文件读取完成,不然结果肯定会乱。

写入

SharedPreferences的写入是通过Editor来实现的,Editor接口在SharedPreferencesImpl具体实现是EditorImpl,在这看看它的源码:


public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;


public Editor putInt(String key, int value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
//... 省略了其他类型的value操作,和clear,remove函数。


public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};

QueuedWork.add(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // enqueueDiskWrite会调用异步线程执行postWriteRunnable。

// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
....省略了commitToMemory
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */); //第二个参数为null,enqueueDiskWrite会直接写入。
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}

... 省略了notifyListeners

}

从源码上面可以看出,首先使用put写入的时候,只是写入到一个mModified里面,但是实际上还没写入SharedPreferencesImpl的mMap当中,更没有写入磁盘,只有当调用commit或者apply函数的时候才会开始写入。而apply是异步写入,而commit是在当前线程直接写入。commit在enqueueDiskWrite的第二个参数传入null,看看enqueueDiskWrite的实现:


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--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};

final boolean isFromSyncCommit = (postWriteRunnable == null); //如果postWriteRunnable就同步写入

// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}

QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

但是需要指出的是,两种方式首先都会先使用commitTomemory函数将修改的内容写入到SharedPreferencesImpl当中。看看commitToMemory的实现:


private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap<String, Object>(mMap);
}
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;

boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList<String>();
mcr.listeners =
new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}

synchronized (this) {
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
mMap.clear();
}
mClear = false;
}

for (Map.Entry<String, Object> e : mModified.entrySet()) { // 在这开始将修改的内容写入到mMap当中。
String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}

mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}

mModified.clear();
}
}
return mcr;
}

通知修改的变化

我们可以通过下面两个函数注册监视xml文件变化的通知,在这里我直接把函数源码给顺便贴出来了,因为比较简短:


public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.put(listener, mContent);
}
}

public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.remove(listener);
}
}

在前面分析了的函数commitToMemory中会返回修改的内容保存在MemoryCommitResult当中,然后使用使用notifyListener函数通知监听者。

总结

SharedPreferences从功能上面来讲就是三个部分读取(一开始异步全部读取出来,get的时候,如果没有读取完,会等待),写入,监听SharedPreferences的变化。另外Android会使用ArrayMap对SharedPreferences进行缓存,以SharedPreferences的name作为key。需要进一步理解的是关于多线程,多进程时的使用。

首先从线程方面来看,从源码上看apply是使用异步线程写入磁盘,commit是同步写入磁盘。所以我们在主线程使用的commit的时候,需要考虑是否会出现ANR问题。我们不用担心apply异步写入会出现先写入的内容,在该线程之后读取会读取不到,因为它写入内存的时候没有使用异步线程,所以在主线程最好使用apply。所有的线程读取的时候都会加SharedPreferencesImpl.this锁,editor写入内存的时候(写入SharedPreferencesImpl.this.mMap)也会加SharedPreferencesImpl.this锁,另外editor调用put,clear, remove方法的时候都会加上EditorImpl.this锁,这些是线程安全的保证,只有在commit/apply后才会写入内存(mMap, xml内容缓存的map变量)和磁盘。

另外从多进程方面来看,SharedPreferences本身提供了MODE_MULTI_PROCESS的模式,但是现在已经deprecated了,不建议使用。MODE_MULTI_PROCESS也仅仅是每次读取缓存的SharedPreferencesImpl时重写读取一次磁盘(其实效率很低,而且从源码看,并不能很好地保持同步)。所以Android建议使用ContentProvider来保持多进程的访问。有人已经实现了,可以通过google搜索multi process sharedpreferences找到,因为我没看过那些,所以自己搜吧,我是直接看的公司的。


理解,分析