主要api
1. edit()
2. commit()
3. apply()
4. putXXX()
5. getXXX()
面对的问题:
1、SharedPreferences是如何保证线程安全的, 其内部的实现用到了哪些锁
2、进程不安全是否会导致数据丢失
3、数据丢失时, 其最终的屏障--文件备份机制是如何实现的
4、如何实现进程安全的SharedPreferences
SharedPreferences每个方法都加了synchronized保证了线程同步, 针对面试中提到SharedPreferences如何支持多进程? 最近插件化和组件化中都发现了ContentProvider的身影, 可以使用ContentProvider来辅助SharedPreferences实现多进程, SharedPreferences所在的进程提供一个ContentProvider, 其他进程通过该ContentProvider对SharedPreferences进行访问
1.SharedPreferences构造函数
SharedPreferencesImpl(File file, int mode) {
mFile = file;
// 1.创建备份文件
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
// 2.将磁盘中的数据拷贝到内存中
startLoadFromDisk();
}
关于这段代码主要关注makeBackupFile与startLoadFromDisk这两个方法, 这两个方法解决了一个异常情况下数据丢失的问题, 也引起另外一个问题, 当一个SP文件数据过大时, 存在耗时的情况.
1.1 问题一:异常情况数据丢失的问题
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
针对当前SP文件, 创建一个与之对应的备份文件对象, 该对象在接下来的 startLoadFromDisk 中会用来解决异常情况下数据丢失的问题
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
// 数据拷贝
loadFromDisk();
}
}.start();
}
1.2 解决数据异常丢失, 数据量过大两个问题
下面这段代码重点关注mBackupFile、map赋值、loadFromDisk结束时将mLoaded置为true三个地方, 其他的都是代码细节与代码架构设计
private void loadFromDisk() {
synchronized (mLock) {
// 1.先判断该备份文件是否存在, 如果存在, 则直接将源文件删除掉, 将备份文件修改为源文件
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
// 2.数据: 磁盘 -> map
// 读取磁盘中指定SP文件数据到内存(map)中
// 现在做了限制, 数据大小限制为16M, 问题就出在这里, 如果数据量很大, 读取就非常耗时
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) {
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<>();
}
}
// 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();
}
}
}
其他的都是代码细节, 在平时工作写代码时可以参考这几处的代码. 上面这段代码比较长, 带着问题看代码, 只关注三处:
1、mBackupFile:加载磁盘中数据之前, 先判断是否存在备份文件, 如果存在备份文件, 则删除源文件, 将备份文件修改为源文件. 解决了异常情况下数据丢失的问题, 至于该备份文件何时被写入数据, 下面分析到commit与apply时具体分析.2、map被赋值:读取SP文件内容到map中(这里对SP文件大小做了限制16M), 当数据量过大时会比较耗时, 虽然这个读取操作是在子线程中, 但是它使用了一个标志位mLoaded, 如果进行读写操作时, 该变量为false, 则会将读写所在线程进行挂起, 这个在分析到edit时会涉及到.3、mLoaded:具体细节这里先暂时不表, 只记住结论, 创建SP对象时将该变量置为false, 当SP数据全部读取到map之后再将该变量置为true, 在这个阶段中, 其他操作该SP文件的线程将会被挂起, 直到该阶段完成(如果该过程耗时比较长, 就可能导致主线程的ANR).
最后再思考一个问题, 如果是我自己做, 估计直接就做成了锁方法, 而这里锁的是代码块, 锁了代码块之后, 如果代码块执行完成之后, 多线程问题, 其他线程开始操作这个SP了会出现什么情况? 一个mLoaded标识位解决了这个问题, 如何解决?就在edit()方法中
2、edit方法
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
结合 loadFromDisk 方法, 只是锁了代码块, 并没有对整个方法加锁, 如果在磁盘数据还没结束, 其他线程拿到SP对象调用edit之后如何处理? 会不会出现并发读写的问题? 并不会
2.1 awaitLoadedLocked如何解决并发读写问题
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 {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
虽然 loadFromDisk 方法将数据从磁盘读取到内存只是对代码块进行了加锁, 但是通过标识位mLoaded将多线程串了起来, 其他线程再拿到SP对象之后, 调用edit时判断如果mLoaded为false, 表示数据初始化还未完成, 此时直接将当前线程挂起, 同时将锁释放, 如果数据初始化线程在loadFromDisk的第二个synchronized处被挂起, 此时就会重新开始与其他线程一起竞争锁, 其他线程 如果拿到锁, 又都会在edit这里挂起, 只有初始化数据的线程会进入到synchronized内部, 至此为何锁代码块不会出现问题, 通过一个mLoaded和适当的wait、notify解决.
3. putXXX与getXXX
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
不同的锁对象与数据集将读写操作进行分离, 保证多线程情况下的读写操作安全, mModified数据何时被同步到map中? 继续向下分析commit与apply方法
4、commit 同步数据提交
public boolean commit() {
long startTime = 0;
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
这套流程代码比较长, 以这个流程图为线进行分析, 在分析时带着以下几个问题:
1、写数据map如何同步给读数据map?
2、写数据时如果遇到异常情况了如何保证源文件不会损坏, 数据不会丢失
4.1 EditorImpl.commitToMemory
这段代码也是非常的长, 只关注两件事: 1.在某处加锁, 保证数据同步与数据读取时的并发问题; 2.将写数据集mModified同步到读数据集mMap中
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
// 1.加锁, 保证mMap同步
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
// 2.加锁, 保证mModified同步问题
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
// 3.数据拷贝: 将mModified数据拷贝置mMap中
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;
}
}
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);
}
写数据集同步到读数据集之后, 接下来就是将数据写入到磁盘中
4.2 SP.writeToFile
这个代码不贴了, 代码太长, 针对commit方式提交, 没有太多需要关注的地方, 只关注writeToFile时首先会将mFile数据保持在mBackupFile中, 然后将内存数据写入到mFile中, 如果写入成功, 删除mBackupFile文件.
mBackupFile的操作对应SP初始化中mBackupFile -> mFile的过程
4.3 总结
分析commit方法时, 发现并没有创建额外的线程来执行 内存mMap -> 磁盘mFile 这个操作, 而是直接在当前线程做了这个操作, 一般情况下如果操作SP.commit是在主线程中执行, 当数据量过大时, 必然会影响到主线程, 存在耗时的情况. 可能会引起主线程ANR.
5. Edit.apply
apply是将数据写入到磁盘放在子线程中执行, 这个操作会不会解决数据量过大时的ANR问题?
public void apply() {
final long startTime = System.currentTimeMillis();
// 1.mModified -> mMap
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
// 4.设置栅栏, 数据写入完成之前走到这里的线程会被挂起
mcr.writtenToDiskLatch.await();
}
};
// 2.写数据之前将任务添加到QueueWork中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
// 3.数据写完之后, 将该task从QueueWork中移除
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
enqueueDiskWrite() {
// writeToDiskRunnable在子线程中执行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
简述一下上面代码的流程:
1、数据从mModified同步到mMap中
2、子线程执行writeToFile将mMap数据写入到SP文件中 3、通过mcr.writtenToDiskLatch.await()开启栅栏, 将所有调用awaitCommit.run方法的线程全部挂起, 直到writeToFile执行完毕, mcr.writterToDiskLatch将计数置为0, 唤醒被挂起的线程 4、QueuedWork.addFinisher添加这个awaitCommit, 为apply处理大数据导致主线程ANR埋下伏笔
5.1 apply引起主线程ANR
关注apply方法, 其内部会将awaitCommit添加到QueuedWork中
public static void addFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.add(finisher);
}
}
5.2 Activity生命周期回调(onStop、onPause)
class ActivityThread:
public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
ActivityClientRecord r = mActivities.get(token);
if (r != null) {
if (userLeaving) {
performUserLeavingActivity(r);
}
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(r, finished, reason, pendingActions);
// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
// 数据量过大时SP.apply引起ANR就是在这里被触发的
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged = true;
}
}
5.3 QueuedWork.waitToFinish
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);
}
// 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);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
// 如果finisher指向SP中的awaitCommit, 那么这里进入到awaitCommit内部的mcr.writtenToDiskLatch.await();
// 超过指定时间, 触发ANR
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
}
}
}
apply时触发主线程的ANR, 如何避免这个问题? 头条给出了方案, 并且在其APP上面已经全量应用 剖析 SharedPreference apply 引起的 ANR 问题