問:SharedPreferences 存儲比較大或者比較讀的鍵值對會有什么問題嗎?為什么?
答:SharedPreferences 是一種輕量級的存儲方式,之所以輕量級是由其設(shè)計所決定的,因為 SharedPreferences 在創(chuàng)建的時候會把整個文件全部加載進內(nèi)存,所以如果 SharedPreferences 文件比較大就會帶來如下一些性能問題:
首次從 SharedPreferences 獲取值時可能阻塞主線程從而使 UI 界面卡頓丟幀。
解析 SharedPreferences 時會產(chǎn)生大量的臨時對象而導(dǎo)致頻繁 GC 使得 UI 界面卡頓丟幀。
被解析的同一個 SharedPreferences 文件的內(nèi)容會占用大量內(nèi)存。
問:如何優(yōu)雅的使用 SharedPreferences 存儲?你有哪些經(jīng)驗?
答:
SharedPreferences 中不要存放大的鍵值對,因為會占用太多內(nèi)存、引起 UI 卡頓及內(nèi)存抖動等問題。
在進行 SharedPreferences 數(shù)據(jù)持久時應(yīng)該對持久化數(shù)據(jù)進行分類,多 SharedPreferences 文件存儲(譬如同一功能相關(guān)的放一個文件,或者按照讀寫頻率及大小進行文件拆分),因為文件越大讀取越慢,所以分類存儲相對會好很多。
對于頻繁修改盡量做到批量一次性提交,盡量不要多次 edit 和 commit 或者 apply。
不要直接用其進行跨進程讀寫操作,因為 SharedPreferences 不是進程安全的(MODE_MULTI_PROCESS 標記只是保證了在 API 11 以后如果內(nèi)存中已經(jīng)存在該 SharedPreference 則重新讀一次文件到內(nèi)存而已),如果要進行跨進程讀寫保證進程并發(fā)安全則建議使用 ContentProvider 對 SharedPreferences 進行包裝或者采用其他 AIDL 等方式存儲實現(xiàn)。
問:SharedPreferences 的加載實現(xiàn)是在子線程,但是為什么說 getX(key) 操作可能還會阻塞主線程呢?
答:看過 SharedPreferences 實現(xiàn)源碼的小伙伴一定知道怎么回事了,下面給出關(guān)鍵部分源碼分析。
//getSharedPreferences 獲取 SP 的核心源碼
class ContextImpl extends Context {
//Context會把對應(yīng) SP 緩存起來
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
......
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
......
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//第一次或者新進程中時緩存沒有就新 new 一個對應(yīng)實例
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
......
return sp;
}
......
}
接下來看看 SharedPreferencesImpl 實例化及通過 SharedPreferences 獲取一個指定 key 的值的核心源碼部分,如下:
//一個File對應(yīng)的一個SP實例
final class SharedPreferencesImpl implements SharedPreferences {
//用來存儲該SP的所有Key-Value對
private Map<String, Object> mMap;
SharedPreferencesImpl(File file, int mode) {
......
//構(gòu)造方法從磁盤加載SP文件
startLoadFromDisk();
}
private void startLoadFromDisk() {
......
//啟動一個名字為SharedPreferencesImpl-load的線程從磁盤讀文件
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
//子線程中讀取文件解析成key-value對Map
private void loadFromDisk() {
......
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;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
//子線程讀取SP文件到mMap成功后通過notify通知釋放阻塞鎖
mLock.notifyAll();
}
}
//通過SharedPreferences對象獲取指定key的值
//(一般與SharedPreferences獲取在一個線程,主線程)
public int getInt(String key, int defValue) {
synchronized (mLock) {
//阻塞等待SP讀取到內(nèi)存后再get
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
......
while (!mLoaded) {
try {
//阻塞等待SP讀取到內(nèi)存完成
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
......
}
從上面代碼可以看出,SharedPreferences 讀取 Map 雖然在子線程,但是其 getX(key) 系列方法想要調(diào)用的前提是 SharedPreferences 子線程已經(jīng)讀取完成,否則就會阻塞,所以 SharedPreferences 中如果存儲太大內(nèi)容或者太多內(nèi)容導(dǎo)致 XML 解析等變慢就會導(dǎo)致后面的 getX(key) 阻塞主線程,從而導(dǎo)致主線程卡頓,所以說 SharedPreferences 是輕量級的持久化工具。
因此首次使用一個指定 File 的 SharedPreferences 時最好先盡可能早的調(diào)用 getSharedPreferences() 獲取 SharedPreferences 實例,然后在用的時候調(diào)用其 getX(key) 方法,這樣就盡可能的避免了潛在的阻塞問題,不過這個建議不是絕對的,如果你的 SharedPreferences 比較小或者這種建議會破壞你代碼的組織性,則可以忽略。
問:SharedPreferences 為什么存儲大數(shù)據(jù)就比較占用內(nèi)存?
答:通過上面一題源碼的分析你會發(fā)現(xiàn) SharedPreferencesImpl 會將 File 文件在子線程中全部加載到一個內(nèi)存的 Map 中,而 SharedPreferences 對象又會被 ContextImpl 的 static Map 進程 Cache 操作,所以 SharedPreferencesImpl 就相當于是一個單例存儲的,故而其 Map 在內(nèi)存中就會持續(xù)存在,即便使用 Editor 進行修改后 commit 操作實質(zhì)也是對 SharedPreferencesImpl 中 Map 進行對應(yīng)操作。
//一個SharedPreferences的get操作與Editor操作對應(yīng)的內(nèi)存Map操作原理
final class SharedPreferencesImpl implements SharedPreferences {
//用來存儲該SP的所有Key-Value對
private Map<String, Object> mMap;
......
SharedPreferencesImpl(File file, int mode) {
......
//構(gòu)造方法新起一個線程從磁盤加載SP文件解析XML到mMap中
startLoadFromDisk();
}
......
//通過SharedPreferences對象獲取指定key的值
public int getInt(String key, int defValue) {
......
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
......
//通過SharedPreferences.edit()獲取的Editor對象
public final class EditorImpl implements Editor {
//Editor要增刪改查的操作Map記錄
private final Map<String, Object> mModified = Maps.newHashMap();
......
public Editor putString(String key, @Nullable String value) {
......
mModified.put(key, value);
}
......
public Editor remove(String key) {
......
mModified.put(key, this);
}
......
//提交到內(nèi)存mMap
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
......
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
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);
}
}
//清空Editor的mModified的Map
mModified.clear();
......
}
......
//commit提交
public boolean commit() {
......
//提交到內(nèi)存
MemoryCommitResult mcr = commitToMemory();
//提交到磁盤文件
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
......
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
......
}
}
你可以理解成一個 SharedPreferencesFile 對應(yīng)一個單例的 SharedPreferencesImpl 對象,這個單例的 SharedPreferencesImpl 實例化時會將文件全部加載到內(nèi)存中以 Map 存儲,然后 Editor 操作會 new 一個新的 modifyMap,接著到了 Editor 的 commit 操作時會將 modifyMap 進行遍歷增刪改查到 SharedPreferencesImpl 的 Map 中,然后進行存盤操作。
而 SharedPreferencesImpl 進行 getX(key) 操作時都是直接從 SharedPreferencesImpl 的 Map 中進行讀取的,所以說 SharedPreferences 只適合輕量級的數(shù)據(jù)存儲操作。