SharedPreferences 源碼分析及使用事項
作爲Android 輕量級的存儲工具,SharedPreferences被廣泛使用,API 簡潔明瞭,易學易用,爲廣大程序小哥哥們喜聞樂見。殊不知,一片和諧的環境下,蘊藏着不少危機,本文將從源碼角度進行解析,並附上踩過的一些坑。
一般用法
SharedPreferences pref = mAppContext.getSharedPreferences(prefName, Context.MODE_PRIVATE);
源碼分析
Conext#getSharedPreferences的內部實現,具體實現在
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);//一個context可以有N多SharedPreferences文件
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);//初始化後,以後從緩存讀取file,再構建SharedPreference,mSharedPrefsPaths沒有做銷燬處理.如果SharedPreferences很多,map會很大,會佔用更多內存。
再看getSharedPreferences(file, mode)方法
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {//8.0新特性,用戶加鎖時,會拋異常,實測暫未開啓
if (isCredentialProtectedStorage()
&& !getSystemService(StorageManager.class).isUserKeyUnlocked(
UserHandle.myUserId())
&& !isBuggy()) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//SharedPreferencesImpl緩存池
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);//一個name對應一個SharedPreferences,可理解爲單例
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||//跨進程,Google不推薦
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;
}
具體分析SharePerferenceImpl
// Lock ordering rules: //一共3把鎖,注意加鎖順序
// - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
// - acquire mWritingToDiskLock before EditorImpl.mLock
構造器
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);//災備文件
mMode = mode;
mLoaded = false;
mMap = 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 map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;//mMap 是該SP文件,所包含的所有Key,Value
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();//Load完成,發一個notifyAll,表示已經準備好SP文件,阻塞結束
}
}
####get方法
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) { //線程安全 如果同時有多個方法,操作SP,這裏可能會阻塞,給我們一個啓示,優先級高的SP文件,最好單獨保存
awaitLoadedLocked();// 對於單進程來說,get方法,應該不會在這裏阻塞
String v = (String)mMap.get(key);//直接從內存中取值
return v != null ? v : defValue;
}
}
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) {
}
}
}
####Put方法
put方法,實際通過EditorImpl 完成,Editor是專爲SharedPreferences 私人定製
@GuardedBy("mLock")
private final Map<String, Object> mModified = Maps.newHashMap();
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
從上面可以看出,put方法,僅僅是把key,value存入內存mModified,並沒有保存至磁盤。有點類似於事務處理,必須是最後一步提交事務,纔算是正式生效了。
####最重要的兩個方法
public void apply() {//異步回寫磁盤
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();//回寫內存
final Runnable awaitCommit = new Runnable() {
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() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(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);
}
這次有出比較要命的地方, QueuedWork.addFinisher(awaitCommit);雖然是異步操作,但也可能會阻塞主線程
要點在ActivityThread handleXXActivity方法中
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();
}
.........
再看 這個 QueuedWork.waitToFinish();
public static void waitToFinish() {
.........
try {
while (true) {
Runnable finisher;
synchronized (sLock) { //加鎖,輪詢,同步,忐忑不?
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
......
}
從上面能看出來,Activity Service 一些操作,是需要等到SP操作結束的。所以即使是異步的apply操作也是有可能阻塞主線程的。使用要慎重。
再來看commit方法
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
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;
}
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */); 是異步的關鍵方法
幾經輾轉,會調用
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);
}
}
}
##總結一下:
####1、 apply 方法也有可能阻塞主線程,儘量保證SP操作時間可控,不要存儲過大文件
以我們工程爲例,一個key,value就有18k之巨,直接導致後臺監控到大量的SP相關的卡頓
####2、不建議通過SP,實現進程共享,完全質量無保障
####3、getXXX方法,本身也有可能阻塞(別的地方也在操作SP),所以高優先級SP文件,建議獨立開來
####4、apply 異步不需要等待執行結果,但也使用到了線程池,因此儘量合併提交
####5、commit 操作,同步操作,除非希望立即獲取返回結果,否則儘量使用apply
####6、如何解決恐怖的apply 方法阻塞主線程的問題???
有不少開發規範都規定,儘量使用apply方法,除非想理解獲得提交狀態,否則不要用commit方法。
對於小型SP存儲來說,是沒有問題的。 但大型項目,通常SP文件品種繁多,且單個SP文件,所包含的各色key,value(單個可能都不大,但好虎架不住狼多),會導致另一個致命問題,ANR。
那麼解決之道是什麼呢?
答案是爲了性能計,棄用apply。
替代方案爲:仿照apply源碼的做法,需要apply的地方,採用子線程,異步提交commit。這樣,既避免了apply方法,影響ActivityThread重要方法問題,也避免了同步操作阻塞主線程。