这是我参与更文挑战的第1天,活动详情查看: 更文挑战
1. ContextImpl作用
SharedPreferences是一个接口,具体实现类是SharedPreferencesImpl
。在Android源码中该类属于隐藏类,因此具体的对象获取需要通过Context
。Application、Service和Activity都间接继承自Context,通过装饰模式,具体的操作交给ContextImpl
对象。ContextImpl提供了getSharedPreferences
方法来获取一个SharedPreferences对象。
我们知道了SharedPreferences
对象是通过ContextImpl
获取的,那么App中是如何保证获取的对象是同一个呢?
其实每一个App中,ContextImpl
的数量为Activity个数+Service个数+Application,为了确保SharedPreferences
d的唯一性,在ContextImpl
中通过一个静态集合来存储SharedPreferences
对象。
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
ArrayMap中通过包名来细分应用的SharedPreferences
。然后通过SharedPreferences的文件地址来获取具体的SharedPreferences
对象。具体的存储路径为:
/data/user/0/com.mdy.sp/shared_prefs/${name}.xml
2. 数据 load 与 get
SharedPreferences的具体实现交给了SharedPreferencesImpl
类。在创建SharedPreferencesImpl对象时,会调用startLoadFromDisk
方法加载磁盘数据到内存中,内部通过创建一个单独的线程来执行loadFromDisk
操作。
private void loadFromDisk() {
synchronized (mLock) {
// mLoaded = true表示数据已经被加载过了,所以直接返回
if (mLoaded) {
return;
}
//mBackupFile表示备份文件,备份文件存在的话,则删除源文件并替换
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
//通过IO的方式读取磁盘的xml文件解析到map集合中
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);
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;
}
//解析成功的话,将mLoaded置为true,并将map引用地址赋值给mMap
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
//唤醒主线程
mLock.notifyAll();
}
}
}
loadFromDisk
方法加载数据并解析,最后将引用地址赋值给SharedPreferencesImpl的mMap
。调用get系列的方法获取缓存值时,实际是从mMap
中获取缓存值。
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
get系列方法中会调用awaitLoadedLocked
判断磁盘的读取是否结束,根据mLoaded的布尔值来确认,true表示完成,否则调用 mLock.wait()
方法暂停主线程。这里就和上述的loadFromDisk
方法最后调用的notifyAll
方法对应上了,磁盘读取结束唤醒主线程。
3. 数据 缓存
SharedPreferences中数据的缓存需要通过Editor
的实现类EditorImpl
来实现。通过对应的get方法来缓存数据并调用commit或者apply提交。
3.1 内存同步
SharedPreferences中数据存储到磁盘之前,会首先调用commitToMemory
方法同步到内存中。
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;
//mDiskWritesInFlight值+1
mDiskWritesInFlight++;
synchronized (mEditorLock) {
boolean changesMade = false;
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// value 属于Editor或者为null,直接抛弃
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
//当存在key时,根据value值是否一致来判断是否添加
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
}
//mCurrentMemoryStateGeneration值+1
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
//memoryStateGeneration值后面会传递给MemoryCommitResult,并在磁盘写入时,用来判断是否有磁盘正在写入
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
commitToMemory
方法还是比较简单的,就是将新增的数据添加到mMap集合中,并生成一个MemoryCommitResult
对象,后面会通过writeToFile
方法写入磁盘。
3.2 commit提交
对于commit方法和apply方法的具体区别在什么地方,可能大家都会说,commit是同步提交,apply是单独开了一个线程提交。到底怎么回事,先看一下源码:
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
commitToMemory
方法上面已经分析了是写入内存并返回mcr
对象。之后调用enqueueDiskWrite
方法写入磁盘,然后调用 mcr.writtenToDiskLatch.await()
方法等待写入完成,最后返回结果mcr.writeToDiskResult
。这里注意到调用enqueueDiskWrite
方法时传入的postWriteRunnable
参数是一个null对象。
3.3 apply提交
apply方法首先也是调用commitToMemory
方法将数据写入内存,返回一个mcr
对象。后续会构造一个postWriteRunnable
对象,然后调用enqueueDiskWrite
时传递进去。根据上述commit方法,我们可以知道commit方法传入的postWriteRunnable
是一个null对象,而apply传入的是一个具体的Runnable对象。
3.4 enqueueDiskWrite
enqueueDiskWrite
方法中根据传递的postWriteRunnable
参数是否为null来区分commit和apply。
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
// commit提交时postWriteRunnable==null
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//具体的写入操作
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
//写入结束mDiskWritesInFlight值-1
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// isFromSyncCommit==true 表示commit提交
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
//wasEmpty==true表示mDiskWritesInFlight==1
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// wasEmpty == false,执行入队操作
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
在isFromSyncCommit
为true时,表示commit提交,会根据mDiskWritesInFlight
值是否为1,来判断是否有磁盘写入操作。在前面调用commitToMemory
方法写入内存时,会将该值+1。若是有磁盘正在写入,那么mDiskWritesInFlight的值必定>1,则会将commit提交的任务添加到QueuedWork
的队列中。磁盘写入操作结束,会将mDiskWritesInFlight
-1,因此可以得出一个结论:commit方法提交数据时,会根据是否有磁盘写入操作在执行来区分,若无则在主线程进行写入操作,有的话就先将任务添加到QueuedWork中,然后再HandlerThread中执行任务
。
在isFromSyncCommit
为false时,表示apply提交。apply方法提交的任务会直接添加到QueuedWork
中。要注意到quene
的最后一个参数,根据!isFromSyncCommit
来获取。
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
// 任务添加到sWork中
sWork.add(work);
if (shouldDelay && sCanDelay) {
//apply调用延迟方法,100ms
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
//commit调用
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
queue
方法中handler获取的是HandlerThread
中的Looper对象。当是apply提交时,执行的是延迟方法,commit提交则是立刻发送Message到QueuedWorkHandler
的handleMessage
方法执行,最终都会走到processPendingWork
方法。
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();
}
}
}
}
processPendingWork
方法就是用于执行sWork中添加的任务,也就是执行我们传递的writeToDiskRunnable
对象,最终会走到内部的writeToFile
方法去写入磁盘。
源码分析到这里,发现SharedPreferences
貌似也没啥大问题,那么它的问题到底出现在哪里呢?在下面:
public static void waitToFinish() {
processPendingWork();
}
QueuedWork
中结识了该方法会在Acitivity执行OnPause、BroadcastReceiver的onReceive之后,具体在ActivityThread中调用该方法。内部调用processPendingWork
方法,但此时要注意到,这是在主线程调用的,而且内部执行的是磁盘的写入操作,如果waitToFinish
超时了,就会导致ANR。
4. 总结
- 不需要返回结果时尽量采用apply方式提交。每个SharedPreferences对应的xml文件应该尽可能的小,这样磁盘写入的才会更快。
- SharedPreferences支持多线程,看看内部这么多的synchronized就知道了,但不支持多进程。
5. SharedPreferences如何支持多进程?
首先明确以下两点:
- SharedPreferences不支持多进程,在多进程的访问的情况下无法实现数据的同步。
- 设置
SharedPreferences的mode = Context.MODE_MULTI_PROCESS
时。当每次调用getSharedPreferences
方法时,都会调用startLoadFromDisk
方法从磁盘加载数据到内存,它的缺点在于依然无法实现数据同步,同时该mode已经被废弃。
虽然不推荐使用SharedPreferences
来实现进程间通信,但万一面试遇到了呢;所以我们可以使用ContentProvider
来实现。实现ContentProvider时,内部数据存储采用SharedPreferences来实现,通过ContentProvider的CRUD方法来操作SharedPreferences的方法。其他进程通过Uri来访问SharedPreferences并执行数据的CRUD。但是在其他进程使用该SharedPreferences对象可能需要我们多封装一层,达到和正常使用SharedPreferences相似的体验。