概述
磁盤存儲(chǔ)有兩種形式椎扬,一種是File存儲(chǔ),一種是DB(DataBase)存儲(chǔ)剥哑。
File
File存儲(chǔ)比較常見,當(dāng)我們數(shù)據(jù)量較小淹父,數(shù)據(jù)的分類以及檢索沒有較大的要求的時(shí)候株婴,可以采用File存儲(chǔ)
File存在的問題:
- 文件較大時(shí),對(duì)文件的讀取速度較慢
- 定位暑认,讀寫具體的數(shù)據(jù)較為困難
DataBase
對(duì)數(shù)據(jù)的并發(fā)性和檢索速度有高要求的時(shí)候困介,這個(gè)時(shí)候,DB就上場(chǎng)了蘸际,DB具有如下特點(diǎn)
- 大數(shù)據(jù)訪問速度更快
- 索引特定條件的數(shù)據(jù)較為方便
Http緩存機(jī)制
相對(duì)于內(nèi)存緩存而言座哩,磁盤時(shí)效性很低,所以通常單獨(dú)的磁盤緩存沒有太大意義粮彤,每次去讀緩存之前需要判斷一下懁促是否有效根穷,必須要結(jié)合HTTP的緩存機(jī)制來做一些處理,這樣緩存才會(huì)比較有效导坟,所以下面還是先介紹一下HTTP緩存機(jī)制缠诅,將從緩存存儲(chǔ)策略,緩存過期策略乍迄,緩存對(duì)比策略三個(gè)方面來分析一下Http的緩存機(jī)制管引。
緩存存儲(chǔ)策略
用來確定 Http 響應(yīng)內(nèi)容是否可以被客戶端緩存,以及可以被哪些客戶端緩存
對(duì)于 Cache-Control 頭里的 Public闯两、Private褥伴、no-cache谅将、max-age 、no-store 他們都是用來指明響應(yīng)內(nèi)容是否可以被客戶端存儲(chǔ)的重慢,其中前4個(gè)都會(huì)緩存文件數(shù)據(jù)(關(guān)于 no-cache 應(yīng)理解為“不建議使用本地緩存”饥臂,其仍然會(huì)緩存數(shù)據(jù)到本地),后者 no-store 則不會(huì)在客戶端緩存任何響應(yīng)數(shù)據(jù)似踱。另關(guān)于 no-cache 和 max-age 有點(diǎn)特別隅熙,我認(rèn)為它是一種混合體,下面我會(huì)講到核芽。
通過 Cache-Control:Public 設(shè)置我們可以將 Http 響應(yīng)數(shù)據(jù)存儲(chǔ)到本地囚戚,但此時(shí)并不意味著后續(xù)瀏覽器會(huì)直接從緩存中讀取數(shù)據(jù)并使用,為啥轧简?因?yàn)樗鼰o法確定本地緩存的數(shù)據(jù)是否可用(可能已經(jīng)失效)驰坊,還必須借助一套鑒別機(jī)制來確認(rèn)才行, 這就是我們下面要講到的“緩存過期策略”哮独。
緩存過期策略
客戶端用來確認(rèn)存儲(chǔ)在本地的緩存數(shù)據(jù)是否已過期拳芙,進(jìn)而決定是否要發(fā)請(qǐng)求到服務(wù)端獲取數(shù)據(jù)
剛上面我們已經(jīng)闡述了數(shù)據(jù)緩存到了本地后還需要經(jīng)過判斷才能使用,那么瀏覽器通過什么條件來判斷呢皮璧? 答案是:Expires舟扎,Expires 指名了緩存數(shù)據(jù)有效的絕對(duì)時(shí)間,告訴客戶端到了這個(gè)時(shí)間點(diǎn)(比照客戶端時(shí)間點(diǎn))后本地緩存就作廢了悴务,在這個(gè)時(shí)間點(diǎn)內(nèi)客戶端可以認(rèn)為緩存數(shù)據(jù)有效浆竭,可直接從緩存中加載展示。
不過 Http 緩存頭設(shè)計(jì)并沒有想象的那么規(guī)矩惨寿,像上面提到的 Cache-Control(這個(gè)頭是在Http1.1里加進(jìn)來的)頭里的 no-cache 和 max-age 就是特例,它們既包含緩存存儲(chǔ)策略也包含緩存過期策略删窒,以 max-age 為例裂垦,他實(shí)際上相當(dāng)于:
Cache-Control:public/private
Expires:當(dāng)前客戶端時(shí)間 + maxAge 。
而 Cache-Control:no-cache 和 Cache-Control:max-age=0 (單位是秒)
這里需要注意的是:
- Cache-Control 中指定的緩存過期策略優(yōu)先級(jí)高于 Expires肌索,當(dāng)它們同時(shí)存在的時(shí)候蕉拢,后者會(huì)被覆蓋掉。
- 緩存數(shù)據(jù)標(biāo)記為已過期只是告訴客戶端不能再直接從本地讀取緩存了诚亚,需要再發(fā)一次請(qǐng)求到服務(wù)器去確認(rèn)晕换,并不等同于本地緩存數(shù)據(jù)從此就沒用了,有些情況下即使過期了還是會(huì)被再次用到站宗,具體下面會(huì)講到闸准。
緩存對(duì)比策略
將緩存在客戶端的數(shù)據(jù)標(biāo)識(shí)發(fā)往服務(wù)端,服務(wù)端通過標(biāo)識(shí)來判斷客戶端 緩存數(shù)據(jù)是否仍有效梢灭,進(jìn)而決定是否要重發(fā)數(shù)據(jù)夷家。
客戶端檢測(cè)到數(shù)據(jù)過期或?yàn)g覽器刷新后蒸其,往往會(huì)重新發(fā)起一個(gè) http 請(qǐng)求到服務(wù)器,服務(wù)器此時(shí)并不急于返回?cái)?shù)據(jù)库快,而是看請(qǐng)求頭有沒有帶標(biāo)識(shí)( If-Modified-Since摸袁、If-None-Match)過來,如果判斷標(biāo)識(shí)仍然有效义屏,則返回304告訴客戶端取本地緩存數(shù)據(jù)來用即可(這里要注意的是你必須要在首次響應(yīng)時(shí)輸出相應(yīng)的頭信息(Last-Modified靠汁、ETags)到客戶端)。至此我們就明白了上面所說的本地緩存數(shù)據(jù)即使被認(rèn)為過期闽铐,并不等于數(shù)據(jù)從此就沒用了的道理了蝶怔。
Android中的磁盤緩存
很多時(shí)候我們都會(huì)說一些圖片加載框架使用了兩級(jí)或者三級(jí)緩存,然后就會(huì)說先從內(nèi)存中取阳啥,然后再?gòu)拇疟P中取添谊,最后再?gòu)木W(wǎng)絡(luò)中去取,我們現(xiàn)在按照這個(gè)思路來分析一下Picasso察迟,Picasso一開始就從內(nèi)存中去讀斩狱,然后就會(huì)去進(jìn)行網(wǎng)絡(luò)請(qǐng)求,如果內(nèi)存中沒有讀取到扎瓶,他就會(huì)去生成一個(gè)Request所踊,去請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù),他為什么沒有直接去讀磁盤緩存概荷,這個(gè)時(shí)候你可能會(huì)說番甩,Picasso默認(rèn)沒有設(shè)置磁盤緩存棠涮,只要當(dāng)設(shè)置了OkHttp.Downloader之后才會(huì)進(jìn)行磁盤緩存,實(shí)際上不是這樣的,Picasso是有磁盤緩存的乞娄,因?yàn)樗木彺嬉蕾囉贖TTP緩存機(jī)制,所以每次是在請(qǐng)求之后根據(jù)Response的響應(yīng)頭去看是否讀取內(nèi)存緩存咧最,當(dāng)Picasso在build的時(shí)候翩腐,如果沒有設(shè)置DownLoader,他會(huì)自己去設(shè)置一個(gè)Downloader
public Picasso build() {
Context context = this.context;
if (downloader == null) {
//創(chuàng)建一個(gè)默認(rèn)的Downloader
downloader = Utils.createDefaultDownloader(context);
}
return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}
}
繼續(xù)查看createDefaultDownloader蓝谨,如果沒有OkhttpDownloader灌具,那么就會(huì)采用UrlConnectionDownloader
static Downloader createDefaultDownloader(Context context) {
try {
Class.forName("com.squareup.okhttp.OkHttpClient");
return OkHttpLoaderCreator.create(context);
} catch (ClassNotFoundException ignored) {
}
return new UrlConnectionDownloader(context);
}
然后我們就分開查看,因?yàn)镺kHttpLoader是在UrlConnectionDownloader的基礎(chǔ)上進(jìn)行改良的,所以我們先查看一下UrlConnectionDownloader譬巫,也就是load方法
UrlConnectionDownloader
@Override
public Response load(Uri uri, int networkPolicy) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
installCacheIfNeeded(context);
}
HttpURLConnection connection = openConnection(uri);
connection.setUseCaches(true);
if (networkPolicy != 0) {
String headerValue;
if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
headerValue = FORCE_CACHE;
} else {
StringBuilder builder = CACHE_HEADER_BUILDER.get();
builder.setLength(0);
if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
builder.append("no-cache");
}
if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
if (builder.length() > 0) {
builder.append(',');
}
builder.append("no-store");
}
headerValue = builder.toString();
}
//設(shè)置緩存策略
connection.setRequestProperty("Cache-Control", headerValue);
}
int responseCode = connection.getResponseCode();
if (responseCode >= 300) {
connection.disconnect();
throw new ResponseException(responseCode + " " + connection.getResponseMessage(),
networkPolicy, responseCode);
}
long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
//我們根據(jù)服務(wù)端返回的Response的Header來判斷是走緩存還是重新取數(shù)據(jù)
boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));
return new Response(connection.getInputStream(), fromCache, contentLength);
}
緊接著看一下UrlConnectionDownloader
OkHttpDownloader
@Override
public Response load(Uri uri, int networkPolicy) throws IOException {
CacheControl cacheControl = null;
if (networkPolicy != 0) {
if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
cacheControl = CacheControl.FORCE_CACHE;
} else {
CacheControl.Builder builder = new CacheControl.Builder();
if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
builder.noCache();
}
if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
builder.noStore();
}
cacheControl = builder.build();
}
}
Request.Builder builder = new Request.Builder().url(uri.toString());
if (cacheControl != null) {
//設(shè)置緩存策略
builder.cacheControl(cacheControl);
}
com.squareup.okhttp.Response response = client.newCall(builder.build()).execute();
int responseCode = response.code();
if (responseCode >= 300) {
response.body().close();
throw new ResponseException(responseCode + " " + response.message(), networkPolicy,
responseCode);
}
//是否讀取緩存的標(biāo)志
boolean fromCache = response.cacheResponse() != null;
ResponseBody responseBody = response.body();
return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength());
}
存儲(chǔ)目錄
在開發(fā)Android的過程中咖楣,也會(huì)涉及到很多的IO操作,比如說網(wǎng)絡(luò)請(qǐng)求芦昔,下載圖片等诱贿,由于很多框架平時(shí)已經(jīng)幫我們封裝好了,所以平時(shí)容易忽略咕缎,下面簡(jiǎn)單分析一下Android下的存儲(chǔ)目錄:
內(nèi)部存儲(chǔ)
data文件夾就是我們常說的內(nèi)部存儲(chǔ)瘪松,對(duì)于沒有root的手機(jī)來說咸作,我們是沒有權(quán)限打開這個(gè)文件夾的但是可以訪問到,
外部存儲(chǔ)
外部存儲(chǔ)才是我們平時(shí)操作最多的宵睦,外部存儲(chǔ)一般就是我們上面看到的storage文件夾记罚,當(dāng)然也有可能是mnt文件夾,這個(gè)名稱不影響我們操作數(shù)據(jù)壳嚎。
路徑獲取
兩種存儲(chǔ)方式都是通過Context類來進(jìn)行獲取的
內(nèi)部存儲(chǔ):
getFilesDir();//獲取內(nèi)部存儲(chǔ)的File路徑
getCacheDir();//獲取內(nèi)部存儲(chǔ)的Cache路徑
getDatabasePath("demo.db");//獲取database路徑
getSharedPreferences("demo",MODE_PRIVATE);//獲取SP
外部存儲(chǔ):
getExternalCacheDir();//獲取外部存儲(chǔ)私有目錄
getExternalFilesDir(Environment.DIRECTORY_DCIM);//獲取外部存儲(chǔ)公有目錄
清除緩存/清除數(shù)據(jù)
清除緩存:緩存是程序運(yùn)行時(shí)的臨時(shí)存儲(chǔ)空間桐智,它可以存放從網(wǎng)絡(luò)下載的臨時(shí)圖片,從用戶的角度出發(fā)清除緩存對(duì)用戶并沒有太大的影響烟馅,但是清除緩存后用戶再次使用該APP時(shí)说庭,由于本地緩存已經(jīng)被清理,所有的數(shù)據(jù)需要重新從網(wǎng)絡(luò)上獲取郑趁,注意:為了在清除緩存的時(shí)候能夠正常清除與應(yīng)用相關(guān)的緩存刊驴,請(qǐng)將緩存文件存放在getCacheDir()或者 getExternalCacheDir()路徑下。
清除數(shù)據(jù):清除用戶配置寡润,比如SharedPreferences捆憎、數(shù)據(jù)庫(kù)等等,這些數(shù)據(jù)都是在程序運(yùn)行過程中保存的用戶配置信息梭纹,清除數(shù)據(jù)后躲惰,下次進(jìn)入程序就和第一次進(jìn)入程序時(shí)一樣
關(guān)于權(quán)限
Android6.0以后,谷歌加強(qiáng)了對(duì)用戶權(quán)限的控制变抽,但是這個(gè)權(quán)限只是針對(duì)于外部存儲(chǔ)的公有目錄础拨,對(duì)于私有目錄的,仍然可以正常訪問绍载。所以當(dāng)遇到有些手機(jī)權(quán)限很難適配的時(shí)候可以把文件存儲(chǔ)在外部存儲(chǔ)的私有目錄诡宗。
總結(jié)
磁盤緩存在Android中需要注意訪問外部存儲(chǔ)時(shí)候需要權(quán)限,注意各個(gè)不同路徑下的區(qū)別击儡,同時(shí)需要結(jié)合Http緩存注意緩存的時(shí)效性塔沃。