Sharedpreferences調(diào)用commit()方法時(shí)寫入文件一定是同步的嗎奈惑?

大家都知道apply()時(shí)寫文件不一定是異步的,極端情況比如activity#stop()時(shí)候有可能同步碘裕。那么標(biāo)題中的問題呢 携取?
先說答案:不一定,在極端情況下帮孔,調(diào)用commit()方法之后雷滋,寫入文件也有可能運(yùn)行在異步線程。

極端情況是這樣的場景:put較大的value后調(diào)用apply()寫入文件文兢,然后立即put新值立即調(diào)用commit()晤斩。
此次的commit()寫入文件就有可能是在異步線程中。

答案的關(guān)鍵就在成員變量mDiskWritesInFlight上姆坚,而且還發(fā)現(xiàn)了這種極端情況澳泵,導(dǎo)致的apply()數(shù)據(jù)丟失問題(不是多進(jìn)程場景)。apply()數(shù)據(jù)丟失兼呵,去年我們團(tuán)隊(duì)和我自己都發(fā)現(xiàn)了兔辅,當(dāng)時(shí)太忙沒找到原因,這次終于找到了击喂。

下面從簡介開始一步步分析维苔。

一 簡介和用法

  1. 簡介
    sharedpreferences簡稱SP,主要用來存儲(chǔ)輕量級的key-value數(shù)據(jù)到本地文件中懂昂,key必須是String類型介时,value是int float boolean long String Set<String>這6種類型。假如我的應(yīng)用包名是your.Package.Name, 存儲(chǔ)的SharedPreferences文件名是yourFileName沸柔,那么文件文件路徑就是/data/data/your.Package.Name/SharedPreferences/yourFileName.xml
  2. 用法
//1 獲取對象
SharedPreference sp = context.getSharedPreferences("yourFileName", Context.MODE_PRIVATE);
SharedPreferences.Editor edit = sp.edit();
//2 存放key對應(yīng)的value值
edit.putString("name", "yangquan").putInt("age", 108);
edit .commit() //or edit.apply();
//3 獲取
String name = sp.getSting("name","");
int age = sp.getInt("age", 0);

二 Sharedpreferences和SharedpreferencesImpl加載過程

獲取SharedPreferences對象的方法是context.getSharedPreferences("yourFileName", Context.MODE_PRIVATE)
所以先看Context.java循衰,Context.java是一個(gè)抽象類,真正的實(shí)現(xiàn)在ContextImpl.java里面

1. 先說明一下ContextImpl.java與SharedPreferencesImpl.java中的部分成員變量

  1. ContextImpl.class中的成員:
 //緩存SP文件名和對應(yīng)的文件
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths; 

//緩存SP文件和對應(yīng)的SPI褐澎,**key是包名**会钝。value也是一個(gè)map,此map的key是文件工三,value就是SPI顽素。
@GuardedBy("ContextImpl.class")
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache; 
  1. EditorImpl.class中相關(guān)成員:
private final Map<String, Object> mModified = new HashMap<>();//緩存新寫入的鍵值對
private final Object mEditorLock = new Object();  //緩存鎖,即寫鎖
private boolean mClear = false;  //是否調(diào)用了clear方法
  1. SharedPreferencesImpl.class中相關(guān)的成員:
private final File mFile;  //SP文件徒蟆,即sSharedPrefsCache的key
private final File mBackupFile;  //mFile的備份文件
private final int mMode;  //模式,私有的型型、公共寫段审、公共讀、多線程(目前已經(jīng)不支持)等
private final Object mLock = new Object();  //讀鎖(不止讀的時(shí)候用到闹蒜,具體可以看源碼)寺枉,同時(shí)也讓主線程等待mLock.awate(),并在子線程中通知喚醒主線程mLock.notifyAll()
private final Object mWritingToDiskLock = new Object();  //內(nèi)存寫磁盤鎖
private Map<String, Object> mMap;//存儲(chǔ)從文件加載的鍵值對绷落,和從緩存中拷貝過來的新鍵值對姥闪。
private int mDiskWritesInFlight = 0;//準(zhǔn)備寫入文件而還沒完成的次數(shù)

2. 獲取SharedPreferencesImpl過程

  1. context.getSharedPreferences("yourFileName", Context.MODE_PRIVATE)時(shí)會(huì)從緩存mSharedPrefsPaths中,嘗試獲取文件名為yourFileName的文件砌烁,如果沒有獲取到則創(chuàng)建文件并存入mSharedPrefsPaths中筐喳,(如果獲取到了直接用此文件做key)
  2. 然后獲取當(dāng)前應(yīng)用的包名,用包名為key獲取 sSharedPrefsCache中對應(yīng)的value即ArrayMap<File, SharedPreferencesImpl> packagePrefs函喉,然后用第一步中獲取到的文件名為key避归,獲取packagePrefs中的value值即SharedPreferencesImpl;如果獲取到了直接返回管呵,
  3. 否則創(chuàng)建SharedPreferencesImpl對象并初始化

3. SharedPreferencesImpl初始化過程

  1. 在SharedPreferencesImpl的構(gòu)造函數(shù)中梳毙,首先會(huì)給mFile,mMode 等賦值捐下,并將 SharedPreferences文件名后面加上.bak后綴账锹,作為文件名創(chuàng)File(注意并未調(diào)用createFile,所以并未在磁盤真正創(chuàng)建文件)坷襟。然后開始從文件中加載鍵值對到mMap中奸柬。
  2. 開啟一個(gè)名為 SharedPreferencesImpl-load 的線程,開始從文件中加載鍵值對到mMap中啤握。
  3. 通過標(biāo)志位mLoaded判斷是否已經(jīng)加載成功鸟缕,加載成功了就不用再重復(fù)加載了。
  4. 判斷備份文件是否存在,如果存在懂从,則刪除原文件授段,將備份文件重命名為原文件。
  5. 從文件中讀取鍵值對番甩,并賦值給mMap侵贵。
  6. 調(diào)用mLock.notifyAll() 通知喚醒,其他被mLock鎖住的線程缘薛。

SharedPreferencesImpl.java中有一個(gè)方法awaitLoadedLocked()窍育,所有的讀取(例如getString()之類的)和獲取Editor(也就是 SharedPreferences#editor())方法里面宴胧,第一行都是調(diào)用這方法漱抓,而這個(gè)方法在文件加載到內(nèi)存中時(shí),會(huì)一直阻塞(通過mLock.wait())恕齐,直到文件加載完成(調(diào)用mLock.notifyAll()喚醒)乞娄,讀取和獲取Editor方法才會(huì)繼續(xù)執(zhí)行

從上面的分析可見显歧,通過context.getSharedPreferences方法獲取的同文件名仪或,都是同一個(gè)SPI對象。
所以在同一個(gè)進(jìn)程中士骤,無論從哪一個(gè)對象和類中獲取相同名稱的sp文件范删,其實(shí)訪問的都是同一個(gè)SharedPreferencesImp對象。

在進(jìn)程第一次調(diào)用context.getSharedPreferences("yourFileName", Context.MODE_PRIVATE)方法時(shí)拷肌,會(huì)將yourFileName.xml文件中的所有內(nèi)容讀取到內(nèi)存中到旦,注意這個(gè)過程是阻塞的。雖然讀取文件內(nèi)容是在子線程廓块,但是主線程在mLock.awaite等待厢绝,子線程讀取完成之后,才會(huì)mLock.notifyAll()带猴,主線程才會(huì)繼續(xù)運(yùn)行昔汉。
所以如果是在main線程的話,是會(huì)阻塞的拴清。

三 commit()方法和apply()方法

1. 簡介

對外調(diào)用的寫入操作API靶病,被封裝到了Editor中,Editor是一個(gè)接口口予,具體的實(shí)現(xiàn)在SharedPreferencesImpl的內(nèi)部類EditorImpl中娄周。它里面有3個(gè)成員變量。
private final Map<String, Object> mModified = new HashMap<>();//緩存新寫入的鍵值對
private final Object mEditorLock = new Object(); //緩存鎖沪停,即寫鎖
private boolean mClear = false; //是否調(diào)用了clear方法

新寫入的鍵值對煤辨,并不是直接存入到mMap中裳涛,而是先暫存到mModified,等調(diào)用commit()或者 apply()方法后众辨,拷貝到mMap中的端三。put過程比較簡單,都只是提交到緩存map中鹃彻,需要等commit或者apply的時(shí)候郊闯,才會(huì)更新到主內(nèi)存和文件中,這樣可以在多次更新鍵值對時(shí)蛛株,只更新一次文件团赁,提高效率。

寫入到內(nèi)存中后谨履,會(huì)新建一個(gè)Runnable的寫入文件任務(wù)欢摄,在調(diào)用線程或者新線程中,執(zhí)行這個(gè)Runnable將鍵值對寫入文件笋粟。

2. put新值

舉例EditorImpl#putString("keyPutString"剧浸, "valuePutString")方法,在加鎖的代碼塊中 mModified.put(key, value);并return this;

3. 內(nèi)部類 MemoryCommitResult和成員變量mCurrentMemoryStateGeneration矗钟、mDiskStateGeneration

在介紹提交到文件之前,一定要先了解MemoryCommitResult這個(gè)內(nèi)部類嫌变,這個(gè)類的作用就是吨艇,在調(diào)用commit 或者apply方法將新值保存到內(nèi)存之后,還將新值和其它的相關(guān)數(shù)據(jù)封裝到局部變量MemoryCommitResult里面腾啥,保存到文件的內(nèi)容就是這個(gè)局部變量MemoryCommitResult里面封裝的內(nèi)容东涡。
執(zhí)行真正的提交到文件任務(wù)時(shí),會(huì)判斷內(nèi)存數(shù)據(jù)年代memoryStateGeneration新于磁盤數(shù)據(jù)年代mDiskStateGeneration倘待,才會(huì)真正寫入文件疮跑。
MemoryCommitResult和mCurrentMemoryStateGeneration、mDiskStateGeneration是寫入文件時(shí)凸舵,做相關(guān)判斷的重要參數(shù)祖娘。

final long memoryStateGeneration;  //數(shù)據(jù)年代
@Nullable final List<String> keysModified;  //記錄哪些鍵值對是有變化的,通知監(jiān)聽SP變化的觀察者
@Nullable final Set<OnSharedPreferenceChangeListener> listeners;  //觀察者列表
final Map<String, Object> mapToWriteToDisk;  //指向mMap啊奄,準(zhǔn)備寫入到文件的鍵值對渐苏。
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);  //倒計(jì)數(shù)器

4. commit()方法

commit方法里面,一共也就十幾行代碼菇夸,去掉打印日志等非重要代碼琼富,關(guān)鍵的代碼就5行,在如下源碼中做了注釋庄新。

@Override
public boolean commit() {
    ........
    MemoryCommitResult mcr = commitToMemory();   //1 提交到內(nèi)存
    SharedPreferencesImpl.this.enqueueDiskWrite(  //2 將提交到文件任務(wù)加入隊(duì)列
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();  //3 阻塞鞠眉,直至等到寫文件完成后薯鼠,被喚醒。
    } catch (InterruptedException e) {
        return false;
    } finally {
    ........
    }
    notifyListeners(mcr);  //4 通知觀察者內(nèi)容已經(jīng)變化
    return mcr.writeToDiskResult;   //5 返回提交結(jié)果
}
  1. 調(diào)用commitToMemory()方法將緩存mModified中的數(shù)據(jù)寫入到mMap中械蹋,并獲取到寫入文件的數(shù)據(jù)結(jié)構(gòu)的封裝 MemoryCommitResult出皇,里面有待寫入的鍵值對、數(shù)據(jù)年齡代朝蜘、監(jiān)聽SP變化的觀察者恶迈、變化的鍵列表、計(jì)數(shù)器等谱醇。
  2. 將MemoryCommitResult傳入 enqueueDiskWrite()中暇仲,此時(shí) enqueueDiskWrite()方法的第二個(gè)參數(shù)是null
  3. 調(diào)用mcr.writtenToDiskLatch.await(); 讓線程等待寫入文件任務(wù)執(zhí)行完畢副渴。
  4. 通知監(jiān)聽SP的觀察者奈附,鍵值對內(nèi)容發(fā)生變化了。
  5. 返回提交結(jié)果煮剧。

5. apply()方法

apply方法也一樣不是很復(fù)雜斥滤,只是稍稍有點(diǎn)繞,因?yàn)橛袃蓚€(gè)Runnable嵌套勉盅。阻塞不是直接在代碼調(diào)用里面佑颇,而是在Runnable里面。

  1. 跟commit()方法一樣草娜,先調(diào)用commitToMemory()方法將緩存mModified中的數(shù)據(jù)寫入到mMap中挑胸,并獲取到MemoryCommitResult。
  2. 新建一個(gè)等待寫入完成的Runnable類型的臨時(shí)變量awaitCommit宰闰,里面阻塞等待計(jì)數(shù)器歸零 mcr.writtenToDiskLatch.await()茬贵,這是第一個(gè)Runnable。
    在commit方法里面阻塞是直接寫在commit方法里面的移袍,這次不一樣了是解藻,放在了一個(gè)名叫awaitCommit的Runnable里面了,這個(gè)Runnable通常情況下不會(huì)跑在調(diào)用apply方法的進(jìn)程里面(極端例如activity在onStop時(shí)葡盗,除外)螟左。
  3. 調(diào)用 QueuedWork.addFinisher(awaitCommit); 將這個(gè)Runnable加入到QueneWork的sFinishers隊(duì)列中。就是這個(gè)加入觅够,會(huì)在特殊場景下路狮,導(dǎo)致apply方法也會(huì)阻塞主線程,因?yàn)檫@個(gè)隊(duì)列中的Runnable蔚约,會(huì)在activity#onStop時(shí)奄妨,用主線程去執(zhí)行。
  4. 再新建第二個(gè)Runnable苹祟,名為postWriteRunnable砸抛,里面主要干兩件事评雌,一是運(yùn)行上面創(chuàng)建的awaitCommit,然后是等待阻塞的awaitCommit在文件寫入成功被喚醒后直焙,將awaitCommit移除隊(duì)列QueuedWork.removeFinisher(),這樣后續(xù)的activity執(zhí)行onStop就不會(huì)阻塞了景东。
  5. 將 寫文件任務(wù)postWriteRunnable和對應(yīng)的數(shù)據(jù)放入隊(duì)列enqueueDiskWrite(mcr, postWriteRunnable)。

上面把我們常用的api奔誓,commit和apply方法介紹完了斤吐,它們里面基本就是干5件事:

  1. 一是將新值提交到內(nèi)存
  2. 阻塞,commit是直接阻塞調(diào)用者厨喂;apply是將阻塞封裝到Runnable里面和措,大部分情況不會(huì)阻塞調(diào)用者,極端情況例如activity在onStop時(shí)蜕煌,會(huì)阻塞主線程派阱。
  3. 寫入文件完成,喚醒阻塞斜纪,讓代碼繼續(xù)執(zhí)行贫母。
  4. 通知觀察者。
  5. commit還有返回結(jié)果盒刚;apply沒有腺劣。

在commit和apply方法里面都要調(diào)用到兩個(gè)方法,一個(gè)是提交內(nèi)存因块,一個(gè)是將寫文件任務(wù)提交到隊(duì)列誓酒。下面來繼續(xù)分析:

四 寫入內(nèi)存和寫入文件

1. commitToMemory()方法

這個(gè)方法顧名思義就是將新值提交到內(nèi)存,對外暴露的提交數(shù)據(jù)api是封裝在EditorImpl.class中贮聂,EditorImpl里面有緩存新值的容器mModified。
在commitToMemory方法里面做的主要事情就是寨辩,將mModified中的值拷貝合并到原mMap中吓懈。并將新合并后的mMap賦值給臨時(shí)變量,將臨時(shí)變量靡狞、數(shù)據(jù)年代等封裝到MemoryCommitResult里面耻警,將MemoryCommitResult做為參數(shù),供以后的提交文件使用甸怕。
無論是commit()還是apply()方法甘穿,寫入內(nèi)存都是發(fā)生在主線程(嚴(yán)謹(jǐn)?shù)卣f應(yīng)該是調(diào)用線程)。

  1. 用SharedPreferencesImpl.this.mLock加鎖代碼塊梢杭,這個(gè)鎖一直到commitToMemory方法最后生成 MemoryCommitResult之前結(jié)束温兼。
  2. 判斷寫入文件而沒有完成的任務(wù)的次數(shù)mDiskWritesInFlight,如果大于0則新拷貝一個(gè)mMap(因?yàn)閷懭肴蝿?wù)的鍵值對即 MemoryCommitResult#mapToWriteToDisk是指向mMap的武契,而寫文件的過程是沒有加鎖的募判,如果不新建可能會(huì)有 正在寫文件時(shí)mMap中的數(shù)據(jù)正在變化 這樣的場景)荡含,并將寫入任務(wù)局部變量mapToWriteToDisk指向mMap。
  3. 將臨時(shí)變量mapToWriteToDisk指向mMap届垫,然后將待寫入任務(wù)次數(shù)mDiskWritesInFlight++释液。
  4. 用mEditorLock加鎖后續(xù)代碼塊,一直到mLock加鎖的代碼塊結(jié)束之前装处。
  5. 判斷mClear是否為真(即是否調(diào)用了EditorImp#clear()方法)误债,如果為真則清空mapToWriteToDisk(mapToWriteToDisk指向mMap,相當(dāng)于清空了mMap)妄迁。
  6. 遍歷緩存mModified寝蹈,并判斷key是否是EditorImpl自己,或者value值是否為空判族,如果是則移除mapToWriteToDisk中的key躺盛。否則,判斷mModify中跟mapToWriteToDisk中鍵和值都不相等的鍵值對形帮,然后拷貝到mapToWriteToDisk中槽惫。
  7. 將有變化的鍵值對的key添加到列表keysModified中。
  8. 清空mModified
  9. 如果數(shù)據(jù)有變化辩撑,將數(shù)據(jù)年齡代自增一mCurrentMemoryStateGeneration++, 并賦值給寫文件任務(wù)的MemoryCommitResult中的成員變量memoryStateGeneration界斜。
  10. 將memoryStateGeneration,keysModified合冀,監(jiān)聽者listeners各薇,mapToWriteToDisk一起構(gòu)造MemoryCommitResult()并作為返回值返回出去。

根據(jù)上面的四.1.2分析君躺,在提交到內(nèi)存后峭判,提交文件的局部變量并沒做深拷貝,猜想在調(diào)用apply方法后再修改內(nèi)存值棕叫,會(huì)影響到寫文件的局部變量林螃。后續(xù)進(jìn)行驗(yàn)證。
根據(jù)四.1.6分析俺泣,在沒有執(zhí)行commit或者apply之前疗认,新寫入的值通過SharedPreferences.getString()等是獲取不到的。這個(gè)很明顯伏钠,不用驗(yàn)證横漏。

2. enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable)方法:

在commit或者apply方法中,調(diào)用提交到內(nèi)存方法后熟掂,緊接著調(diào)用的就是enqueueDiskWrite這個(gè)方法缎浇。這個(gè)方法可以理解為:將寫文件任務(wù)提交到隊(duì)列,當(dāng)然里面會(huì)有很多條件判斷赴肚;也有可能是根據(jù)判斷华畏,將寫文件任務(wù)直接執(zhí)行而不是提交到隊(duì)列異步執(zhí)行鹏秋。
這個(gè)方法需要兩個(gè)入?yún)ⅲ琈emoryCommitResult類型的mcr和Runnable類型的postWriteRunnable亡笑。這個(gè)方法做的事情如下:

  1. 通過postWriteRunnable等于null侣夷,將isFromSyncCommit賦值為true,認(rèn)為是從commit提交過來的仑乌。
  2. 創(chuàng)建寫文件的Runnable writeToDiskRunnable百拓,這個(gè)writeToDiskRunnable的run方法里面主要做三件事:一是 用文件鎖mWritingToDiskLock加鎖,并在加鎖的代碼塊中調(diào)用寫文件方法晰甚,傳入?yún)?shù)是MemoryCommitResult和isFromSyncCommit衙传。二是 用mLock加鎖,并在加鎖的代碼塊中將待寫入文件任務(wù)個(gè)數(shù)自減一mDiskWritesInFlight--厕九。三是 如果 postWriteRunnable不為空蓖捶,則執(zhí)行這它(由前面分析,從commit方法調(diào)用過來時(shí)候扁远,是空俊鱼。從apply方法調(diào)過來時(shí)候不是空)。
  3. 判斷如果是從commit方法調(diào)用的畅买,而且等待寫入文件的任務(wù)個(gè)數(shù)是1并闲,就直接執(zhí)行寫文件任務(wù)writeToDiskRunnable,并返回谷羞,后續(xù)入隊(duì)列方法不執(zhí)行帝火。
  4. 第三步中,條件不滿足湃缎,沒有直接執(zhí)行寫文件任務(wù)和返回犀填,就會(huì)到這一步,這一步就是調(diào)用QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)方法將寫文件任務(wù)加入隊(duì)列嗓违。有兩個(gè)參數(shù)九巡,第一個(gè)是寫文件任務(wù),第二個(gè)是否從commit方法調(diào)用過來靠瞎,如果不是從commit方法調(diào)用過來,MSG消息會(huì)延時(shí)100MS發(fā)送求妹。

3. 寫入文件方法writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit)

這個(gè)方法里面乏盐,主要就是通過加鎖判斷數(shù)據(jù)年代決定是否需要真正寫入文件,然后將原文件重命名為備份文件制恍,再將數(shù)據(jù)寫入內(nèi)存父能,然后刪除備份文件。詳細(xì)如下:

  1. 在原文件存在的情況下净神,如果磁盤中的數(shù)據(jù)年代小于局部變量MemoryCommitResult 中的數(shù)據(jù)年代何吝,判斷是從commit調(diào)用過來 則需要寫磁盤溉委,或者不是從commit調(diào)用過來但是成員變量數(shù)據(jù)年代等于局部變量MemoryCommitResult 中的數(shù)據(jù)年代也需要寫磁盤。
  2. 根據(jù)上面的判斷爱榕,如果不需要寫磁盤瓣喊,則設(shè)置標(biāo)志位mcr.setDiskWriteResult(false, true)并返回。
  3. 備份文件如果不存在黔酥,則將原文件重命名為備份文件藻三,如果重命名失敗則返回失敗,不在將內(nèi)容寫入文件跪者。
  4. 備份文件如果存在棵帽,則將原文件刪除。這就是它能夠保持?jǐn)?shù)據(jù)穩(wěn)定的方法渣玲,至少倒數(shù)第二次的數(shù)據(jù)是有效的逗概。
  5. 寫入文件,完成后關(guān)閉流忘衍。
  6. 刪除備份文件逾苫。
  7. 磁盤數(shù)據(jù)年代賦值為內(nèi)存數(shù)據(jù)年代。
  8. 返回成功結(jié)果淑履。

MemoryCommitResult
QueneWork中有個(gè)HandlerThread

ps 需要注意的是隶垮,Editor.clear只是更改了mClear這個(gè)標(biāo)志位,在commit()或者

SharedPreferencesImpl#apply() 和QueneWork#waitToFinish()的幾種線程場景
正常場景:子線程執(zhí)行寫入文件秘噪,寫入完成后CountDownLatch在子線程減到0狸吞,并在子線程通過加鎖的方式移除sFinisher隊(duì)列,這樣waitToFinish被調(diào)用時(shí)指煎,包含mcr.writtenToDiskLatch.await()的awaitCommit Runnabel已經(jīng)從sFinisher隊(duì)列移除蹋偏,不會(huì)阻塞主線程。
主線程寫文件場景:主線程執(zhí)行寫入文件至壤,
主線程被子線程阻塞場景威始。

提問:

既然有主線程await等待子線程,有可能ANR嗎 像街?
clear時(shí)候黎棠,只是做了標(biāo)記,然后真正提交的時(shí)候镰绎,清除了mMap脓斩,但是并未清除mModified,所以在為提交到文件的put都不會(huì)被清除畴栖。
通知sp監(jiān)聽的時(shí)候随静,apply方法其實(shí)文件還未寫完,所以有可能和文件中不一致

優(yōu)化方法:
不用,apply而是在子線程用commit

參考文獻(xiàn):
Sp效率分析和理解
面試高頻題:一眼看穿 SharedPreferences
讓源碼告訴你:Android 不要濫用 SharedPreferences(上)
Android 之不要濫用 SharedPreferences(下)
[Google] 再見 SharedPreferences 擁抱 Jetpack DataStore

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末燎猛,一起剝皮案震驚了整個(gè)濱河市恋捆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌重绷,老刑警劉巖沸停,帶你破解...
    沈念sama閱讀 216,997評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異论寨,居然都是意外死亡星立,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評論 3 392
  • 文/潘曉璐 我一進(jìn)店門葬凳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绰垂,“玉大人,你說我怎么就攤上這事火焰【⒆埃” “怎么了?”我有些...
    開封第一講書人閱讀 163,359評論 0 353
  • 文/不壞的土叔 我叫張陵昌简,是天一觀的道長占业。 經(jīng)常有香客問我,道長纯赎,這世上最難降的妖魔是什么谦疾? 我笑而不...
    開封第一講書人閱讀 58,309評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮犬金,結(jié)果婚禮上念恍,老公的妹妹穿的比我還像新娘。我一直安慰自己晚顷,他們只是感情好峰伙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著该默,像睡著了一般瞳氓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上栓袖,一...
    開封第一講書人閱讀 51,258評論 1 300
  • 那天匣摘,我揣著相機(jī)與錄音,去河邊找鬼裹刮。 笑死音榜,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的必指。 我是一名探鬼主播囊咏,決...
    沈念sama閱讀 40,122評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼恕洲,長吁一口氣:“原來是場噩夢啊……” “哼塔橡!你這毒婦竟也來了梅割?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,970評論 0 275
  • 序言:老撾萬榮一對情侶失蹤葛家,失蹤者是張志新(化名)和其女友劉穎户辞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體癞谒,經(jīng)...
    沈念sama閱讀 45,403評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡底燎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弹砚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片双仍。...
    茶點(diǎn)故事閱讀 39,769評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖桌吃,靈堂內(nèi)的尸體忽然破棺而出朱沃,到底是詐尸還是另有隱情,我是刑警寧澤茅诱,帶...
    沈念sama閱讀 35,464評論 5 344
  • 正文 年R本政府宣布逗物,位于F島的核電站,受9級特大地震影響瑟俭,放射性物質(zhì)發(fā)生泄漏翎卓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評論 3 327
  • 文/蒙蒙 一摆寄、第九天 我趴在偏房一處隱蔽的房頂上張望失暴。 院中可真熱鬧,春花似錦椭迎、人聲如沸锐帜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缴阎。三九已至,卻和暖如春简软,著一層夾襖步出監(jiān)牢的瞬間蛮拔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評論 1 269
  • 我被黑心中介騙來泰國打工痹升, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留建炫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,831評論 2 370
  • 正文 我出身青樓疼蛾,卻偏偏與公主長得像肛跌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評論 2 354

推薦閱讀更多精彩內(nèi)容