Android存儲(chǔ)基礎(chǔ)
1、Android 分區(qū)
分區(qū)簡(jiǎn)單來說就是將設(shè)備中的存儲(chǔ)劃分為一些互不重疊的部分真友,每個(gè)部分都可以單獨(dú)格式化黄痪,用作不同的目的。這樣系統(tǒng)可以靈活的針對(duì)單獨(dú)的分區(qū)進(jìn)行不同的操作盔然,而不影響其他分區(qū)的數(shù)據(jù)桅打。
2、Android存儲(chǔ)安全
- 權(quán)限控制
Android的每個(gè)應(yīng)用都在自己的沙盒內(nèi)運(yùn)行愈案,在Android4.3之前采用了標(biāo)準(zhǔn)的Linux保護(hù)機(jī)制油额,通過為每個(gè)應(yīng)用創(chuàng)建獨(dú)一無二的Linux UID來定義。在Android4.3引入了SELinux(Security Enhance Linux)機(jī)制進(jìn)一步定義Android應(yīng)用沙盒的邊界刻帚。它的作用在于即使進(jìn)程有root權(quán)限也不能為所欲為,必須要在專門的安全策略配置文件中賦予權(quán)限涩嚣。 - 數(shù)據(jù)加密
除了權(quán)限控制崇众,數(shù)據(jù)加密也是保護(hù)數(shù)據(jù)的不錯(cuò)選擇。Android有兩種設(shè)備加密方法:全盤加密和文件加密航厚。全盤加密是在Android4.4中引入的顷歌,并在Android5.0中默認(rèn)打開的。它會(huì)將/data分區(qū)的用戶數(shù)據(jù)操作加密/解密幔睬,對(duì)性能會(huì)有一定的影響眯漩,但是新版本的芯片都會(huì)在硬件中直接提供支持。基于文件系統(tǒng)的加密赦抖,如果設(shè)備被解鎖了舱卡,加密就沒有用了,所以Android7.0增加了基于文件的加密队萤。在這種模式下轮锥,將會(huì)給每個(gè)文件分配一個(gè)必須用戶的passcode推導(dǎo)出來的密鑰,特定的文件被屏幕鎖屏之后要尔,直到用戶下一次解鎖屏幕期間都不能訪問舍杜。
常見數(shù)據(jù)存儲(chǔ)方法
1、關(guān)鍵因素
選擇存儲(chǔ)方法時(shí)一般會(huì)考慮一下因素:
2赵辕、存儲(chǔ)選項(xiàng)
- SharePreferences
- ContentProvider
- 文件
- 數(shù)據(jù)庫
1)SharePreferences的使用
- 跨進(jìn)程不安全
- 加載緩慢既绩。SharePreferences文件的加載使用了異步線程,而且加載線程沒有設(shè)置線程優(yōu)先級(jí)还惠,如果這個(gè)時(shí)候主線程讀取數(shù)據(jù)就需要等待文件加載線程的結(jié)束饲握。這就導(dǎo)致出現(xiàn)主線程等待低優(yōu)先級(jí)線程鎖的問題。
- 全量寫入
無論調(diào)用commit()還是apply()吸重,即使我們只改動(dòng)其中一個(gè)條目互拾,都會(huì)把整個(gè)內(nèi)容全部寫到文件。而且多次寫入嚎幸,SP也沒有合并寫入的操作颜矿。 - 卡頓
由于執(zhí)行了異步落盤的apply機(jī)制,在崩潰或者其他異常情況可能會(huì)導(dǎo)致數(shù)據(jù)丟失嫉晶。所以當(dāng)應(yīng)用收到系統(tǒng)廣播骑疆,或者被調(diào)用OnPause等一些時(shí)機(jī),系統(tǒng)會(huì)強(qiáng)制把所有的SharePreferences對(duì)象數(shù)據(jù)落地到磁盤替废,如果沒有落地完成箍铭,這是主線程會(huì)一直被阻塞,這樣非常容易造成卡頓椎镣,很值A(chǔ)NR诈火。
系統(tǒng)提供的SharePreferences的應(yīng)用場(chǎng)景是用來存儲(chǔ)一些非常簡(jiǎn)單、輕量的數(shù)據(jù)状答,不適合復(fù)雜和大量的數(shù)據(jù)冷守。推薦微信開源MMKV。
2)ContentProvider的使用
- 啟動(dòng)性能
ContentProvider的生命周期默認(rèn)在Application onCreate()之前惊科,而且在主線程創(chuàng)建的拍摇,我們自定義的ContentProvider類的構(gòu)造函數(shù)、靜態(tài)代碼馆截,onCreate函數(shù)盡量不要做耗時(shí)的操作充活,會(huì)拖慢啟動(dòng)速度蜂莉。 -
穩(wěn)定性
ContentProvider在跨進(jìn)程數(shù)據(jù)傳遞時(shí),利用了Android的Binder和匿名共享內(nèi)存機(jī)制混卵。就是通過Binder傳遞CursorWindow對(duì)象內(nèi)部的匿名共享內(nèi)存的文件描述符映穗,這樣在跨進(jìn)程傳輸中,結(jié)果數(shù)據(jù)并不需要跨進(jìn)程傳輸淮菠,而在不同進(jìn)程中通過傳輸匿名共享文件描述符來操作同一塊匿名內(nèi)存男公,這樣來實(shí)現(xiàn)不同進(jìn)程訪問相同數(shù)據(jù)的目的。
基于mmap的匿名共享內(nèi)存機(jī)制也是有代價(jià)的合陵,當(dāng)傳輸?shù)臄?shù)量非常小的時(shí)候枢赔,并不一定劃算。所以ContentProvider提供了一種call函數(shù)拥知,他會(huì)直接通過Binder來傳輸數(shù)據(jù)踏拜。Android的Binder傳輸是有大小限制的,一般來說是1-2MB低剔,ContentProvider接口的調(diào)用參數(shù)和call函數(shù)調(diào)用并沒有使用匿名共享機(jī)制速梗,當(dāng)插入很多數(shù)據(jù),那么就會(huì)出現(xiàn)個(gè)很大的插入數(shù)組襟齿,那么這個(gè)操作可能會(huì)出現(xiàn)數(shù)據(jù)超大異常姻锁。
-
安全性
雖然ContentProvider提供了很好的安全機(jī)制,但如果ContentProvider支持exported猜欺,當(dāng)支持SQL時(shí)要注意SQL注入問題位隶,另外如果傳入的參數(shù)是文件路徑,要校驗(yàn)合法性开皿,不然應(yīng)用的私有數(shù)據(jù)可能被別人拿到涧黄。
對(duì)象序列化
對(duì)象序列化就是把一個(gè)Object對(duì)象所有的信息表示成一個(gè)字節(jié)序列,包括class信息赋荆、繼承關(guān)系信息笋妥、訪問權(quán)限、變量類型以及數(shù)值信息等窄潭。
1春宣、Serializable
Serializable是Java原生的序列化機(jī)制,廣泛地在Android 中使用嫉你,可以通過Bundle傳遞Serializable的序列化數(shù)據(jù)信认。
Serializable的原理是通過ObjectInputStream和ObjectOutputStream來實(shí)現(xiàn)的。整個(gè)序列化過程大量地使用了反射和臨時(shí)變量均抽,整個(gè)計(jì)算非常復(fù)雜,序列化性能比較差其掂。
- Serializable支持替代默認(rèn)流程的序列化油挥,它會(huì)先反射判斷是否存在我們自己實(shí)現(xiàn)的序列化方法writeObject和反序列化方法readObject。通過這兩個(gè)方法可以對(duì)某些字段做些特殊修改,也可以實(shí)現(xiàn)序列化的加密功能深寥。
- writeReplace和readReplace這個(gè)兩個(gè)方法代理序列化對(duì)象攘乒,可以實(shí)現(xiàn)自定義返回的序列化實(shí)例。我們可以通過它們實(shí)現(xiàn)對(duì)象序列化的版本兼容惋鹅。
Serializable注意事項(xiàng): - 不被序列化的字段则酝。類的static變量以及被聲明為transient字段,默認(rèn)的序列化機(jī)制都會(huì)忽略該字段闰集,不會(huì)進(jìn)行序列化存儲(chǔ)沽讹。當(dāng)然我們可以通過writeObject和readObject實(shí)現(xiàn)自定義序列化存儲(chǔ)方案。
- serialVersionUID武鲁。在類實(shí)現(xiàn)了Serializable接口后爽雄,我們需要添加一個(gè)Serial Version ID,它相當(dāng)于版本號(hào)沐鼠,這個(gè)ID可以顯示聲明也可以讓編譯器自己計(jì)算挚瘟。通常建議顯示聲明會(huì)更加穩(wěn)妥,因?yàn)殡[式聲明假如發(fā)生了一點(diǎn)點(diǎn)變化饲梭,進(jìn)行反序列化都會(huì)由于serialVersionUID的變化而引發(fā)InvalidClassException異常乘盖。
- 構(gòu)造方法。 Serializable的反序列化默認(rèn)是不會(huì)執(zhí)行構(gòu)造函數(shù)的憔涉,它會(huì)根據(jù)數(shù)據(jù)流中對(duì)Object的描述信息創(chuàng)建對(duì)象订框,如果一些邏輯依賴構(gòu)造函數(shù),就可能出現(xiàn)問題监氢。
2布蔗、Parcelable
Parcelable是專門針對(duì)Android設(shè)計(jì)的輕量且高效地對(duì)象序列化和反序列化實(shí)現(xiàn)方案。Parcelable的實(shí)現(xiàn)核心都在Parcel.cpp浪腐,它只會(huì)在內(nèi)存中進(jìn)行序列化操作纵揍,并不會(huì)將數(shù)據(jù)存儲(chǔ)到磁盤里。也可以通過Parcel.cpp的marshall接口獲取byte數(shù)組议街,然后存在文件中從而實(shí)現(xiàn)永久保存泽谨。
Parcelable注意事項(xiàng):
Parcelable在使用的時(shí)候需要手動(dòng)添加自定義代碼,使用起來比Serializable要麻煩一些特漩,正是因?yàn)槿绱薖arcelable才不需要使用反射的方法去實(shí)現(xiàn)吧雹,提高了性能。
- 系統(tǒng)版本兼容性涂身。由于Parcelable設(shè)計(jì)的本意是在內(nèi)存中使用的雄卷,無法保證所有Android 版本的Parcel.cpp的實(shí)現(xiàn)都完全一致,可能會(huì)存在兼容問題蛤售。
- 數(shù)據(jù)前后兼容性丁鹉。Parcelable并沒有版本管理的設(shè)計(jì)妒潭,如果出現(xiàn)版本升級(jí),寫入的順序及字段類型兼容需要格外注意揣钦,一般來說雳灾,如果需要持久存儲(chǔ)的話,還是不得不選擇性能較差的Serializable方案冯凹。
3谎亩、Serial
Teitter開源的高性能序列化方案Serial。從實(shí)現(xiàn)原理來看Serial就像是把Parcelable和Serializable的有點(diǎn)集合在一起的方案宇姚。
- 由于沒有使用反射匈庭,比傳統(tǒng)方案更加高效
- 開發(fā)者對(duì)于序列化過程的控制較強(qiáng),可以定義哪些Object空凸、Field需要序列化
- 有很強(qiáng)的debug能力嚎花,可以調(diào)試序列化的過程
- 有很強(qiáng)的版本管理能力,可以通過版本號(hào)和OptionalFieldException做兼容
數(shù)據(jù)的序列化
1呀洲、JSON
JSON是一種輕量級(jí)的數(shù)據(jù)交換格式紊选,它被廣泛的使用在網(wǎng)絡(luò)傳輸中,很多應(yīng)用與服務(wù)端的通訊業(yè)采用JSON進(jìn)行通訊道逗。
JSON的優(yōu)勢(shì):
- 相比對(duì)象序列化方案兵罢,速度更快,體積更小滓窍。
- 相比二進(jìn)制的序列化方案卖词,結(jié)果可讀,易于排查問題吏夯。
-
使用方便此蜈,支持跨平臺(tái)、跨語言噪生,支持嵌套使用裆赵。
2、Protocol Buffers
Protocol Buffers是Google開源的跨語言編碼協(xié)議跺嗽,適用于數(shù)據(jù)量非常大战授,對(duì)性能要求高的方案。
- 性能
使用二進(jìn)制編碼壓縮桨嫁,相比于JSON體積更小植兰,編解碼速度更快 - 兼容性
跨語言支持和前后兼容性都不錯(cuò),也支持基本類型的自動(dòng)轉(zhuǎn)換璃吧,但不支持繼承和引用類型楣导。 -
使用成本
開發(fā)成本很高,需要定義.proto文件畜挨,并用工具生成對(duì)應(yīng)的輔助類爷辙。輔助類特有一些序列化的輔助方法彬坏,所有要序列化的對(duì)象,都需要先轉(zhuǎn)化為輔助類的對(duì)象膝晾,這讓序列化代碼跟業(yè)務(wù)代碼大量耦合,是侵入性較強(qiáng)的一種方式务冕。
存儲(chǔ)監(jiān)控
1血当、性能監(jiān)控
- 正確性
- 時(shí)間開銷
- 空間開銷
2、ROM監(jiān)控
除了某個(gè)存儲(chǔ)模塊的監(jiān)控禀忆,我們也需要對(duì)應(yīng)用整體的ROM空間做詳細(xì)監(jiān)控臊旭。ROM監(jiān)控的兩個(gè)核心指標(biāo)是文件總大小與總文件數(shù)。
SQLite優(yōu)化
1箩退、基礎(chǔ)知識(shí)
1)ORM
ORM(Object Relational Mapping)即關(guān)系對(duì)象映射离熏,用面向?qū)ο蟮母拍畎褦?shù)據(jù)庫中表和對(duì)象關(guān)聯(lián)起來,讓我們可以不用關(guān)心數(shù)據(jù)庫底層實(shí)現(xiàn)戴涝。Android中常見ORM框架有g(shù)reenDAO和Google官方的Room滋戳,ORM框架使用簡(jiǎn)單,但是以犧牲部分執(zhí)行效率為代價(jià)的啥刻。
2)進(jìn)程與線程并發(fā)
并發(fā)問題會(huì)導(dǎo)致SQLiteDatabaseLockedException奸鸯,SQLite并發(fā)有兩個(gè)維度,一個(gè)多進(jìn)程并發(fā)可帽,另一個(gè)是多線程并發(fā)娄涩。
多進(jìn)程并發(fā)
SQLite默認(rèn)支持多進(jìn)程并發(fā),它通過文件鎖來控制多進(jìn)程的并發(fā)映跟。SQLite鎖的粒度并沒有非常細(xì)蓄拣,它針對(duì)的是整個(gè)DB文件,內(nèi)部有5個(gè)狀態(tài)努隙。多進(jìn)程可以同時(shí)獲取SHARED鎖來讀取數(shù)據(jù)球恤,但是只有一個(gè)進(jìn)程可以獲取EXCLUSIVE鎖來寫數(shù)據(jù)庫,在EXCLUSIVE模式下剃法,數(shù)據(jù)庫連接在斷開之前都不會(huì)釋放SQLite文件的鎖碎捺,從而避免不必要的沖突,提高數(shù)據(jù)庫訪問速度贷洲。
多線程并發(fā)
SQLite默認(rèn)支持多洗那成并發(fā)模式收厨,更多進(jìn)程的鎖機(jī)制一樣,為了實(shí)現(xiàn)簡(jiǎn)單优构,SQLite鎖的粒度都是文件級(jí)別诵叁,并沒有實(shí)現(xiàn)表級(jí)甚至行級(jí)的鎖。同一個(gè)句柄同一時(shí)間只有一個(gè)線程在操作钦椭,這個(gè)時(shí)候我們需要打開連接池Connection Pool拧额。多線程可以同時(shí)讀取數(shù)據(jù)庫數(shù)據(jù)碑诉,但是寫數(shù)據(jù)庫依然是互斥的。SQLite提供了Busy Retry的方案侥锦,即發(fā)生阻塞時(shí)會(huì)觸發(fā)Busy Retry进栽,此時(shí)可以讓線程休眠一段時(shí)間后,重新嘗試操作恭垦。如果出現(xiàn)多個(gè)寫并發(fā)的情況快毛,依然有可能出現(xiàn)SQLiteDatabaseLockedException,這個(gè)時(shí)候應(yīng)用可以捕獲這個(gè)異常番挺,然后等待一段時(shí)間后再嘗試唠帝。
3)查詢優(yōu)化
索引優(yōu)化
正確使用索引大部分情況下可以大大降低查詢速度,索引的建立總體來說有一定的原則:
- 建立正確的索引玄柏, 選擇最優(yōu)的索引創(chuàng)建
- 單例索引襟衰、多列索引與符合索引的選擇
索引要綜合數(shù)據(jù)表中不同查詢與排序語句一起考慮,如果查詢結(jié)果集過大粪摘,還是希望通過復(fù)合索引直接在索引表返回查詢結(jié)果瀑晒。 - 索引字段的選擇
整型類型索引效率會(huì)遠(yuǎn)高于字符串索引,而對(duì)于主鍵SQLite會(huì)默認(rèn)幫我們建立索引赶熟,所以主鍵盡量不要用復(fù)雜字段瑰妄。
頁大小與緩存大小
對(duì)于SQLite的DB文件來說,頁(page)是最小的存儲(chǔ)單位映砖,每個(gè)表對(duì)應(yīng)的數(shù)據(jù)在整個(gè)DB文件都是通過一個(gè)一個(gè)頁存儲(chǔ)间坐,屬于同一個(gè)表不同的頁以B樹(B-tree)的方式組織索引,每一個(gè)表都是一個(gè)B樹邑退。
跟文件系統(tǒng)的頁緩存一樣竹宋,SQLite會(huì)將讀過的頁緩存起來,用來加快下一次讀取速度地技。頁大小默認(rèn)1024Byte蜈七,緩存大小默認(rèn)1000頁。每個(gè)頁永遠(yuǎn)只會(huì)存放一個(gè)表或一組索引的數(shù)據(jù)莫矗,即不可能同一個(gè)頁存放多個(gè)表或索引數(shù)據(jù)飒硅。增大頁的大小不能不斷提升性能,在拐點(diǎn)之后會(huì)起反作用作谚,建議選擇4KB作為默認(rèn)的page size以獲得更好的性能三娩。
其他優(yōu)化
- 慎用select *
- 正確使用事務(wù)
-預(yù)編譯與參數(shù)綁定,緩存被編譯后的SQL語句 - 對(duì)于blob或超大的text列妹懒,可能會(huì)超出一個(gè)頁的大小雀监,導(dǎo)致出現(xiàn)超大頁,建議將這些列單獨(dú)拆表,或者放到表字段的后面
- 定期整理或清理無用或可刪除的數(shù)據(jù)会前。
2好乐、SQLite其他特性
1)損害與修復(fù)
2)加密與安全
數(shù)據(jù)庫的安全主要有兩個(gè)方面,一個(gè)是防注入瓦宜,一個(gè)是加密蔚万。防注入可以通過靜態(tài)安全掃描的方式,而加密一般會(huì)使用SQLClipher支持临庇。
SQLite的加解密都是以頁為單位笛坦,默認(rèn)使用AES算法加密,加解密的耗時(shí)跟選用的密鑰長(zhǎng)度有關(guān)苔巨。