最近发现我们的很多anr的原因都指向了SharedPreference,那么带着一些疑问,作如下探索:
- sharedPreference为什么会阻塞主线程?
- sharedPreference有没有内存缓存,他是如何读和写的?会立即写入文件吗?
- 他是如何保证数据同步的,如何才能避免sharedPreference引起的anr?
从sharedPreference的创建,到读取,到写入
sp的创建
先来看看sharedPreference是如何创建的,在ContextImple.getSharedPreference()中,
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
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;
}
他会有缓存,并不是每次都去文件中读写,有一个以sharedPreference的名称为key(通过名称缓存一个file,以这个file为key),对应这个sharedPerference的内容为value的静态的map来缓存整个应用中的sp,所以我们最好不要创建过多的小的sp,尽量合并,不然这个静态的map会很大。
然后看看sp的构造函数:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();//
}
//初始化的时候会开一个线程去读取xml文件。
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
从构造函数中可以看出来:他会开一个线程去读取文件数据,也就是上次存储的文件,读到内存中。(由此可以看出,sp是有内存缓存的)
sp的读取:
每次读取都会对当前的sp对象加锁,然后判断是否load本地文件成功
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
这里的awaitLoadedLocked()
就是等待sp的创建,其实在sp的构造方法中已经开了一个线程去load本地文件,这里只是等待他load完成。
load完成之后就可以从内存中去取了。
sp的写操作:
我们一般使用editor对sp去进行写操作。
先来看看editor如何创建出来的:
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 (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
这里可以看出来,就算是你不读,只写,他也需要等到读取本地文件完成。
editor里用一个map将改动的东西存起来,当提交的时候他会把他先提交到内存,然后再形成一个异步的提交。
editor里可以暂时存放多个key的改动,然后形成一次提交,如果我们可以将多个提交合并成一次提交,尽量合并,因为每一次调用apply或者commit都会形成一个新的提交,创建各种锁。
主要来看一下他的apply方法:
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//阻塞调用者,谁调用,阻塞谁
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
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);
}
这里会先创建一个awaitCommit的Runnable,主要是用来阻塞调用者(writtenToDiskLatch.await()
谁调用阻塞谁),然后将这个awaitCommit加到QueuedWrok的队列中,然后又创建了一个postWriteRunnable,里面主要是做清除工作。然后最后一句enqueueDiskWrite()这个方法:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
这里又创建了一个Runnable,我们来理清一下他们之间的调用关系。
从上图可以看到,其实那个加入到单线程线程池中的异步写文件操作(writeToDiskRunnable)才真正成为了一个异步任务,其他的两个runnable只是被调用了run方法。
一个异步写操作:先调用写入文件,写入完成调用setDiskWriteResult()这里将计数锁减一,表示当前这个写操作完成。然后调用postWriteRunnable做清除队列操作,这里会调用awaitCommit这个runnable里的await()但是因为刚刚的锁已经解除了,所以这里不会阻塞。这样就表示一次apply的异步任务完成。
但是他为什么要把awaitCommit这个Runnable存放到一个静态的队列中去呢?这里就是阻塞主线程的关键了。
在QueuedWork这个类的主要内容:
/**
* Internal utility class to keep track of process-global work that's
* outstanding and hasn't been finished yet.
*
* This was created for writing SharedPreference edits out
* asynchronously so we'd have a mechanism to wait for the writes in
* Activity.onPause and similar places, but we may use this mechanism
* for other things in the future.
*
* @hide
*/
// The set of Runnables that will finish or wait on any async
// activities started by the application.
private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
new ConcurrentLinkedQueue<Runnable>();
/**
* Add a runnable to finish (or wait for) a deferred operation
* started in this context earlier. Typically finished by e.g.
* an Activity#onPause. Used by SharedPreferences$Editor#startCommit().
*
* Note that this doesn't actually start it running. This is just
* a scratch set for callers doing async work to keep updated with
* what's in-flight. In the common case, caller code
* (e.g. SharedPreferences) will pretty quickly call remove()
* after an add(). The only time these Runnables are run is from
* waitToFinish(), below.
*/
public static void add(Runnable finisher) {
sPendingWorkFinishers.add(finisher);
}
/**
* Finishes or waits for async operations to complete.
* (e.g. SharedPreferences$Editor#startCommit writes)
*
* 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() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
这里可以看出,他是要保证写入的内容不会丢失,所以才会将每个apply的await存起来,然后依次调用,如果有没有完成的,则阻塞调用者也就是主线程。
那,到底是在哪里调用的呢?
那我们就来找在我们的崩溃日志中,多次出现的
at java.util.concurrent.CountDownLatch.await(CountDownLatch.java:202)
at android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:364)
at android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
at android.app.ActivityThread.handleStopActivity(ActivityThread.java:3246)
at android.app.ActivityThread.access$1100(ActivityThread.java:141)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1239)
这里看到ActivityThread。handleStopActivity()这个方法,果然在这个方法中能找到调用QueueWork中的await的地方:
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) {
ActivityClientRecord r = mActivities.get(token);
if (!checkAndUpdateLifecycleSeq(seq, r, "stopActivity")) {
return;
}
r.activity.mConfigChangeFlags |= configChanges;
StopInfo info = new StopInfo();
performStopActivityInner(r, info, show, true, "handleStopActivity");
if (localLOGV) Slog.v(
TAG, "Finishing stop of " + r + ": show=" + show
+ " win=" + r.window);
updateVisibility(r, show);
// Make sure any pending writes are now committed.
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
// Schedule the call to tell the activity manager we have
// stopped. We don't do this immediately, because we want to
// have a chance for any other pending work (in particular memory
// trim requests) to complete before you tell the activity
// manager to proceed and allow us to go fully into the background.
info.activity = r;
info.state = r.state;
info.persistentState = r.persistentState;
mH.post(info);
mSomeActivitiesChanged = true;
}
这个方法会在什么时候调用呢?
当系统给app发送了命令之后会调用
再看一下这个handleStopActivity调用了哪些方法:
handleStopActivity的调用链
ActivityThread.handleStopActivity
ActivityThread.performStopActivityInner
ActivityThread.callCallActivityOnSaveInstanceState
Instrumentation.callActivityOnSaveInstanceState
Activity.performSaveInstanceState
Activity.onSaveInstanceState
ActivityThread.performStop
Activity.performStop
Instrumentation.callActivityOnStop
Activity.onStop
updateVisibility
H.post(StopInfo)
AMP.activityStopped
AMS.activityStopped
ActivityStack.activityStoppedLocked
AMS.trimApplications
ProcessRecord.kill
ApplicationThread.scheduleExit
Looper.myLooper().quit()
AMS.cleanUpApplicationRecordLocked
AMS.updateOomAdjLocked
看到当handleStopActivity被调用之后会回调一些我们熟悉的方法
- Activity.onSaveInstanceState
- Activity.onStop
总结一下:
使用了apply方式异步写sp的时候每次apply()调用都会形成一次提交,每次有系统消息发生的时候(handleStopActivity, handlePauseActivity)都会去检查已经提交的apply写操作是否完成,如果没有完成则阻塞主线程。