分析達成目標
- 瞭解基本實現
- SharePreferences是否執行緒安全
- SharePreferences的mode引數是什麼
- 瞭解apply與commit的區別
- 導致ANR的原因
- Android8.0做了什麼優化
基本實現
簡單使用
先從如何簡單使用開始
val sp = context.getSharedPreferences("123", Context.MODE_PRIVATE)
//通過SharedPreferences讀值
val myValue = sp.getInt("myKey",-1)
//通過SharedPreferences.Editor寫值
sp.edit().putInt("myKey",1).apply()
SharedPreferences物件從哪裡來
SharedPreferences只是一個有各種get方法的介面,結構是這樣的
//SharedPreferences.java
public interface SharedPreferences {
int getInt(String key, int defValue);
Map<String, ?> getAll();
public interface Editor {
Editor putString(String key, @Nullable String value);
Editor putInt(String key, int value);
}
}
那麼它從哪裡來,我們得到context具體實現類ContextImpl裡去找,以下程式碼都會省略不必要的部分
//ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
//可以看到返回的SharedPreferences其實就是一個SharedPreferencesImpl例項
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//每個File都對應著一個SharedPreferencesImpl例項
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
return sp;
}
從上面可以看出
- SharedPreferences真正實現是SharedPreferencesImpl
- 對於同一個程式來說,SharedPreferencesImpl和同一個檔案是一一對應的
SharedPreferencesImpl
內部儲存了一個Map用於把資料快取到記憶體
//SharedPreferencesImpl.java
@GuardedBy("mLock")//操作時通過mLock物件鎖保證執行緒安全
Map<String, Object> mMap
對於同一個SharedParences.Editor來說,每個Editor也包含了一個map用來儲存本次改變的資料
//SharedPreferencesImpl.java
@GuardedBy("mEditorLock")//操作時通過mEditorLock物件鎖保證執行緒安全
Map<String, Object> mModified
getInt
//SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//如果正在從xml檔案中同步map到記憶體,則會阻塞等待同步完成
awaitLoadedLocked();
//直接從記憶體mMap中拿資料
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
從上面程式碼可以看出,SharedPreferences會優先從記憶體中拿資料
Editor.putInt
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
putInt只是存入了mModified中,並沒有進行其它操作
Editor.apply
//SharedPreferencesImpl.java
public void apply() {
//1. 遍歷mModified
//2. 合併修改到mMap中
//3. 當前memory的代數 mCurrentMemoryStateGeneration++
//以此完成記憶體的實現。返回的MemoryCommitResult用於之後的xml檔案寫入
final MemoryCommitResult mcr = commitToMemory();
//這裡是一個純粹等待xml寫入用的任務
//writtenToDiskLatch只在本次Editor的修改完全寫入到檔案後釋放
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
//把上面的任務加入到QueuedWork的finisher列表中
//ActivityThread在呼叫Activity的onPause、onStop,或者Service的onStop之前都會呼叫QueuedWork的waitToFinish
//waitToFinish方法則會輪流遍歷執行它們的run方法,即在主執行緒觸發await
QueuedWork.addFinisher(awaitCommit);
//在上一個等待任務外面再封裝一層等待任務,用於在寫入檔案完成後從QueuedWork裡移除finish
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
//若成功完成,則從QueuedWork裡移除該finisher
QueuedWork.removeFinisher(awaitCommit);
}
};
//把寫入磁碟的任務提交去執行,commit就不會帶第二個引數,後面會說這裡
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
//寫入記憶體就直接觸發回撥監聽
notifyListeners(mcr);
}
Editor.commit
//SharedPreferencesImpl.java
@Override
public boolean commit() {
//與apply相同,直接寫入記憶體
MemoryCommitResult mcr = commitToMemory();
//直接提交disk任務給執行緒進行處理,第二個引數為空,表示自己是同步的
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
//注意這裡是與apply的不同,直接自己觸發await,不再放到Runnable裡
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
SharedPreferencesImpl.this.enqueueDiskWrite
執行寫入磁碟的任務
//SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//通過第二個引數來判斷是apply還是commit,即是否是同步提交
final boolean isFromSyncCommit = (postWriteRunnable == null);
//這個runnable就是寫入磁碟的任務
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//關鍵方法:寫入磁碟
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
//這個值在寫入一次記憶體後+1,寫入一次磁碟後-1,表示當前正在等待寫入磁碟的任務個數
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
//與QueuedWork的waitToFinish不同,這裡是在子執行緒等待寫入磁碟任務的完成
postWriteRunnable.run();
}
}
};
// 下面的條件我判斷只有當前是最後一次commit任務才會到當前執行緒執行
// 而commit正常情況下是同步進行的,因此只要之前的apply任務未執行完成,也會改為非同步執行
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
//此次同步任務為當前所有任務的最後一次
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//直接在當前執行緒執行寫入xml操作
writeToDiskRunnable.run();
return;
}
}
//這裡是把寫入xml檔案的任務放到QueuedWork的子執行緒去執行。
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
//8.0之前則是直接用單執行緒池去執行
//QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
QueuedWork
QueuedWork更像是一個等待任務的集合,其內部含有兩個列表
//寫入磁碟任務會存入這個列表中,在8.0之前沒有這個列表,只有一個SingleThreadExecutor執行緒池用來執行xml寫入任務
private static final LinkedList<Runnable> sWork = new LinkedList<>();
//等待任務會存入這個列表中
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
//插入一個磁碟寫入的任務,會放到QueuedWork裡的一個HandlerThread去執行
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
//如果是apply則100ms後再觸發去遍歷執行等待任務,commit則不延遲
if (shouldDelay && sCanDelay) {
//這裡只需要知道是觸發執行sWork裡的所有任務,即寫入磁碟任務
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
而等待任務列表sFinishers會在waitToFinish方法中使用到,作用是直接去執行所有磁碟任務,執行完成之後再輪流執行所有等待任務
//SharedPreferencesImpl.java
public static void waitToFinish() {
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// 由於我們會手動執行所有的磁碟任務,所以不再需要這些觸發執行任務的訊息
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
}
// 執行此方法的過程中若插入了其它任務,都不需要再延遲了,直接去觸發執行
sCanDelay = false;
}
//遍歷執行當前的所有等待硬碟任務的run方法
processPendingWork();
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
//所有任務執行完成之後,道路通暢了,這次waitToFinish執行通過,可以繼續延遲100ms
sCanDelay = true;
}
}
以下是Android8.0之前的waitToFinish,只是遍歷執行所有等待任務,也不會去主動寫入xml,從而導致ANR出現
public static void waitToFinish() {
Runnable toFinish;
//只是去輪流執行所有等待任務
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run();
}
}
mode許可權
我們會通過context獲取SharedPreferences物件時傳入mode
context.getSharedPreferences("123", Context.MODE_PRIVATE)
該mode會在生成SharedPreferencesImpl例項時傳入
//SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
//...
mMode = mode;
}
在xml檔案寫入完成後呼叫
//SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
//寫入檔案
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
str.close();
//給檔案加許可權
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
}
加許可權的過程就終相當於我們在串列埠使用chmod給許可權
//ConetxtImpl.java
static void setFilePermissionsFromMode(String name, int mode,
int extraPermissions) {
//預設給了同一使用者與同一群組的讀寫許可權
int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
|FileUtils.S_IRGRP|FileUtils.S_IWGRP
|extraPermissions;
if ((mode&MODE_WORLD_READABLE) != 0) {
//其它使用者讀許可權
perms |= FileUtils.S_IROTH;
}
if ((mode&MODE_WORLD_WRITEABLE) != 0) {
//其它使用者寫許可權
perms |= FileUtils.S_IWOTH;
}
FileUtils.setPermissions(name, perms, -1, -1);
}
//FileUtils.java
public static int setPermissions(String path, int mode, int uid, int gid) {
Os.chmod(path, mode);
return 0;
}
總結
基本實現
SharedPreference有一個記憶體快取mMap,以及一個硬碟快取xml檔案。每次通過apply或者commit提交一次editor修改,都會先合入mMap即記憶體中,之後再快取到硬碟。注意提交會觸發整個檔案的修改,因此多個修改最好放在同一個Editor物件中。
執行緒安全
SharedPreferences主要通過物件鎖來保證執行緒安全,Editor修改時用的是另一個物件鎖
,寫入disk時也用的是另一個物件鎖。
mode是什麼
mode類似於給通過chmod給xml檔案不同的許可權,從而實現其他應用也可以訪問的效果,預設MODE_PRIVATE給的是所有者和群組的讀寫許可權,而MODE_WORLD_READABLE與MODE_WORLD_WRITEABLE分別給了其它使用者的讀寫許可權
apply與commit
commit與apply的不同主要在於:commit直接在自己的執行緒等待寫入硬碟任務的執行,且commit一次就寫一次。而apply不會等待寫入硬碟,且8.0之後會根據當前最新的記憶體代數來過濾掉之前的所有記憶體修改,只儲存最後一次記憶體修改。
導致ANR的原因
apply提交時會生成一個等待任務放到QueuedWork的一個等待列表裡,在Activity的pause、Stop,或者Service的stop執行時,會依次呼叫這個等待列表的任務,保證每個等待列表所等待的任務都可以執行。若未執行完畢則會導致ANR
Android8.0做了什麼優化
8.0的優化方案為
- 若提交了多個apply,在執行時只會執行最後一次提交,減少了檔案的寫入次數
- QueuedWork優化:執行寫入磁碟的任務時,不再直接放到執行緒池執行,而是先放入一個真實任務的List,在waitToFinish呼叫時,會主動執行這些真實任務,再執行所有等待任務。而8.0之前只會執行等待任務,對推動任務執行沒有任何幫助