Android的兩種數據存儲方式分析(一)

Android上常見的數據存儲方式有哪些呢?

SharedPreferences這種存儲數據的方式我們平時用的都對嗎?

怎么使用SQLiteDatabase才是安全的?

帶著這些問題,我們今天來深入分析一下SharedPreferences和database這兩種Android上常見的數據持久化方式。

一、SharedPreferences

1、Preference和sharedPreferences是什么

Preference在Android上是首選項的意思,主要是指FrameWork上的各種UI組件,我們看一下Preference的各個子類:

preference的子類

它們一般用在PreferenceActivity中,當使用這些組件時,設置在組件中的數據會自動進行保存。
說的更加直白一些,Preference就是應用的設置界面。

SharedPreferences是用來存取Preference中設置的數據的,它是key-value鍵值對的形式存在,Android 3.0后又增加了StringSet的value形式,可以說SharedPreferences就是用來為Preference做數據持久化的。我們也來看看官方對它的說明:

SharedPreferences說明

從這個官方說明里我們注意到我們平時容易忽略的兩點:(1)對于任何一類的preference(實際就是同一個preference name),SharedPreferences是唯一的;(2)SharedPreferences不支持多進程(這個我們接下來也會分析到)。

2、SharedPreferenced的內部實現

對于怎么使用SharedPreferences,我們就不多討論了,這是Android最基本的一種數據存儲方式了,如果你還不知道如何使用它,那你要保持低調了,不要讓人知道你是一個Android的程序員,同時趕緊去找資料學習一下吧。

(1)數據存儲格式

SharedPreferences的數據是以xml格式存儲的;它的存儲位置在我們應用程序私有文件目錄下的shared_prefs中,每個preference_name會存儲一個xml文件;同時,這些數據都是明文存儲的,擔心數據泄漏的,記得加密后再寫入哦。

具體的文件存儲目錄是:/data/data/${packageName}/shared_prefs/

我們看一下SharedPreferences的get方法接口:

SharedPreferences get方法

從這里可以看出SharedPreferences只支持6種數據類型,分別是boolean,float,int,long,String和StringSet,基本StringSet還是在Android3.0后才加入。我們再來看看存儲在xml中是什么樣子:

pref文件中的形式

可以看出,xml中的標簽也是對應的幾個。

(2)數據載入和緩存

SharedPreferences會在第一次打開這個Preference時,會啟動一個新進程將整個文件中的內容輸入到內存中,并進行緩存,以后再用到這個SharedPreferences時,都使用這一個實例。

我們看一下代碼,SharedPreferences只是一個接口,它的真實代碼在SharedPreferencesImpl.java中。

context中獲取sharedpreferences

獲取到SharedPreferences只有這一種方法,即使用PreferenceManager獲取,最終也是通過這里,也只有這樣,才能在context類中緩存已經載入的SharedPreferences。

獲取SharedPreferences的代碼

第一個紅框,我們看到,context中的靜態成員sSharedPrefs用來緩存SP(以后用它來簡寫SharedPreferences),不過它的第一層key竟然是packageName,不知道是不是為了給插件留的兼容(純屬瞎猜的);第二個紅框,我們看到,如果沒有緩存,則會創建一個SP的實例SharedPreferencesImpl,并放入到緩存中。

加載SP數據的過程

啟動新線程加載,可能主要是為了考慮它要讀文件,但如果用到get方法或edit方法時,又是要等到新線程里把文件讀完的,而我們平時使用時,很多時候拿到了SP,直接就要用的,或讀或寫,所以這個新線程起的不是特別必要,但畢竟給了我們預加載的一個SP的可能。
還要注意一點,SPImpl加載時,默認給的Buffer是16K,所以解析一個SP文件還是比較耗內存的。
還有一點,這里try catch中的異常,并不包括所有的異常,所以,如果這個pref文件出了其它問題,后果是很嚴重的,基本這個應用程序只能清除數據或重裝了。
最后,讀到的內容被放到了mMap中加以緩存,使用時可以直接從這個map中拿數據,所以,從SP的數據載入看,它為了提高性能,也是在內存中做了一個SP文件內容的映射。

(3)數據寫入

SP的數據寫入方式是,通過editor將要存儲的數據同步或異步的寫入內存,并將整個Pref的內容寫回到文件中,我們從代碼中一步一步來看:

先補一下SharedPreferencesImpl的源碼,大家想看全部內容,可以直接訪問:http://www.grepcode.com/file/repo1.maven.org/maven2/org.robolectric/android-all/5.0.0_r2-robolectric-1/android/app/SharedPreferencesImpl.java#SharedPreferencesImpl.%3Cinit%3E%28java.io.File%2Cint%29

獲取editor

想了想,還是把edit()的代碼貼了出來,因為這里有兩個點需要注意和思考,一是,獲取到editor之前,必須要等數據加載完成,這個Android的程序員也想把它移除掉,我們覺得可以嗎?二是,大家再思考一個問題,SP為什么要通過editor去操作數據呢?為什么不像get方法一樣,在SP中加幾個put方法直接就搞定了,使用起來還非常的方便?

第一個問題,我覺得可以,畢竟editor只是將要寫入數據的一個緩存,等真正去commit時再確認數據是否已經加載完成也是可以的。

第二個問題,我的看法是,SP就是為Preference設計的,用戶在設置首選項時,會選擇多個后,點擊“應用”按鈕,讓他們一塊兒生效,這也符合首選項的操作習慣,而Editor也為了這種場景而存在;同時,Editor還有一個好處,可以攢一堆數據后批量的進行一次寫入,提升效率。

Editor Put數據

Editor中put的數據,都臨時存放在自己的成員變量mModified中。

Editor的兩種數據提交方式

Editor可以通過apply或commit提交數據,兩者的區別是,apply是異步提交,不阻塞;commit是同步提交,會阻塞當前線程直到數據寫入磁盤完成。注意,apply方法在Android 3.0及以后才支持。

apply方法

apply方法中,目前我們可以忽略綠色框的那些內容,只看紅色部分,commitToMemory()方法將editor數據寫入內存,enqueueDiskWrite方法將寫操作放入隊列,由另外的線程去做寫入,沒有任何阻塞就返回了,這也就是apply沒有阻塞的原因。

注意,綠色部分看起來沒有什么用處,實際上這個地方隱藏了一個大坑,后面講到“主線程對SP的依賴”時,會再講到這一點。

commit方法

我們再來看commit方法,它執行完成apply相同的兩步后,開始進行wait,而這個writtenToDiskLatch實際上是一個countDownLatch,寫完磁盤后會countDown,這時函數才能返回,所以,commit是阻塞的,盡量不要在主線程commit哦。

commitToMemory()方法

commitToMemory方法也是有不少看點的,首先,提交到內存后返回值是要攜帶不少信息的,所以這里返回一個對象MemoryCommitResult,以方便把這些信息帶到write線程;其次,如果當前有其它editor也在等待寫磁盤(mDiskWritesInFlight > 0,大家自己去源碼中看這個標志變量的作用吧),則是要把SP中所有的鍵值對復制一份出來的,這也是一個數據同步的技巧,大家可以學習一下,但是,如果sp中鍵值對過多,這里復制一份出來是很占內存的,如果寫線程阻塞嚴重,這里復制出來的份數更多,內存占用就更嚴重啦;三、后面寫入內存時會做一定的判斷,如果value==null,表示remove,如果value的值跟原值都一樣,這個editor是不需要再執行寫操作的,即changesMade=false,節省開銷。

這里回想一下,我們在這個分析中提到了幾個SP關于內存方面需要注意的點?

排隊寫入的實現

從后兩個紅框我們看以看出,postWriteRunnable為空,表示是commit過來的,此時會直接執行runnable.run,同步完成;如果非空,表示是apply方法過來的,此時會使用一個singleThreadExecutor這樣的單線程池,順次進行文件寫入。

注意,這里的線程池是QueuedWork類中的,為什么不在SP內自己定義呢?為什么SP要跟QueuedWork攪來攪去呢?留著這個疑問吧,一會兒我們仍然在對"主線程的依賴"中揭曉。

寫文件

最后再來看一下寫入的過程吧,從代碼可以看出,Android采取的是先備份,再寫入的過程。如果本次寫入失敗,則刪除寫的內容,在下次加載時,會加載備份的文件,如果寫入成功,則刪除備份文件,不得不說,Android考慮的還是比較周全的。

(4)多進程操作SharedPreferences

從SP的官方說明看,SP是不支持多進程的,注意這里說的是進程,不是線程。但我們再看另外一個值

MULTI_PROCESS

這是context中的一個Mode值,從它的說明看,它是為多進程而生,從deprecated看,Android又放棄了它,建議我們有多進程,還是自己用contentProvider來解決吧。有點凌亂,這個值到底還起作用嗎?我們還是去看源碼吧。

這段代碼來自SharedPreferencesImpl中的writeToFile方法

這個是方是SharedPreferencesImpl中唯一使用到mMode的地方,也就是我們從Context中獲取SP時傳入的mode,從使用來看,它只是設置Pref文件的讀取權限,是私有,還是開放,跟Multi_process,沒什么關系。

ContextImpl中getSharedPreferences方法中的代碼

再看ContextImpl中獲取SP時的代碼,可以看出,這個mode的作用就是,標識Pref文件可能被其它進程改寫了,你再重新reload一次得一下最新數據吧,同步的安全級太低了,難怪Android官方也不建議用了。

所以結論就是,SP不支持多進程。

3、主線程對SharedPreferenced的依賴

SP的apply提供了異步線程操作,那它不應該很安全嗎,怎么又跟主線程扯上關系了呢?要弄清楚這個問題,我們還是先了解一下QueuedWork:

ActivityThread中對QueuedWork的調用

從ActivityThread的調用來看,它在處理一個Service啟動時會調用到QueuedWork的waitToFinish方法,實際上,我們在ActivityThread代碼中搜一下會發現,在pauseActivty,stopActivity中,都有對這個方法的調用,從而我們可以猜測,QueuedWork這個隊列中存放的應該是一些關鍵事務,ActivityThread的looper中處理的很多事件要想繼續進行,都必須等待這個隊列中的關鍵事務執行完。

QueuedWork中的waitToFinish方法

從waitToFinish方法中可以看出,它果然是把所有隊列中的Runnable拿出來順次執行一下,更加殘酷的是,它還是同步的,可見QueuedWork隊列中的任務有多么的關鍵了。

好,我們再回過頭我們SP的Editor中的apply方法:

apply方法中的QueuedWork

在apply時,Editor把commit完成的這個task放入到了QueuedWork隊列中,在寫入磁盤完成后才移除;從這里可以看出Android對SP的重視程度,SP不寫完,Activity,Service的很多操作就必須要等待,而這些操作都在主線程,這也就是為什么apply仍然跟主線程有關系的原因了。

那為什么Android這么重視SP呢?我的猜想是,SP就是為Preference這個首選項而生的,首選項對全局的影響還是很大的,所以必須等待首選項存儲完成。

4、sharedPreferences使用注意事項

SP中存放的是一個個的鍵值對,而我們平時的很多數據就是鍵值對,所以我們很自然的把它作為我們存儲數據的默認方式,從上面的分析來看,這樣做還是有不少弊端的,在講這些弊端前,我們先看看Android官方對SP的使用建議:

Android官方建議

注意“較小”,如果我們存的數據量大,就不要用它啦。

我們來說說SP的弊端吧:

(1)讀寫速度慢,使用xml格式存儲,解析效率本來就低,平時修改任何一個key,都要重寫整個文件。

(2)明文存儲,太不安全啦。

(3)內存占用高,讀寫文件內存占用高;高頻并發寫入時,不斷的復制,臨時內存會飆升。

(4)如果SP文件損壞,我們無法捕獲異常,可能造成應用頻繁崩潰。

(5)最最關鍵的,它會影響到主線程,造成卡頓,甚至造成anr哦。

建議:如果我們有很多值需要保存,改數據庫吧,效率會高很多。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容