最近項目驗收遇到了SharedPreferences
(如下簡稱SP
)有關的問題,因而就去網上搜,知道了大概,這裏作一次源碼解析,加深理解。衆所周知,SP
是輕量級持久化工具,把鍵值對寫成xml文件保存在data/data/packagename/shared_prefs路徑下,注意SP
這個類並不支持跨進程使用!下面來看一下如何獲取SP
:java
//Context.java
//name是xml的文件名,mode是操做模式
public abstract SharedPreferences getSharedPreferences(String name, int mode);
複製代碼
mode有如下幾種:android
//SharedPreferences.java
public interface SharedPreferences {
/**
* 用於修改SharedPreferences值的接口,要修改sp裏的值必須經過Editor對象
*/
public interface Editor {
/**
* 保存一個鍵值對
*/
Editor putString(String key, @Nullable String value);
/**
* 根據key移除鍵值對
*/
Editor remove(String key);
/**
* 清除sp文件裏的內容
*/
Editor clear();
/**
* 同步提交修改數據
*/
boolean commit();
/**
* 異步提交修改數據
*/
void apply();
}
/**
* 獲取數據
*/
@Nullable
String getString(String key, @Nullable String defValue);
/**
* 獲取編輯器
*/
Editor edit();
}
複製代碼
上面註釋簡單的介紹了SP
的方法,固然還有其餘類型數據的get
和put
方法沒有列舉,這裏只介紹一種讀取和寫入的方法。Context
的實現類是android.app.ContextImpl
,看一下getSharedPreferences
的實現:緩存
//ContextImpl.java
@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) {
mSharedPrefsPaths = new ArrayMap<>();
}
//根據名字取文件
file = mSharedPrefsPaths.get(name);
if (file == null) {
//根據名字建立文件
file = getSharedPreferencesPath(name);
//保存到ArrayMap中
mSharedPrefsPaths.put(name, file);
}
}
//根據文件獲取sp對象
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
//sp實現類
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//getSharedPreferencesCacheLocked根據包名獲取ArrayMap<File, SharedPreferencesImpl>
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//檢查傳入的mode,若是是MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE將拋SecurityException
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;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// 若是是多進程模式須要從新讀取文件
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
複製代碼
從代碼能夠看到SP
的實現類是SharedPreferencesImpl
,先看一下構造方法:bash
//SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
mFile = file;
//備份file
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
//開啓一個名爲SharedPreferencesImpl-load的線程從磁盤讀取數據
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
//是否讀取過
if (mLoaded) {
return;
}
//若是備份文件存在,刪除mFile,將備份文件重命名給mFile
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
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);
//將xml文件轉成map
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) {
//表示已經讀取過,下次調用getSharedPreferences不會再從磁盤讀取
mLoaded = true;
if (map != null) {
//賦值給成員變量mMap
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
//釋放鎖
mLock.notifyAll();
}
}
複製代碼
整個流程的代碼看起來都比較簡單,這裏也就不過多的闡述了,這裏我有個疑問爲何要備份文件呢?且往下看。app
SharedPreferencesImpl
構造方法中主要是開啓一個子線程將xml文件讀取到內存中(轉成
map
)。
//SharedPreferencesImpl.java
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//阻塞等待sp將xml讀取到內存後再get
awaitLoadedLocked();
String v = (String)mMap.get(key);
//若是value爲空返回默認值
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
...
// sp讀取完成後會把mLoaded設置爲true
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
複製代碼
這裏主要說一下awaitLoadedLocked()
這個操做會阻塞主線程,因此說SP
是輕量級持久化方式,若是文件過大阻塞時間就會變長,由於將xml文件讀取完成後纔會釋放鎖mLock.notifyAll()
;異步
還記得以前說過要想修改值,必須經過Editor
對象,SP
獲取Editor
:編輯器
//SharedPreferencesImpl.java
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
//每次都會返回一個新的Editor
return new EditorImpl();
}
複製代碼
再看putString
,remove
,clear
:ide
//SharedPreferencesImpl.EditorImpl.java
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
//將鍵值對寫入mModified
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (mLock) {
//移除操做很雞賊,後面會根據value是不是this來判斷是put仍是remove
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (mLock) {
//後面會根據這個判斷是否clear
mClear = true;
return this;
}
}
複製代碼
這裏能夠看到修改值只會將其存到mModified
的map
中去,因此在編輯器中所作的全部更改都會批處理,直到咱們調用commit
或apply
纔會設置到mMap
和xml文件中去,這裏先說個結論當咱們調用putxxx
和remove
或clear
時,不管誰在前誰在後都會先執行remove
或clear
,接下來就該分析commit
和apply
了。工具
這裏先說個結論當咱們調用putxxx
和remove
或clear
時,不管誰在前誰在後都會先執行remove
或clear
,具體說明看後面代碼。post
//SharedPreferencesImpl.EditorImpl.java
public boolean commit() {
//提交到內存中,並返回一個MemoryCommitResult對象,具體看3.5
MemoryCommitResult mcr = commitToMemory();
//放入寫入隊列,具體看3.6
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
//線程等待,直到寫入文件操做後
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
//通知監聽
notifyListeners(mcr);
//返回提交結果
return mcr.writeToDiskResult;
}
複製代碼
//SharedPreferencesImpl.EditorImpl.java
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
// 這個時候說明正在寫入文件,沒法修改,寫入文件以後會重置爲0,具體看3.6
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());
}
synchronized (mLock) {
//是否發生改變
boolean changesMade = false;
//調用了clear方法的操做,也驗證了前面的結論,會先執行clear操做
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// 還記得前面remove方法裏面存放this就是爲了判斷remove的,
// 也驗證了前面的結論,其次會執行remove
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
//未發生修改跳過
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
//發生修改或者修改存入map
mMap.put(k, v);
}
// 發生改變
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//將修改的map置空
mModified.clear();
if (changesMade) {
// 當前提交到內存的次數
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
//將提交內存次數,發生修改的集合,監聽,整個map封裝成MemoryCommitResult
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
複製代碼
//SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// commit操做爲true,apply爲false
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
// 寫入文件
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
// 寫入完成後減1,和3.5節相照應來確保寫入狀態是否完成
mDiskWritesInFlight--;
}
//apply操做纔會執行,具體看3.7節
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// 前面也說了commit操做時才爲true
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
// 和3.5節的這行代碼mDiskWritesInFlight++相照應
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 執行寫入文件
writeToDiskRunnable.run();
return;
}
}
//apply操做纔會執行,具體看3.8節
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
複製代碼
到這裏也就看出來了commit
是同步的。writeToFile
方法就不貼出來了,簡單說說它的做用:
key
發生改變,則直接返回;不然執行step2;mMap
所有信息寫入文件,若是寫入成功則刪除備份文件,若是寫入失敗則刪除mFile
,同時也就解答了以前爲何要備份文件的問題;mcr.setDiskWriteResult
設置寫入文件是否成功,並讓線程結束等待狀態。//SharedPreferencesImpl.EditorImpl.java
public void apply() {
// 3.5節已講過
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
// 線程等待
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
// 將awaitCommit加入QueuedWork
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 放入寫隊列
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 通知監聽
notifyListeners(mcr);
}
複製代碼
//QueuedWork.java
// apply纔會執行這個方法,而且第二個參數爲true
public static void queue(Runnable work, boolean shouldDelay) {
// HandlerThread的handler
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
複製代碼
這個方法主要做用是延遲執行Runnable
,從這也就看出來了apply
是異步操做。結合3.6,3.7,3.8總結一下apply
,否則看起來有點亂。
SP
文件不宜過大,若是SP
文件須要存儲的內容過多,能夠根據不一樣的功能劃分紅多個文件;getSharedPreferences
,這樣在調用put
和get
操做時,文件已經被讀取到內存中了;edit()
, 應該調用一次edit()
,由於每次調用edit()
都會新建一個Editor
;commit()
或apply()
,若是屢次存入值,應該在最後一次調用。