起因
在项目开发中发现很多数据都是用SharedPreferences做本地保存的,操作SharedPreferences只需要建立Editor,然后这个向这个Editor对象put各种各样的键值对,最后调用它的commit或者apply保存信息即可,非常简单方便。同时注释文档中说明commit是同步的而apply是异步的,说明SharedPreferences是支持多线程的,那就有些疑惑了:
- 假设要保存的数据很多,apply异步线程保存,同时主线程再去读取数据,读取数据会不会等待?
- 在异步保存时退出Activity,退出应用有影响吗?
下面将带着这两个问题去学习SharedPreferences的实现
分析
1.获取SP对象
调用Activity的getSharedPreferences方法,发现这是父类ContextWrapper的方法,如下:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
这个方法只是再次调用mBase的相应方法,mBase是ContextImpl对象,而每一个ContextWrapper都会被绑定ContextImpl,ContextWarpper子类有Application、Service、Activity,所以在这三个常见的Context中都可以使用SP。
接着看看ContextImpl中的getSharedPreferences的实现:
@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) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {//用的是类锁,不是对象锁
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);//创建File对象,name + ".xml"
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
上面的mSharedPrefsPaths是一个Map对象,它保存的是SP的名字与文件的对应关系,如果没有对应的File就创建一个File对象,注意mSharedPrefsPaths是对象的成员,但是在操作这个对象时用的不是对象锁而是类锁,为什么?接着看最后调用的它的重载方法,如下:
/**
* Map from package name, to preference name, to cached preferences.
静态的sSharedPrefsCache
*/
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//file -> SP 的map
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");
}
}
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
//获取静态的sSharedPrefsCache
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
在这里创建、返回了正儿八经干活的SharedPreferencesImpl对象,以上的分析可以总结为:获取SP时存在这样的映射关系: (对象中sp name) -> (对象 file) -> (全局的 sp)的关系,这个映射关系为了确保万无一失在操作的过程中全部使用类锁,究竟在什么情况会出错,我的单线程脑子还没想出来。。 。
2. 操作SP对象
2.1 新建与get方法
首先要明确这个SP对象它是static的,任意线程只要姿势对了都可以通过Context操作它,所以对它的操作全部都加了对象锁,构造方法如下:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
//加载数据
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
//开启新的线程加载数据
private void loadFromDisk() {
synchronized (mLock) {
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<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
//将XML文件转化为内存中的键值对
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) {//比较豪放,任意问题都catch住
thrown = t;
}
synchronized (mLock) {
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 = 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();//通知其他线程加载完成了
}
}
}
上面的代码中将所有保存在本地的数据都读取到了mMap中,接下来的getXXX方法都是从这个mMap中获取值
public Map<String, ?> getAll() {
synchronized (mLock) {
awaitLoadedLocked();//等待锁释放
//noinspection unchecked
return new HashMap<String, Object>(mMap);
}
}
2.2 edit方法
@Override
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
直接返回了EditorImpl对象,它是SharedPreferences.Editor的实现类。
//保存更改的键值对
private final Map<String, Object> mModified = new HashMap<>();
//
@Override
public Editor putBoolean(String key, boolean value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
调用putXXX方法时数据被保存在了mModified键值对中,在commit或者apply的时候提交,下面先看commit方法:
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//将mModified的变化同步给mMap
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// 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);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;//正在同步+1
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.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) {//为null删除mMap的数据
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {//更新mMap中的数据
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
上面将mModified的键值同步给了mMap,并且返回了MemoryCommitResult对象,顾名思义它是同步的结果,它的构造方法的最后一个参数其实就是mMap对象,此刻mMap中已经同步了mModified的值,接下来就顺理成章地要将mMap转换成xml文件了,看commit()中的调用的SharedPreferencesImpl.this.enqueueDiskWrite方法:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//是否是同步commit,commit方法中postWriteRunnable为null
final boolean isFromSyncCommit = (postWriteRunnable == null);
//写文件的runnable
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.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {//此刻只有这一个commit
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {//只有一个提交,就在主线程写文件了
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
接下来看apply方法:
public void apply() {
final long startTime = System.currentTimeMillis();
//和commit一样,也调用了commitToMemory同步mModified
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//也调用了enqueueDiskWrite将数据写到文件中
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);
}
apply的调用逻辑和commit是一样的都是先将mModified的数据同步给mMap。
到这里开头的第一个问题就有了答案:
不管是commit还是apply,最后都是将Editor的修改的数据同步赋值给SP的mMap对象之后才会做写文件的操作,所以apply时立即读数据,读的是内存mMap的数据,不会等待写完了然后再读文件,要等待也只是等待同步的过程。
apply调用enqueueDiskWrite写文件时postWriteRunnable不为null,直接跳到 enqueueDiskWrite方法的QueuedWork.queue处,将任务进队列了,而QueuedWork是什么?结合apply的异步写功能,很容易想到它是一个排队执行的异步线程,QueuedWork如下:
public class QueuedWork {
private static final Object sLock = new Object();
private static Object sProcessingWork = new Object();
private static Handler sHandler = null;
private static final LinkedList<Runnable> sWork = new LinkedList<>();
//新建HandlerThread 线程以及一个对应的QueuedWorkHandler
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;
}
}
//进队列方法,将work保存到sWork中,并通知使用handler去通知handlerThread执行
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);
}
}
}
/**
* @return True iff there is any {@link #queue async work queued}.
*/
public static boolean hasPendingWork() {
synchronized (sLock) {
return !sWork.isEmpty();
}
}
//取出sWork中的runnable挨个执行
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
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) {
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
/**
* 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() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}
// 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);
}
}
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();
}
}
}
}
QueuedWork封装了HandlerThread,queue方法将runnable保存到sWork中,并通知使用handler去通知handlerThread取sWork中的runnable执行,最后在子线程调用processPendingWork取出sWork中的runnable挨个执行,这时退出应用会怎么样?
QueuedWork有一个waitToFinish方法,看这个方法名大致知道它是退出前的被调用的方法,它被调用地方如下图:
一目了然,在Activity、Service stop的时候会调用这个方法。再看看这个方法的实现,等待HandlerThread中的上一个runnable结束后,判断这个线程的Looper的队列中是否有消息,如果有就接管这个事件,取sWork中所有的Runnable在主线程执行。
所以,退出Activity、退出应用不会导致apply的数据丢失,它会在退出时将异步线程切换到主线程来执行,等待数据都保存了才会退出。