前言
最近有一场阿里一面的电话面试,其中面试官问了一道有关SharedPreference(简称sp)的ANR问题,之前同事有遇到该类问题,不过回答面试的时候没有回答的很好,因此这里总结一下。
SharedPreference源码分析
使用方法
val sharedPreferences = getSharedPreferences("TEST_SP", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("KEY_TEST","KEY_TEST")
// 异步
editor.apply()
// 同步
editor.commit()
ok,就这么简单,我们看下通过Context.getSharedPreference(XX,XX)就可以获取Sharedpreference接口实例对象。文件路径位置在:
文件路径:data/data/包名/shared_prefs/xxx.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="XXX">xxx</string>
<string name="XXX">xxx</string>
</map>
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
说句题外话,这个Context抽象类,ContextWrapper装饰代理类和ContextImpl具体实现类三者关系还蛮有意思的,等等我在写篇文章讲讲这三者之间的关系,我们就知道ContextWrapper中的mBase就是ContextImpl就可以了。
ContextImpl#getSharedPreferences()
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
// 加锁
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
// key为name value为File类的Map
mSharedPrefsPaths = new ArrayMap<>();
}
// 获取name对应的File
file = mSharedPrefsPaths.get(name);
if (file == null) {
// 若根据key拿到的File为空,则我们要进行一次生成File类 name.xml
file = getSharedPreferencesPath(name);
// 存储
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
// 加锁 保证多线程同步时只返回同一个SPImpl对象
synchronized (ContextImpl.class) {
// 一个File XML文件对应一个SharedPreferencesImpl类进行文件操作
// 这里getSharedPreferencesCacheLocked()方法是根据包名进行隔离,就不详细说了
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 实例创建
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
// 多进程访问同一个SP文件的时候,如果文件被其他的进程修改,则重新加载文件
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
ok,到现在来说,我们已经很清晰了,Context#getSharedPreferences()最终是从ArrayMap中根据Key也就是File,取到SharedPreferencesImpl的实例。
高潮部分
SharedPreferencesImpl
先看看构造方法都有些什么...
SharedPreferencesImpl(File file, int mode) {
// .xml
mFile = file;
// 用于异常的回滚备份
mBackupFile = makeBackupFile(file);
// sp 模式
mMode = mode;
// 是否将.xml文件中的内容都读取到了内存中
mLoaded = false;
// 我们存档的数据项 key value
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);
}
}
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
// 输入流将文件读出来
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
// XmlUtils读Xml文件到Map中
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
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();
}
}
}
以上是从硬盘中的.xml文件读到内存中的过程。
先看下根据Key获取Value的过程吧。
@GuardedBy("mLock")
private Map<String, Object> mMap;
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
// 加锁
synchronized (mLock) {
// 1
awaitLoadedLocked();
// 2
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
代码2处很简单吧, 就是单纯的从HashMap中根据Key取Value,但注意代码1处,这个方法很关键。还记得我们在构造方法中,开辟了一个子线程进行将磁盘中的.xml文件读取到内存中的过程么?这个awaitLoadedLocked()的目的就是防止其他线程在调用getString()方法时,但是读.xml的工作子线程还没有读完,导致的获取数据问题而存在的。
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
// 死循环判断是否读完标记
while (!mLoaded) {
try {
// 没读完就阻塞,释放锁等待
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
其他的getXXX()方法类似就不详细说明了。
edit()
@Override
public Editor edit() {
// 锁的原因 同样如getString(key,value)一样
synchronized (mLock) {
awaitLoadedLocked();
}
// 返回Editor实现类
return new EditorImpl();
}
public interface Editor {
Editor putString(String key, @Nullable String value);
Editor putInt(String key, int value);
// 移除某一个Key
Editor remove(String key);
// 清空所有的内存中的Key Value 下文会提到一个注意事项
Editor clear();
// 同步提交 返回是否提交
boolean commit();
// 异步提交 无返回结果
void apply();
....
}
Editor是一个接口,里边定义了一些putXXX的方法。
// EditorImpl 实现了Editor接口
public final class EditorImpl implements Editor {
// 锁住的对象
private final Object mEditorLock = new Object();
// 将put进来的数据先保存在内存HashMap集合中
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
// 标记是否清空内存中的数据
@GuardedBy("mEditorLock")
private boolean mClear = false;
// putXXX
@Override
public Editor putString(String key, @Nullable String value) {
// 加锁
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
// 这里remove某一个Key,并不是使用Map中remove方法 而是将这个Key对应的value设置为this,在之后进行提交到内存map中的时候不进行深拷贝这个key,目的是避免HashMap remove操作带来的性能损失。
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
@Override
public Editor clear() {
synchronized (mEditorLock) {
// 这里加入Clear标记。
mClear = true;
return this;
}
}
}
commit()方法
@Override
public boolean commit() {
...
// 将修改的内容 mModified提交到内存mMap中
MemoryCommitResult mcr = commitToMemory();
// 排队写入磁盘
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
commitToMemory()
private MemoryCommitResult commitToMemory() {
... 省略部分代码
Map<String, Object> mapToWriteToDisk;
// SharedPreferencesImpl的对象锁
synchronized (SharedPreferencesImpl.this.mLock) {
// 标记这个只是针对于apply方法作用的,因为apply方法是异步的,在主线程多次调用apply()方法会多次调用commitToMemory()方法并且mDiskWritesInFlight加1,但是向磁盘中提交数据可能还为完成,因此当磁盘提交完成后mDiskWritesInFlight 会减1。
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
// 外部的内存中的Key Value
mapToWriteToDisk = mMap;
// 标记加一
mDiskWritesInFlight++;
// putXXX() 操作只能有一个线程进行操作
synchronized (mEditorLock) {
// ... 省略部分代码
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
// 这里就是remove操作,还记得remove操作的Value被设置了this,因此在这里要直接略过
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
// 直接remove掉
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);
}
// 最终还是clear了 所以就不remove了
mModified.clear();
}
}
}
}
apply()
// 省略部分代码
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
// 1
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
// writtenToDiskLatch是CountDownLaunch,await()方法等待所有线程执行完毕之后再继续执行后面的
// 上方的代码1处调用await()
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 这里传入了Runnable方法参与异步的构成
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
enqueueDiskWrite()
// 注意这个方法的参数2 就是用来区分是异步提交还是同步提交
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
// Runnable 参数是否为空 为空就是同步方法
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
// 写入磁盘 就是拿到File的输出流,利用XmlUtil写入。
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// 写入磁盘操作以后 标记减1 之前有提过
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
// 执行
postWriteRunnable.run();
}
}
};
// 同步方法
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
// 由于是同步的 所以写入标记一定为1
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 执行
writeToDiskRunnable.run();
return;
}
}
// 加入异步队列中写入磁盘 apply方法需要执行的
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
writeToFile()
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
// ... 省略部分代码
if (fileExists) {
boolean needsWrite = false;
// 每次更新内存写入内存版本号加 1 用于判断是不是最新的
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
// 不是最新的话就直接设置写入失败
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
/******** 开始写入 ********/
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
// 写入成功结果
mcr.setDiskWriteResult(true, true);
}
QueuedWork#queue()
// 写入磁盘操作任务队列
private static final LinkedList<Runnable> sWork = new LinkedList<>();
public static void queue(Runnable work, boolean shouldDelay) {
// 1
Handler handler = getHandler();
// 加锁
synchronized (sLock) {
// 加入到Runnable队列中
sWork.add(work);
if (shouldDelay && sCanDelay) {
// 通过Handler发送执行消息
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
这里代码1 我们来看下是如何创建的子线程来执行任务队列的。
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
// 这里利用了Android提供的HandlerThread帮我们封装好的子线程与主线程通信的类,本质上是继承于Thread,并且为Thread创建了Looper继而创建了MessageQueue。
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
// 这里又单独创建了一个Handler但是传入的Looper是HandlerThread中的Looper。
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
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();
}
}
}
private static void processPendingWork() {
// ...省略部分代码
long startTime = 0;
// 1
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
// 2
synchronized (sLock) {
// 一旦获取到本类的锁 对执行队列克隆一份
work = (LinkedList<Runnable>) sWork.clone();
// 清除执行队列
sWork.clear();
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
// 遍历执行
w.run();
}
}
}
}
代码1,2处被锁了两次,这里为什么要锁两次呢?注释中给出了答案。
/** Lock for this class */
private static final Object sLock = new Object();
/**
* Used to make sure that only one thread is processing work items at a time. This means that
* they are processed in the order added.
*
* This is separate from {@link #sLock} as this is held the whole time while work is processed
* and we do not want to stall the whole class.
*/
private static Object sProcessingWork = new Object();
简单的说就是确保在同一时间只有一个线程对其进行操作,并且也意味着写入磁盘操作时按照任务队列的顺序进行执行的,sProcessingWork 所住的时整个类,只能有一个线程进入sProcessingWork锁代码块,然后让这有且只有这一个Thread去等待sLock锁的释放。
ok,到了这里,我们已经对SP有个深入的理解了。
ANR 问题
其实面试官考察的一般会问有没有遇到过apply()方法导致的ANR问题,但我们也别忘了commit()方法也会导致ANR问题。出现ANR一般是在低端机型以及Android 8.0以下会复现出来(Android8.0理论上也会出现),所以我们切换到Android 7.0看看
apply()
回头我们再来看下该方法。
@Override
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
// 2
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
// 1
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
apply()方法并没有什么问题,最大的变化在于QueuedWork类。
public static void waitToFinish() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
// 1
toFinish.run();
}
}
由于我们apply的数据过于大而又频繁,所以在我们跳转Activity的过程中ActivityThread#handlePauseActivity()调用了WorkQueue()为了确保数据已经向磁盘提交成功,但是这时候CountDownLaunch还在等待子线程结束写入,所以一直在堵塞主线程,因此导致的ANR。
ActivityThread#handlePauseActivity()
@Override
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);
// 确保数据写入完成
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
mSomeActivitiesChanged = true;
}
}
ok,我们看到在主线程Activity各个生命周期都会等待写入磁盘任务执行完毕时再去执行相应的生命周期,因为我们将任务加入队列中是通过应用托管运行的,可能由于系统内存吃紧,进程被回收或者用户主动杀死的情况下,导致写入失败,在这种不确定的情况下,所以会在生命周期的方法完成写入磁盘的操作。
ANR的解决办法
一种办法就是我们可以Hook,ActivityThread中的handler变量,处理Callback,然后反射拿到WorkQueue对象,清空等待队列,具体代码看字节跳动分析SP ANR问题这篇文章。
使用建议
1、我们知道commit()方法是同步的,所以在工作线程的时候我们就使用commit()即可,开销小。
2、主线程用apply。
3、保存的文件尽量不要太大,分模块读写sp文件。
4、存储的文件要小,不要存储大量的数据配置信息,例如长串的JSON字符串。
5、连续调用putXXX()时,不需要立即读取的情况下,最后一次调用commit或者apply方法。
6、终极办法改用MMKV腾讯出品