前言
提到Sp(后面都用這個簡稱),相信Android開發者都不會陌生,基本上寫項目都會用到,但是可能沒有深究實現細節,因此當面試時
被面試官問到相關問題,往往不知所措.
先提幾個問題:
- q1:Sp可以跨進程么?為什么?
- q2:有什么方法可以讓Sp實現跨進程?
- q3:commit和apply有什么區別?使用場景?
- q4:為什么Sp不適合存放占用內存較大的內容?如bitmap
使用
Sp的簡單使用如下
SharedPreferences preferences = this.getSharedPreferences("sp_name", Context.MODE_PRIVATE);
String key = preferences.getString("key", "");
preferences.edit().putString("my_key", "hell").apply();
調用Context.getSharedPreferences
,傳遞sp的名稱和操作模式既可獲取Sp的實例
默認的操作模式是Context.MODE_PRIVATE
,也是官方推薦的,其他幾種模式基本都被棄用了,即官方不推薦使用.
源碼分析
getSharedPreferences
我們知道Context的唯一實現是ContextImpl
,不管是Activity的context還是Application的context,context.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.
//目標sdk小於4.4時,支持sp的名字為null
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);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
ContextImpl
有一個成員變量mSharedPrefsPaths
,保存sp的名字與對應的文件的映射,這個很好理解,當我們通過context拿sp的實例
的時候,肯定先要找到sp對應文件,然后再對該文件進行讀寫操作.
值得注意的是這里對於mSharedPrefsPaths
的操作時加鎖了,鎖的對象是ContextImpl.class
,所以不論是從哪個Context的子類來獲取sp,都能保證
mSharedPrefsPaths
的線程安全.
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//1
sp = cache.get(file);//2
if (sp == null) {
checkMode(mode);//3
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 = new SharedPreferencesImpl(file, mode);//4
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();
}//5
return sp;
}
這個方法大概涉及5個比較重要的點,上面都有標注,接下來一一分析
- 首先看
getSharedPreferencesCacheLocked()
方法的實現
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
這里主要涉及兩個映射關系,一個是應用包名與sp之間的映射,因為一個應用可能創建多個sp文件來存儲不同的業務配置項
第二個是sp文件與sp實現類SharedPreferencesImpl
之間的映射關系,這個之前有提到.
值得注意的是它們使用的都是ArrayMap
而不是HashMap
,主要是因為ArrayMap
比HashMap
更省內存,這個以后單獨寫一篇.
-
通過file拿到對應的sp的實現類實例.
-
檢查操作模式,看一下實現
private void checkMode(int mode) {
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
if ((mode & MODE_WORLD_READABLE) != 0) {
throw new SecurityException("MODE_WORLD_READABLE no longer supported");
}
if ((mode & MODE_WORLD_WRITEABLE) != 0) {
throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
}
}
}
當目標sdk版本大於N的時候,如果操作模式設置為MODE_WORLD_READABLE
或MODE_WORLD_WRITEABLE
話,即允許其他應用讀寫sp的話,就會拋出
安全異常,可見Google對應用安全方面的限制越來越嚴格了.
-
創建sp的實現類的實例,並加入到緩存中,以便下次能夠快速的拿到.
-
當操作模式設置為
Context.MODE_MULTI_PROCESS
或者目標sdk版本小於3.2時,調用sp.startReloadIfChangedUnexpectedly()
void startReloadIfChangedUnexpectedly() {
synchronized (mLock) {
// TODO: wait for any pending writes to disk?
if (!hasFileChangedUnexpectedly()) {
return;
}
startLoadFromDisk();
}
}
該方法先去檢查文件狀態是否改變,如果有的話就重新讀取文件數據到內存.這里我們知道MODE_MULTI_PROCESS
是不靠譜的,它並不能支持數據
跨進程共享,只是getSharePreference
時回去檢查文件狀態是否改變,改變就重新加載數據到內存.
SharedPreferencesImpl
上面了解到getSharePreference
返回的其實是SharedPreferencesImpl
的實例,現在重點看一下SharedPreferencesImpl
的實現.
構造函數
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
都是一些常規操作,初始化一些值,創建備份文件,重點看一下startLoadFromDisk
startLoadFromDisk
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
將mLoaded
變量置為false,表示數據還沒有加載成功,然后開啟了一個線程,並調用loadFromDisk
loadFromDisk
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {//1
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);//2
} 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;//3
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();//4
}
}
-
先判斷備份文件是否存在,如果存在就刪除當前文件,將備份文件重命名為正式文件.
-
然后創建文件輸出流讀取文件內存並轉化為Map,注意這里創建帶緩存的輸出流時,指定的buffer大小為16k.可以借鑒.
-
將讀取到的Map賦值給mMap成員變量,如果map為空就創建一個空的
HashMap
,這里又是用到HashMap了,因為這里
設計頻繁查找或插入操作,而hashMap的查詢和插入操作的效率是優於ArrayMap的.
- 通知喚醒線程,有喚醒就有阻塞,看一下哪里阻塞了,全局搜索一下
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) {
}
}
}
該方法在mLoaded為false的時候一直阻塞,而之前的notifyAll喚醒的就是此處的阻塞.再看一下awaitLoadedLocked
在哪里被調用了.
public Map<String, ?> getAll() {
synchronized (mLock) {
awaitLoadedLocked();
//noinspection unchecked
return new HashMap<String, Object>(mMap);
}
}
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@Nullable
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
synchronized (mLock) {
awaitLoadedLocked();
Set<String> v = (Set<String>) mMap.get(key);
return v != null ? v : defValues;
}
}
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
public long getLong(String key, long defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
public float getFloat(String key, float defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Float v = (Float)mMap.get(key);
return v != null ? v : defValue;
}
}
public boolean getBoolean(String key, boolean defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
public boolean contains(String key) {
synchronized (mLock) {
awaitLoadedLocked();
return mMap.containsKey(key);
}
}
這里可以知道,所有的get相關方法都被阻塞,直到完成數據從文件加載到內存的過程.因此當第一次調用sp的get相關
函數時是比較慢的,需要等待數據從文件被讀取到內存,之后會比較快,因為是直接在內存中讀取.
至此,get相關方法已經分析完畢,原理也比較容易理解,接下來看看put相關方法.
edit
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 (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
調用put相關方法之前需要調用edit方法,此處也是需要等待的,返回的是EditorImpl
的實例.
EditorImpl
@GuardedBy("mLock")
private final Map<String, Object> mModified = Maps.newHashMap();
EditorImpl是SharePreferenceImpl的內部類,內部有一個HashMap保存被更改的鍵值對.
public Editor putBoolean(String key, boolean value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (mLock) {
mModified.put(key, this);
return this;
}
}
從以上兩個方法可以知道,put方法就是向mModified添加一個鍵值對,remove方法添加的value為當前editor實例.
它們都是被mLock加鎖保護的,有兩個原因
- HashMap不是線程安全的
- 需要和其他的get方法互斥
commit
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory(); //1
SharedPreferencesImpl.this.enqueueDiskWrite( //2
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;
}
- commitToMemory實現
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
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;
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();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
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;
}
}
mMap.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
顧名思義,就是把更改的鍵值對提交到內存中,即把mModified中的鍵值對更新到mMap中,順便獲取被更新
的鍵的集合以及外部設置監聽器列表(基於深拷貝)
- enqueueDiskWrite
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//判斷是同步任務還是異步任務
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);//寫到文件中
}
synchronized (mLock) {
mDiskWritesInFlight--;//寫入操作完畢,計數減一
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();//如果當前沒有正在執行的同步任務,就直接執行
return;
}
}
//異步提交任務
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
commit方法分析完畢,其實就是將最新的mMap寫入到文件中.
apply
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);
}
enqueueDiskWrite之前看過了,關鍵代碼如下
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
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);
}
}
}
首先獲取一個Handler
的實例,然后再通過Handler
發送一個消息,先看一下getHandler
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
這是一個典型的單例模式寫法,Handler
構造方法的Looper來自HandlerThread
,這是一個內部維護消息機制
的線程,任務是按照時間順序依次執行的,不了解的可以去看一下源碼.
接下來看一下handleMessage
方法實現
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
private static void processPendingWork() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
//直接循環調用runnable的run方法執行任務
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
其實到這里apply方法也基本上分析完畢,該方法是在子線程被調用的,為了線程安全考慮,使用的是HandlerThread
來依次執行寫文件任務.
但我們需要依次提交更改多個鍵值對時,只需要保留最后一個commit或apply方法既可.
跨進程的Sp
結合ContentProvider
並重寫call方法
總結
-
sp不適合存儲過大的數據,因為它一直保存在內存中,數據過大容易造成內存溢出.
-
sp並不支持跨進程,因為它不能保證更新本地數據后被另一個進程所知道,而且跨進程的操作標記已經被棄用.
-
sp的commit方法是直接在當前線程執行文件寫入操作,而apply方法是在工作線程執行文件寫入,盡可能使用apply,因為不會阻塞當前線程.
-
sp批量更改數據時,只需要保留最后一個apply即可,避免添加多余的寫文件任務.
-
每個sp存儲的鍵值對不宜過多,否則在加載文件數據到內存時會耗時過長,而阻塞sp的相關
get
或put
方法,造成ui卡頓. -
頻繁更改的配置項和不常更改的配置項應該分開為不同的sp存放,避免不必要的io操作.
感覺寫的有些亂,不過一些重要的點基本都有,如果有理解錯誤的地方還請大佬指正.