[置顶] Android本地存储之SharedPreferences源码解析

时间:2022-12-24 00:17:09

先来看下SharedPreferences的使用方法

SharedPreferences存数据:

//获得SharedPreferences的实例 sp_name是文件名
SharedPreferences sp = getSharedPreferences("sp_name", Context.MODE_PRIVATE);
//获得Editor 实例
SharedPreferences.Editor editor = sp.edit();
//以key-value形式保存数据
editor.putString("data_key", "data");
//apply()是异步写入数据
editor.apply();
//commit()是同步写入数据
//editor.commit();

SharedPreferences取数据:

//获得SharedPreferences的实例
SharedPreferences sp = getSharedPreferences("sp_key", Context.MODE_PRIVATE);
//通过key值获取到相应的data,如果没取到,则返回后面的默认值
String data = sp.getString("data_key", "defaultValue");

数据以xml形式存储在/data/data/项目包名/shared_prefs/sp_name.xml里,如图:

[置顶]        Android本地存储之SharedPreferences源码解析

[置顶]        Android本地存储之SharedPreferences源码解析
以上是SharedPreferences的简单用法,下面从源码角度来看下整个过程:

先把结论贴出来:

1. SharedPreferences读取xml文件时,会以DOM方式解析(把整个xml文件直接加载到内存中解析),在调用getXXX()方法时取到的是内存中的数据,方法执行时会有个锁来阻塞,目的是等待文件加载完毕,没加载完成之前会wait()。

2. SharedPreferences写文件时,如果调用的commit(),会将数据同步写入内存中,内存数据更新,再同步写入磁盘中;如果调用的apply(),会将数据同步写入内存中,内存数据更新,然后异步写人磁盘,也就是说可能写磁盘操作还没有完成就直接返回了。在主线程中建议使用apply(),因为同步写磁盘,当文件较大时,commit()会等到写磁盘完成再返回,可能会有ANR问题。

3. SP第一次初始化到读取到数据存在一定延迟,因为需要到文件中读取数据,因此可能会对UI线程流畅度造成一定影响。

我们通过context.getSharedPreferences方法获取SharedPreferences实例

//name是存储的文件名,mode是创建文件时的模式
public abstract SharedPreferences getSharedPreferences(String name, int mode);

Context里面的getSharedPreferences方法是抽象方法,接着就找到了Context的实现类是ContextImpl:

//Map from preference name to generated path. 
//mSharedPrefsPaths为保存文件地址的Map
private ArrayMap<String, File> mSharedPrefsPaths;

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 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) {
//如果参数name为null,则直接创建一个null.xml的文件
if (name == null) {
name = "null";
} }
File file;
synchronized (ContextImpl.class) {
//第一次进的时候初始化
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
//通过getPreferencesDir()来获取shared_prefs目录,然后根据文件名加上xml后缀
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
} }
return getSharedPreferences(file, mode);}

@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
//Android N以后不支持Context.MODE_WORLD_WRITEABLE和Context.MODE_WORLD_READABLE模式
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//通过getSharedPreferencesCacheLocked()根据包名来获得缓存preferences的Map
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
//如果静态内存缓存中有,直接取出来
sp = cache.get(file);
if (sp == null) {
//缓存中没有,new一个sp出来(SharedPreferences是一个接口,SharedPreferencesImpl是其实现类)
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.
//MODE_MULTI_PROCESS多进程模式或者SDK<11时,会重新从磁盘加载文件,不过多进程模式
//已经被deprecated了,官方建议使用ContentProvider来处理多进程访问.
sp.startReloadIfChangedUnexpectedly();
}
return sp;}
//通过getPreferencesDir()来获取shared_prefs目录,然后根据文件名加上xml后缀
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
//Android N以后不支持Context.MODE_WORLD_WRITEABLE和Context.MODE_WORLD_READABLE模式
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");
} }}
/** * Map from package name, to preference name, to cached preferences. */
//sSharedPrefsCache根据包名来缓存preferences的Map
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
//首先在静态缓存sSharedPrefsCache中查找preferences的Map,如果有,直接取出来返回
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
//如果静态缓存中没有,直接new一个Map并且加到静态缓存sSharedPrefsCache中
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;}

下面再来分析一下SharedPreferencesImpl实例化过程,也是从磁盘读取文件到内存中的过程:

SharedPreferencesImpl(File file, int mode) {
mFile = file;
//产生一个.bak结尾的临时File
mBackupFile = makeBackupFile(file);
//加载模式
mMode = mode;
//标志位,表示从磁盘加载到内存中是否完成
mLoaded = false;
//保存在内存中sp对象的Map
mMap = null;
//从磁盘中加载文件到内存中
startLoadFromDisk();
}
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");}

private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
//新起一个Thread开始加载
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();}

private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
} }
// Debugging
if (mFile.exists() && !mFile.canRead()) {
//忘了写权限
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//SharedPreferences文件以流形式读出来
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
//读取xml中的内容,构造一个Map赋值给下面的mMap
map = XmlUtils.readMapXml(str);
} catch (XmlPullParserException | IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
} } catch (ErrnoException e) {
/* ignore */ }
synchronized (SharedPreferencesImpl.this) {
//标记位,数据已经加载完成
mLoaded = true;
if (map != null) {
//赋值给内存中的mMap
mMap = map;
//设置时间戳和文件的大小
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
// 通知所有线程被SharedPreferencesImpl.this对象锁住的数据已经加载完成了,数据可以使用了。
notifyAll(); }}

最后来看下读文件和写文件,首先是读文件(文件从磁盘加载到内存中),这里看的SharedPreferencesImpl类中getString(String key, String defValue)地源码,其他getXXX()也是一样的。

@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
//同步等待文件从磁盘加载到内存完成为止,否自wait()
awaitLoadedLocked();
//从内存中的mMap直接取值
String v = (String)mMap.get(key);
return v != null ? v : defValue; }}

private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}}

写文件(先把要修改的数据写到内存中,再写入磁盘中):

public final class EditorImpl implements Editor {
// 保存putXXX()方法时提供的所有要提交修改的数据
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
} }

public boolean commit() {
//数据提交到内存中
MemoryCommitResult mcr = commitToMemory();
//将数据写入磁盘中
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//阻塞等待写操作完成
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;}
}


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

private void notifyListeners(final MemoryCommitResult mcr) {
if (mcr.listeners == null || mcr.keysModified == null ||
mcr.keysModified.size() == 0) {
return;
} if (Looper.myLooper() == Looper.getMainLooper()) {
for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
final String key = mcr.keysModified.get(i);
for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
if (listener != null) {
listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key); }
}
} } else {
// Run this function on the main thread.
ActivityThread.sMainThreadHandler.post(new Runnable() {
public void run() {
notifyListeners(mcr);
}
});
}}

// Returns true if any changes were madeprivate MemoryCommitResult
//commit()和apply()两个方法都调用了commitToMemory。该方法主要根据mModified和是否被
//clear修改内存中mMap的值,然后返回写磁盘需要的一些相关值
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
// 当有多个写操作时,clone一份
mMap = new HashMap<String, Object>(mMap);
}
mcr.mapToWriteToDisk = mMap;
//未完成的写操作数+1
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()) {
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;}

// Return value from EditorImpl#commitToMemory()
//写磁盘需要的相关值
private static class MemoryCommitResult {
public boolean changesMade;
// any keys different? public List<String> keysModified;
// may be null public Set<OnSharedPreferenceChangeListener> listeners;
// may be null public Map<?, ?> mapToWriteToDisk;
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
public volatile boolean writeToDiskResult = false;
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}}

commit()和apply()这两个方法都是首先修改内存中缓存的mMap的值,然后将数据写到磁盘中。它们的主要区别是commit会等待写入磁盘后再返回,而apply则在调用写磁盘操作后就直接返回了,但是这时候可能磁盘中数据还没有被修改。