Android輕量級儲存原始碼分析
Android提供了輕量級的資料儲存方式,那就是SharePreference資料儲存。其實質也就是檔案儲存,只不過是符合XML標準的檔案儲存而已,是Android中比較常用的簡易型資料儲存解決方案。下面簡單分析下原始碼實現,原始碼都是一些獨立的東西,實現也比較符合大家的編碼習慣,相比其他模組原始碼比較簡單一些
留個小問題:
1,這樣儲存基本型別資料有問題嗎?
SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE); sp.edit().putString("name", "小張"); sp.edit().putInt("age", 11); sp.edit().commit();
2,這樣儲存基本基本型別資料有什麼不好的地方?
SharedPreferences sp = getSharedPreferences("sp_demo", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.editor();
editor.putString("name", "小張");
editor.commit();
//SharedPreferences.Editor editor = sp.editor();
editor.putInt("age", 11);
editor.commit();
先從獲取SharedPreference得入口getSharedPreferences(String name, int mode)開始
@Override public SharedPreferences getSharedPreferences(String name, int mode) { (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; } } File file; synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } //欄位儲存在xml檔案中,而xml檔案是放在ArrayMap<String, File>中 file = mSharedPrefsPaths.get(name); if (file == null) { file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode); }
原來我們的sp物件放在一個ArrayMap< String, File> mSharedPrefsPaths,看來name--File進行了關聯,看下這個getSharedPreferencesPath(String name)到底幹了啥
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
原來這貨就是建立xml檔案,結合getSharedPreferences(String name, int mode),那麼可以理解在第一次儲存資料放在某個xml時候,如果沒有xml檔案,去建立這個名稱的xml檔案,那麼我們繼續往下看如何獲取SharedPreference物件
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
return sp;
}
要看sp物件盡然還要進一層,為啥這麼艱難,nm
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;
}
原來,這傢伙是把File物件和SharedPreferencesImpl關聯到一塊了,存在一個ArrayMap中,那麼我們小結一下,我們命名一個fileName的xml檔案,那麼會生成一個File,所以有了fileName-File物件關聯在一塊,緊接著,File--SharedPreferencesImpl關聯在一塊,那麼我猜測最終所有的儲存邏輯是在SharedPreferencesImpl這個實現類中實現,請繼續看
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
這個是Editor是SharedPreferencesImpl的內部類,是一個介面,0.001s看下暴露的方法
//Editor編輯介面提供的方法概覽
public interface Editor {
Editor putString(String key, String value);
Editor putStringSet(String key, Set<String> values);
Editor putInt(String key, int value);
Editor putLong(String key, long value);
Editor putFloat(String key, float value);
Editor putBoolean(String key, boolean value);
Editor remove(String key);
Editor clear();
boolean commit();
void apply();
}
public final class EditorImpl implements Editor {
//mModified對的是一個String--Object的Map,其實就是儲存所有put過來的key--value,放在記憶體中,記憶體中,記憶體中(重要事情說三遍)
//突然間我發現getXXX時候可以從mModified裡面取,不用讀取檔案了,那麼每一回儲存檔案都會呼叫sp.editor(),進而每次都會new EditorImpl(),所以有很多個 mModified這樣的map,有這麼一瞬間我覺得很坑爹,這裡問題留在這
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
//舉例分析儲存boolean型別,其他的意淫一下
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;
}
}
public Editor clear() {
synchronized (mLock) {
mClear = true;
return this;
}
}
//你們要的commit在這
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;
} finally {
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
//寫記憶體的這貨在這,看他幹啥了
private MemoryCommitResult commitToMemory() {
//從這貨名字可以猜處它最後應該是要寫到記憶體裡
Map<String, Object> mapToWriteToDisk;
//這又出來個mMap,好亂,猜一下m開頭,成員變數,沒錯,這個就是你以後每次getXXX(String key)時候要從記憶體中操作的map,你app所有儲存的東西都在這個成員變數裡
mapToWriteToDisk = mMap;//結果賦給這個馬甲map,這個馬甲map待會肯定要去變成xml檔案內容存到手機裡去
synchronized (mLock) {
boolean changesMade = false;
//mClear好眼熟,成員變數,還記得clear()方法裡對mClear進行賦值嗎?.....原來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();
//map.remove應該是你呼叫了editor.remove()方法以後要進行最終結果map的remove,這裡為啥是value == this就給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;
}
}
//大胸弟我終於看到我曾經跨過山河大海put的值,你現在終於放進這個mMap裡了
mMap.put(k, v);
}
if (hasListeners) {
keysModified.add(k);
}
}
//本次commit臨時儲存用的map可以皈依佛門了
mModified.clear();
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
//我想要的東西都很貴,我想要去的地方都很遠,不造一個包裝資料結構的類怎麼把他們打包帶走
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
//小姐姐長啥樣,一睹芳容
private static class MemoryCommitResult {
final long memoryStateGeneration;
@Nullable final List<String> keysModified;
@Nullable final Set<OnSharedPreferenceChangeListener> listeners;
final Map<String, Object> mapToWriteToDisk;
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
@GuardedBy("mWritingToDiskLock")
volatile boolean writeToDiskResult = false;
boolean wasWritten = false;
private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
@Nullable Set<OnSharedPreferenceChangeListener> listeners,
Map<String, Object> mapToWriteToDisk) {
this.memoryStateGeneration = memoryStateGeneration;
this.keysModified = keysModified;
this.listeners = listeners;
this.mapToWriteToDisk = mapToWriteToDisk;
}
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
//原來就放了幾個成員變數,盛放下打包的物品,太醜陋了,這不是去小姐姐家的路
//存完記憶體中的map以後,俺們看看如何存到disk裡
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
//原來這貨根據runnabl == null就是comit,不是就是apply模式
final boolean isFromSyncCommit=(postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
//最終寫檔案到disk的操作在這
writeToFile(mcr,isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//確認過眼神,這是commit模式
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//commit模式下,當前只有一個寫入任務,直接在當前執行緒write
writeToDiskRunnable.run();
return;
}
}
//apply模式 || 當前是commit模式,但是有很多個寫入任務,原來這麼坑爹commit在任務多的時候也會apply,為啥嘞?要是胡寫亂呼叫commit,阻塞了UI執行緒那不就掛了(這是我猜測)
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
public void apply() {
//和commit一樣,三七二十一先給它在記憶體裡的mMap操作完再說,再處理打包回來的馬甲map
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
//這個包裝的資料結構的writtenToDiskLatch到底是啥勒,他是一個標誌位,表示當前mac這個檔案是否寫入到disk完成,如果完成在繼續下一個mc資料結構寫入,那麼這貨到底是在什麼時候賦值的呢? mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//這個上面commit已經分析過,runnable !=null,新執行緒寫檔案
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
到這裡我們知道了為啥apply和commit一個非同步一個同步了,以及我們getXX是從mMap這個成員變數中獲取的了,那麼萬水千山還有一步,說了半天到底怎麼從把mMap的馬甲Map這個mWriteToDiskMap,所在的包裝類MemoryCommitResult,這個破玩意如何變成xml,再把xml寫入到disk裡呢?最後一個機關來了
//我靠好長啊
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
//mFile是個成員變數,是當初傳過來的File檔案,判空是因為有地方會有mFile.delete()操作
boolean fileExists = mFile.exists();
if (fileExists) {
boolean needsWrite = false;
//mDiskStateGeneration是一個標誌位,在構造資料包裝結構MemoryCommitResult的時候賦值過
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
if (!backupFileExists) {
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
//把mMap的馬甲Map結果集寫入到mFile檔案,mFile是啥,waht?,當初你在ContextImpl裡getSharedPreference(String name,Mode mode)傳入一個name,生成一個mFile,然後new SharedPreferenceImpl(File file,Mode mode),這個mFile就來了,如今將結果map寫入到mFile裡,木毛病
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
//將xml檔案最終寫入到disk裡
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
mBackupFile.delete();
mDiskStateGeneration = mcr.memoryStateGeneration;
mcr.setDiskWriteResult(true, true); mSyncTimes.add(Long.valueOf(fsyncDuration).intValue());
mNumSync++;
return;
} catch (XmlPullParserException e) {
} catch (IOException e) {
}
if (mFile.exists()) {
if (!mFile.delete()) {
}
}
mcr.setDiskWriteResult(false, false);
}
小結
1,SharedPreference儲存資料的形式是xml檔案,並且建立時不同的name對應不同的xml檔案,本質是檔案讀寫。
2,在SharedPreferences的Editor中如果用commit()方法提交資料,其過程是先把資料更新到記憶體,然後在當前執行緒中寫檔案操作,如果用的是apply()方法提交資料,首先也是寫到記憶體,接著在一個新執行緒中非同步寫檔案,注意:操作commit時有鎖操作,所以效率很低一些,如果當我們一次有多個修改寫操作時等都批量put完了再一次提交確認,這樣可以提高效率
3,SharedPreferences在例項化時首先會從disk非同步讀檔案,然後快取在記憶體中,接下來的讀操作都是記憶體快取操作而不是檔案操作。
4,由於鍵值對在記憶體中儲存了一份,放在mMap中,用記憶體換取getXXX(String key)時候速度上的提高,所以如果儲存大量資料在裡面,那麼這個map將會很大,記憶體上存在問題,所以回到題目上來,輕量級儲存,不適合大而複雜的資料儲存