一,如何使用?
先從簡(jiǎn)單的使用示例開始
寫入數(shù)據(jù)
SharedPreferences sharedPreferences = this.getSharedPreferences("settings", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean("isOpen", true);
editor.putString("name", "coder");
editor.putInt("number", 10);
editor.commit();
讀取數(shù)據(jù)
boolean isOpen = sharedPreferences.getBoolean("isOpen", false);
String name = sharedPreferences.getString("name", "");
int years = sharedPreferences.getInt("years", 0);
使用很簡(jiǎn)單螟够,接下來我們一句一句的來分析源碼實(shí)現(xiàn)。
二, 源碼分析
1. 獲取 SharedPreferences
/frameworks/base/core/java/android/content/ContextWrapper.java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
mode可以選以下四種妓笙,:
MODE_PRIVATE 默認(rèn)模式若河,創(chuàng)建的文件只能在應(yīng)用內(nèi)訪問(或者共享相同userID的所有應(yīng)用)
MODE_WORLD_READABLE(過時(shí))允許其他應(yīng)用訪問本應(yīng)用的文件,使用此模式會(huì)拋出異常
MODE_WORLD_WRITEABLE(過時(shí))允許其他應(yīng)用寫本應(yīng)用的文件寞宫,使用此模式會(huì)拋出異常
MODE_MULTI_PROCESS(過時(shí))官方提示這種模式在某些版本無法可靠運(yùn)行萧福,并且未來也不會(huì)支持多進(jìn)程
但目前除了第一種其他的都不建議使用,并且從 Android N 開始MODE_WORLD_READABLE辈赋, MODE_WORLD_WRITEABLE會(huì)直接拋出異常鲫忍,后面我們?cè)诜治鲈创a的時(shí)候會(huì)詳細(xì)說明這塊;
上面 mBase 是 Context 類型的實(shí)例钥屈,Context是一個(gè)抽象類悟民,它有兩個(gè)直接子類,一個(gè)是ContextWrapper篷就,一個(gè)是ContextImpl射亏,ContextWrapper 是上下文功能的封裝類,而ContextImpl則是上下文功能的實(shí)現(xiàn)類竭业, Activity智润,Service, Application都是繼承自ContextWrapper的永品,因此這里的mBase其實(shí)最終指向的就是 ContextImpl做鹰;
ContextImpl 是何時(shí)創(chuàng)建的?
ContextImpl 是主線程ActivityThread 的成員變量鼎姐,ActivityThread 是管理應(yīng)用進(jìn)程的主線程的執(zhí)行钾麸,ActivityThread 是在App冷啟動(dòng)main(String[] args)中初始化的,說明ActivityThread只有一個(gè)炕桨,從而對(duì)應(yīng)一個(gè)ContextImpl 饭尝,分析這個(gè)有利于我們接下來分析SharedPreferences 的一些代碼;
public ContextImpl getSystemContext() {
synchronized (this) {
if (mSystemContext == null) {
mSystemContext = ContextImpl.createSystemContext(this);
}
return mSystemContext;
}
}
static ContextImpl createSystemContext(ActivityThread mainThread) {
LoadedApk packageInfo = new LoadedApk(mainThread);
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0, null, null);
context.setResources(packageInfo.getResources());
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
context.mResourcesManager.getDisplayMetrics());
return context;
}
/frameworks/base/core/java/android/app/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);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
name 傳 null 是被允許的献宫,它會(huì)生成一個(gè)以null.xml為名的文件, 我用targetSdkVersion為29钥平,在Android 10的機(jī)器上也驗(yàn)證了確實(shí)是可以的:
/data/user/0/com.cjl.loadingview/shared_prefs/null.xml
我們知道一個(gè)App可以對(duì)SharedPreferences設(shè)置不同name,這樣最終也就對(duì)應(yīng)著不同的xml文件姊途,mSharedPrefsPaths 是一個(gè)map涉瘾,它就是用來保存不同name的文件的;如果mSharedPrefsPaths
里沒有該name對(duì)應(yīng)的文件捷兰,那么就 通過getSharedPreferencesPath(name)獲取一個(gè),代碼如下:
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
上述代碼最終會(huì)生成一個(gè)下面路徑的.xml文件:
/data/user/0/com.cjl.loadingview/shared_prefs/settings.xml
/data/user/0 是一個(gè) /data/data 的 link立叛,
@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) {
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 = 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;
}
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;
}
sSharedPrefsCache 存儲(chǔ)不同packageName 的 SharedPreferencesImpl, 冷啟動(dòng)進(jìn)來SharedPreferencesImpl是為null的贡茅,因此回去新建一個(gè)秘蛇,在新建之前會(huì)檢查最初傳進(jìn)來的mode其做,從如下代碼可以看到Android N 后已強(qiáng)制不能在使用 MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE 赁还。
如果sp不為null妖泄,mode 是 多進(jìn)程模式 MODE_MULTI_PROCESS, 此時(shí)需要重新讀取文件艘策;
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");
}
}
}
我們來看看SharedPreferencesImpl 到底是什么蹈胡?
final class SharedPreferencesImpl implements SharedPreferences {
···
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
SharedPreferences 是一個(gè)接口類型的,SharedPreferencesImpl 是它的實(shí)現(xiàn)類柬焕,因此SharedPreferencesImpl 才是我們分析的重點(diǎn)审残,這里面有SharedPreferences 各種操作的具體實(shí)現(xiàn):
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
創(chuàng)建.bak備份文件,接下來會(huì)開啟一個(gè)名為 SharedPreferencesImpl-load
的子線程從磁盤讀取文件斑举,并且讀取到文件后立即將其轉(zhuǎn)換成了map文件保存在內(nèi)存中搅轿,為什么轉(zhuǎn)換成map保存在內(nèi)存中呢,這里留一個(gè)伏筆富玷,看到后面你自然會(huì)明白璧坟;
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
Map<String, Object> map = null;
StructStat stat = null;
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
}
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
}
mLoaded 這個(gè)成員變量得留意一下,在首次讀取完磁盤文件后赎懦,下次調(diào)用getSharedPreferences就不會(huì)再從磁盤讀取了雀鹃;
2. 寫入數(shù)據(jù)
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("name", "coder");
editor.commit();
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
private void awaitLoadedLocked() {
...
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {}
}
...
}
寫入數(shù)據(jù)需要使用Editor 實(shí)例,Editor 是一個(gè)接口励两,它的實(shí)現(xiàn)類是EditorImpl黎茎,在獲取Editor 之前如果mLock鎖沒有被釋放則會(huì)處于等待狀態(tài), 等待什么呢,從上面分析我們不難看出其實(shí)是在等待getSharedPreferences時(shí)從磁盤中讀取文件当悔,如果文件都沒有讀取完成傅瞻,我們拿到Editor 去寫數(shù)據(jù)肯定是不行的,加載成功后的 notifyAll 要結(jié)合 awaitLoadedLocked 來分析盲憎。在準(zhǔn)備讀嗅骄、寫 SP 的時(shí)候,都會(huì)先調(diào)用 awaitLoadedLocked 等待 loadFromDisk loadFromDisk 饼疙,在讀取磁盤文件結(jié)束后會(huì)調(diào)用mLock.notifyAll()
喚醒這些等待數(shù)據(jù)加載完成的線程溺森,接下來我們就可以去獲取EditorImpl去寫文件了
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
private final Map<String, Object> mModified = new HashMap<>();
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
mcr.writtenToDiskLatch.await();
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
...
}
寫入操作大致可分為兩步完成:
第一步是用EditorImpl直接更改內(nèi)存mMap的值;
第二步是將內(nèi)存mMap中的鍵值對(duì)寫入到磁盤文件中窑眯;
我們putString為例來分析下寫文件的操作屏积,在調(diào)用完putString之后我們必須要調(diào)用commit()或者apply()去保存數(shù)據(jù);
這兩個(gè)方法都會(huì)調(diào)用commitToMemory()將數(shù)據(jù)寫入內(nèi)存map磅甩,接著都會(huì)add一個(gè)寫入文件的任務(wù)炊林,等待后續(xù)系統(tǒng)執(zhí)行
commit()或者apply()不同的地方在于:
apply將文件寫入操作放到一個(gè)Runnable對(duì)象中,等待系統(tǒng)在子線程中調(diào)用更胖, 此時(shí)不會(huì)阻礙主線程;
commit 是直接在主線程中同步進(jìn)行寫入操作, 因此使用commit是會(huì)阻塞主線程的却妨,這點(diǎn)得注意饵逐;
關(guān)于上述不同點(diǎn)可以詳細(xì)追一下enqueueDiskWrite()方法, 如果是apply()方法會(huì)使用writeToDiskRunnable , commit會(huì)在主線程寫入
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable 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();
}
}
};
// 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);
}
3. 數(shù)據(jù)的讀取
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}