簡單使用
SharedPreferences sharedPreferences = getSharedPreferences("ouwen", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = sharedPreferences.edit();
edit.putString("ouwen","123456");
保存數(shù)據(jù):edit.apply(); 或者 edit.commit()
讀取數(shù)據(jù):sharedPreferences.getString("ouwen","");
通常查看源碼都是帶著問題去分析, 避免在源碼里面迷失了,
那么關(guān)于SharedPreferences的幾點問題:
- 怎么保存數(shù)據(jù)的?
- 怎么讀取數(shù)據(jù)的?
- 它是線程安全的嗎?
- apply和commit都可以提交數(shù)據(jù),那么有什么區(qū)別?
- 有什么需要注意的嗎?要怎么優(yōu)化?
怎么保存數(shù)據(jù)的?
先說結(jié)論再看源碼分析
本質(zhì)是把Map中的數(shù)據(jù)轉(zhuǎn)成xml文件, 以鍵值對的方式存儲在本地
- 第一步: 獲取SharedPreferences實例
- 第二步: 創(chuàng)建一個Map把我們當(dāng)前修改的數(shù)據(jù)存儲在內(nèi)存中
- 第三步: 調(diào)用commit()或者apply() 把內(nèi)存中的數(shù)據(jù)存儲到本地
getSharedPreferences()源碼
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
mBase: 是ContextImpl
傳入一個name和mode, 首先會嘗試從mSharedPrefsPaths中獲取name在本地的File, 如果為null, 就調(diào)用getSharedPreferencesPath(name) 創(chuàng)建一個name +".xml"的文件并且加入mSharedPrefsPaths緩存中, 繼續(xù)調(diào)用getSharedPreferences(file,mode)
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) {
checkMode(mode);
...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
...
return sp;
}
為了簡潔容易看, 刪除了部分代碼
- 先調(diào)用getSharedPreferencesCacheLocked()獲取File對應(yīng)的緩存cache, 這個ArrayMap是ConextImpl里的一個靜態(tài)成員屬性
- 第一次緩存是null, 所以會調(diào)用new SharedPreferencesImpl(file, mode) 創(chuàng)建一個實例加入緩存中并且返回
然后調(diào)用edit()方法獲取Editor的實例EditorImpl
@Override
public Editor edit() {
...
synchronized (mLock) {
awaitLoadedLocked(); 這里會阻塞等待,直到讀取文件完成
}
return new EditorImpl();
}
public final class EditorImpl implements Editor {
...
private final Map<String, Object> mModified = new HashMap<>();
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor putInt(String key, int value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
...
}
拿到SharedPreferences.Editor后就可以往里面putString(), putInt 等等...
哦~~~原來我們put的數(shù)據(jù)都是保存在mModified
這個Map集合里面了,
這個時候數(shù)據(jù)還是在內(nèi)存里面, 只有調(diào)用commit()或者apply()才會保存到本地文件, 那就繼續(xù)看源碼吧
先對比一下commit() 和 apply() 源碼上有什么區(qū)別?
public boolean commit() {
...
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
...
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
public void apply() {
...
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await(); 等待提交完成
} catch (InterruptedException ignored) {
}
}
};
//把這個等待提交的Runnable, 加入到QueuedWork中
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run(); 執(zhí)行上面的Runnable
//從QueuedWork中移出等待提交的Runnable
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
相同點:
- 都會調(diào)用commitToMemory(), 然后調(diào)用enqueueDiskWrite()把Map的數(shù)據(jù)寫入本地文件中
不同點:
- commit() 有返回值, 可以知道成功還是失敗
- apply() 沒有返回值, 并且多了2個Runnable
commitToMemory()方法會遍歷之前put數(shù)據(jù)的mModified
這個Map, 把我們修改的數(shù)據(jù)同步到另外一個Map中, 并且清空mModified自己
enqueueDiskWrite()
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
commit()方法調(diào)用enqueueDiskWrite的時候, 這個postWriteRunnable為空
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run(); 這里會在主線程執(zhí)行, 寫入本地文件
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
創(chuàng)建一個writeToDiskRunnable里面會執(zhí)行當(dāng)writeToFile()把數(shù)據(jù)寫入文件
調(diào)用commit的時候, isFromSyncCommit = true, 所以會當(dāng)前線程執(zhí)行
調(diào)用apply的時候會把寫入文件的Runnable加入QueuedWork.queue()中執(zhí)行, 其實是把Runnable發(fā)送到HandlerThread的子線程中執(zhí)行
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);
}
}
}
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;
}
}
怎么讀取數(shù)據(jù)的?
我們的程序啟動后,當(dāng)在某個地方想從SharedPreferences 里面獲取數(shù)據(jù)的時候
會調(diào)用下面代碼:
SharedPreferences sharedPreferences = getSharedPreferences("name", Context.MODE_PRIVATE);
sharedPreferences.getString(key,"");
- 獲取SharedPreferences實例
- 調(diào)用getString(key,"") 方法獲取數(shù)據(jù)
第一次調(diào)用的時候, 會調(diào)用new SharedPreferencesImpl()來創(chuàng)建SharedPreferences 的實例, 也就是在SharedPreferencesImpl的構(gòu)造方法里, 會從本地的xml文件加載數(shù)據(jù)到內(nèi)存
SharedPreferencesImpl(File file, int 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;
}
...
}
try {
...
BufferedInputStream str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
try {
if (thrown == null) {
if (map != null) {
mMap = map;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll(); 加載完成, 通知正在等待的代碼繼續(xù)執(zhí)行
}
}
}
startLoadFromDisk()方法會啟動一個線程, 然后異步調(diào)用loadFromDisk()把xml文件的內(nèi)容加載到內(nèi)存Map中
思考: new Thread? 為啥不用線程池呢?
其實不是所有需要創(chuàng)建線程都用線程池來實現(xiàn), 當(dāng)我們確認(rèn)了調(diào)用的次數(shù)很少的時候,直接new Thread也是可以的
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked(); 阻塞等待加載文件完成
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
思考: 讀取本地文件是異步的, 那么如果保證數(shù)據(jù)正確?
在getString()方法中可以看到, 先會調(diào)用awaitLoadedLocked(), 再從mMap中根據(jù)key獲取value. 這個awaitLoadedLocked()會判斷當(dāng)前文件是否下載完成, 如果沒有完成就阻塞等待; 上面異步加載文件的loadFromDisk()方法, 在加載完成后會調(diào)用mLock.notifyAll()通知這里繼續(xù)執(zhí)行
private void awaitLoadedLocked() {
...
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
有什么需要注意的嗎? 要怎么優(yōu)化?
SP表示SharedPreferences
1.apply()也有可能導(dǎo)致卡頓甚至ANR
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await(); 等待提交完成
} catch (InterruptedException ignored) {
}
}
};
//把這個等待提交的Runnable, 加入到QueuedWork中
QueuedWork.addFinisher(awaitCommit);
apply()雖然在子線程寫入文件, 但是在提交的時候會把a(bǔ)waitCommit加入到QueuedWork中, 并且在ActivityThread.handlePauseActivity(), ActivityThread.handleStopActivity(), ActivityThread.handleStopService(), ActivityThread.handleSleeping() 等等多處地方都會調(diào)用QueuedWork.waitToFinish()去阻塞等待SP保存數(shù)據(jù)完成, 也就是說如果apply()執(zhí)行寫入文件, Activity會等待它執(zhí)行完成才能關(guān)閉頁面...
更多關(guān)于 apply() 的問題, 可以看一下字節(jié)跳動技術(shù)團(tuán)隊的這篇文章
2.不要保存比較大的內(nèi)容, 會導(dǎo)致卡頓
因為SP加載的key和value以及put的內(nèi)容, 會一直保存在內(nèi)存當(dāng)中,占有內(nèi)存
第一次從SP中獲取數(shù)據(jù)的時候, 如果文件過大可能會導(dǎo)致卡頓,雖然讀取文件是在子線程中執(zhí)行, 但是看getString的源碼知道, 會調(diào)用awaitLoadedLocked阻塞在當(dāng)前線程等讀取文件完成
優(yōu)化: 可以提前在子線程先初始化SP, 過一段時間再去getString()獲取數(shù)據(jù)
3.不相關(guān)的配置, 不要保存在同一個文件中
因為文件越大加載越慢, 從而導(dǎo)致保存在內(nèi)存的數(shù)據(jù)越多, 如果有的配置用的比較頻繁,有的又很少用到,那分開文件存儲可以提高一點性能(文件比較小可以忽略)
4.避免頻繁提交, 盡量批量修改一起提交
因為寫入本地文件的時候會加鎖, 并且可能阻塞線程, 所以要避免頻繁提交,提交使用apply()
5.最好不要用來跨進(jìn)程使用
SP無法保證跨進(jìn)程使用時數(shù)據(jù)正常
如果項目對性能要求比較高, 或者想要跨進(jìn)程使用
可以看下微信的MMKV, 使用也很簡單, 聽說性能高,穩(wěn)定性強(qiáng)(暫時沒用過= =)
微信MMKV項目地址
小結(jié):
怎么保存數(shù)據(jù)的?
修改的數(shù)據(jù)會存儲內(nèi)存Map中,當(dāng)調(diào)用commit或者apply,就把數(shù)據(jù)寫入本地文件怎么讀取數(shù)據(jù)的?
第一次會創(chuàng)建子線程從本地文件讀取到內(nèi)存Map中, 所以第一次稍微慢一點, 后面的都在內(nèi)存操作所以比較快它是線程安全的嗎?
是線程安全的, 因為所有的讀和寫操作都加了synchronized,保證線程安全apply和commit都可以提交數(shù)據(jù),有什么區(qū)別?
commit有返回值,可以馬上知道提交結(jié)果, 但是commit是在當(dāng)前線程執(zhí)行寫入文件操作的
apply沒有返回值, 是在HandlerThread的子線程中寫入到文件