前言
android中我们可以通过SharedPreferences来对简单的key-value(键-值)对数据进行存取,主要用于轻量级的数据存储。SP采用xml文件格式来保存数据,该文件所在目录位于data/data/packageInfo/shared_prefs。
知识点
- context.getSharedPreferences()方法的调用源码分析
- getXxx调用的源码分析
- putXxx的源码分析
- apply的源码分析
SharedPreferences对象获取的源码分析
获取SharedPreferences对象的方法,一般有两种方式:
- PreferenceManager.getDefaultSharedPreferences()
- context.getSharedPreferences()
这里我们以第二种方式来看源码分析,这里context的最终实现类是在ContextImpl中,所以我们在ContextImpl类中查看getSharedPreferences(name, Context.MODE_PRIVATE)的源码:
public SharedPreferences getSharedPreferences(String name, int mode) {
....省略无关代码....
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
我们看一下getSharedPreferencesPath(name),这里根据传入的name在指定的目录下面创建$name.xml文件,getPreferencesDir()最终获取的是data/data/packageInfo/shared_prefs目录路径(有则获取,没则创建)。然后将其放入mSharedPrefsPaths缓存中。这里的所有操作都是在主线程。
接下来看一下getSharedPreferences(file, mode)的源码。
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
//这里每次都会先从缓存中获取SharedPreferencesImpl对象,如果存在的话则不用重新创建
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");
}
}
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;
}
这个方法返回的是SharedPreferencesImpl对象,所以说我们通过getSharedPreferences()方法最终返回的是SharedPreferencesImpl对象,而且第一次调用的时候会将创建的SharedPreferencesImpl对象存在cache缓存中,后面每次调用时都是直接从缓存中获取,并且在获取的时候添加了synchronized关键字,保证了线程的安全。(关于线程安全的内容,我们会从源码中看到,对于读写操作都添加了synchronized关键字以保证线程安全。)
理论上来说到这里我们对getSharedPreferences()方法的源码分析已经结束了,最终返回的是SharedPreferencesImpl对象。但是这里我们还需要分析一下SharedPreferencesImpl的构造方法。
SharedPreferencesImpl的构造函数
我们看一下ShreadPreferencesImpl的构造函数
ShredPreferencesImpl(File file, int mode){
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
这里我们看一下参数:
- mFile代表我们创建的SharedPreferences文件
- mBackupFile是一个备份文件,用户写入失败时会进行恢复。
这里我们先看一下makeBackupFile(file)的源码:
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
就一行代码,就是创建一个相同文件名的.bak文件(data/data/$packageInfo/shared_prefs/$name.xml.bak)。这个文件会在最终写入成功的时候删除。
然后我们主要看一下startLoadFromDisk()方法,
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
这里是开启了一个线程调用loadFromDisk()方法,
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
//当备份文件存在时,我们会删除原文件,并把备份文件移到原文件下面
//这里的目的是为了后面原文件写入本地时发生错误,下次获取时从备份文件获取
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map<String, Object> map = null;
try {
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//这里就是将SharedPreferences保存的xml文件写入内存中,这个操作在子线程
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;
}
synchronized (mLock) {
//这个标签会在后面awaitLoadedLocked用到,表示在xml从本地加载到内存完成成功之后置为true
mLoaded = true;
try {
if (thrown == null) {
if (map != null) {
//这里的mMap就是在构造函数中的mMap,用来在内存中缓存我们的配置文件
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} 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();
}
}
}
SharedPreferences对象获取的小结
这里首先是在data/data/packageInfo/shared_prefs/目录下面创建一个xml文件。而且最终返回的是SharedPreferencesImpl对象。然后在SharedPreferencesImpl对象的构造方法中创建备份文件,并在子线程中将本地创建的xml文件写入到内存中。写入成功后通知其它等待线程。
getXxx调用源码分析
这里我们以getString的源码来进行分析
public String getString(String key, @Nullable String defValue) {
//对于读写操作要加同步代码块,保证线程安全
synchronized (mLock) {
//等待上面的将本地xml读入内存中成功的通知
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
getString()方法的源码很简单,一共就三行代码,后面两行是直接从内存中(mMap)中获取数据,基本没有性能方面的问题。我们主要看一下awaitLoadedLocked()方法。
private void awaitLoadedLocked() {
//如果没有读取完成,则线程会处于等待状态
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
这里要说一下,当我们执行getString()方法时,如果xml文件还没有加载到内存中,那么所有的操作都处于等待状态,由于我们的getString()方法一般是在主线程调用,所以这里可能会造成主线程的卡顿问题。
putXxx的源码流程
相比于getXxx()方法,putXxx()方法比较麻烦一点。这里我们仍然以putString()方法来分析。一般来说putString()方法的格式如下
getSharedPreferences().edit().putString(key, value).apply();或者commit()提交
我们先看一下edit()方法
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
这里源码也很简单,首先是等待xml文件从本地读写到内存中。成功之后会创建一个EditorImpl对象,每次调用edit()方法都会创建新的EditorImpl对象。
接下来我们看一下EditorImpl类中的putString()方法。
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
这里的源码也比较简单,就是将我们传入进来的key-value值存入mModified中。mModified会在后面的apply方法中被遍历。
最后我们看一下apply()方法的源码分析
apply方法的源码分析
apply方法是各种“最佳实践”都推荐的方式,我们就来分析一下apply的源码
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
//将awiatCommit添加到LinkedList集合中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
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);
}
//这是将mMap集合的引用赋值给mapToWriteToDisk,两者指向的是同一块地址
//所以当后面修改mapToWriteToDisk的值时,mMap对应的值也修改了
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
//这个表示当调用clear方法时,会清空mapToWriteDisk集合
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
//这个方法时重点,遍历通过putString添加到mModified集合中的`key-value`
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
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;
}
}
//将mModified集合中的 'key-value'存入集合中
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//清空mModified集合
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
这里分析主要过程:
mapToWriteToDisk = mMap这里mMap前面分析过,是将本地xml文件解析到内存的mMap集合中,然后赋值给mapToWriteToDisk集合。mapToWriteToDisk.put(k, v)表示将mModified集合中的元素存入mapToWriteDisk集合中mModified.clear()清空mModified中的集合return new MemoryCommitResult返回MemeoryCommitResult对象
commitToMemory方法的主要过程就是将读取到mMap集合中的元素赋值给mapToWriteToDisk集合中,并将mModified集合中的元素添加到mapToWriteToDisk集合中。并最终返回MemoryCommitResult对象。
这里我们回到apply方法,看一下apply方法的源码,在源码中创建了两个Runnable的实现类:awaitCommit与postWriteRunnalbe。这里最最重要的方法是SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable),我们看一下enqueueDiskWrite()方法的源码:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
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();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//这里表示当通过commit()方法提交时,会直接在主线程运行run方法
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//这里调用apply方法提交时执行的方法,会将writeToDiskRunnable中的run方法最终放在子线程执行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
这里我们简单看一下queue方法是如何将wrtieToDiskRunnable放入子线程执行的,这里以前的实现是通过线程池来放入子线程执行的,现在改成通过handlerThread来放入子线程执行run方法。
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
//看一下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;
}
}
//这是QueuedWorkHandler类的handleMessage方法,这个方法在子线程中执行,所以processPendingWork()方法在子线程中执行
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
private static void processPendingWork() {
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);
}
if (work.size() > 0) {
for (Runnable w : work) {
//这里调用改了传入进来的writeToDiskRunnable的run方法
w.run();
}
}
}
}
这里在getHandler方法中创建了HandlerThread对象,HandlerThread对象继承自Thread,具体HandlerThread类的用法这里不介绍了,HandlerThread类的源码很简单,主要是在子线程中(run方法中)创建了Looper,所以我们通过sHandler发送的消息最终都是在子线程中进行处理的。然后我们进一步分析可以看到最终是在processPendingWork()方法中执行了传入进来的writeToDiskRunnable的run方法。
这里我们在回到writeToDiskRunnable中的run方法,这里我们主要分析writeToFile()方法,
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
//判断本地文件是否存在,这里为true
boolean fileExists = mFile.exists();
// Rename the current file so it may be used as a backup during the next read
if (fileExists) {
//判断备份文件是否存在(在前面调用renameTo方法时,mBackupFile文件会被删除),所以这里为false
boolean backupFileExists = mBackupFile.exists();
if (!backupFileExists) {
//这里会将mFile文件修改为备份文件
if (!mFile.renameTo(mBackupFile)) {
return;
}
} else {
mFile.delete();
}
}
try {
FileOutputStream str = createFileOutputStream(mFile);
//通过XmlUtils工具类将mapToWriteToDisk集合中的 'key-value'保存到本地创建的`xml`文件里面。
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
return;
}cache(...){
...
}
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
}
writeToFile代码分析:
- 先判断mFile(本地xml文件)文件是否存在,存在的话判断mBackupFile(备份文件)是否存在,我们这里是mFile文件存在,mBackupFile文件不存在,所以这里的操作是将mFile文件重命名为备份文件(即添加
.bak的后缀),删除mFile文件。 - 接着根据mFile创建FileOutputStream,并通过XmlUtils工具将
mapToWriteToDisk中的key-value键值对写入到本地xml文件中。 - 操作成功的话则删除
mBackupFile(备份文件),如果不存在的话则删除mFile。
对于writeToFile方法来说,apply方法是在子线程中执行的,而commit方法是在主线程中执行的(具体commit方法比较简单,这里不贴出源码了)。
这里总结一下apply方法:
- 在
commitToMemory方法中我们将mMap引用赋值给了mapToWriteToDisk的引用,所以当我们修改mapToWriteToDisk中的值时,mMap中的值也跟着修改了。 - 当我们调用
getXxx方法来获取值时,直接通过mMap来获取最新的数据。 - 我们通过
writeToFile将mapToWriteToDisk中的配置项异步写入到磁盘。