在使用iOS的webview的時(shí)候發(fā)現(xiàn)這樣一個(gè)問題,加載一個(gè)網(wǎng)頁進(jìn)來,webview會負(fù)責(zé)緩存頁面里的css,js和圖片這些資源。但是這個(gè)緩存不受開發(fā)者控制,緩存時(shí)間非常短.所以為了節(jié)省用戶流量,大量的Hybird混合應(yīng)用和電商類應(yīng)用都在研究H5頁面熱更新和圖片交由本地保存的策略,今天我們來研究一下如何緩存webivew的圖片資源。
第一種方式:NSURLCache
作為iOS御用的緩存類,NSURLCache給我們提供了一個(gè)簡單的緩存實(shí)現(xiàn)方式,但在使用的時(shí)候,某些情況下,應(yīng)用中的系統(tǒng)組件會將緩存的內(nèi)存容量設(shè)為0MB,這就禁用了緩存。解決這個(gè)行為的一種方式就是通過自己的實(shí)現(xiàn)子類化NSURLCache,拒絕將內(nèi)存緩存大小設(shè)為0。
在我們僅僅為了實(shí)現(xiàn)一個(gè)緩存圖片的類的時(shí)候,我們的代碼極其簡單,就是繼承NSURLCache,重載下面這兩個(gè)方法,就實(shí)現(xiàn)類圖片緩存和讀?。?/p>
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
NSString *pathString = [[request URL] absoluteString];
if(![pathString hasSuffix:@".jpg"] || ![pathString hasSuffix:@".png"]) {
return[super cachedResponseForRequest:request];
}
if([[BGURLCache sharedCache] hasDataForURL:pathString]) {
NSData *data = [[BGURLCache sharedCache] dataForURL:pathString];
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[request URL]
MIMEType:[pathString hasSuffix:@".jpg"]?@"image/jpg":@"image/png"
expectedContentLength:[data length]
textEncodingName:nil];
return [[NSCachedURLResponse alloc] initWithResponse:response data:data];
}
return[super cachedResponseForRequest:request];
}
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request {
NSString *pathString = [[request URL] absoluteString];
if(![pathString hasSuffix:@".jpg"] || ![pathString hasSuffix:@".png"]) {
[super storeCachedResponse:cachedResponse forRequest:request];
return;
}
[[BGURLCache sharedCache] storeData:cachedResponse.data forURL:pathString];
}
NSURLCache的坑
使用NSURLCache做緩存看起來簡單又好用,但是為什么各種大佬都不建議用呢?具體到我這個(gè)需求,是因?yàn)橄旅孢@幾個(gè)坑:
1.只能用在get請求里面,post可以洗洗睡了。
2.需要服務(wù)器定義數(shù)據(jù)是否發(fā)生變化,需要在請求頭里查找是否修改了的信息。公司服務(wù)器沒有定義的話,就不能夠判斷讀取的緩存數(shù)據(jù)是否需要刷新。
3.刪除緩存的removeCachedResponseForRequest 這個(gè)方法是無效的.所以緩存是不會被刪除的—只有刪除全部緩存才有效。
總結(jié)
不能刪除對應(yīng)的緩存方案是沒有意義的,所以我放棄了這個(gè)方案。
第二種方案:NSURLProtocol
NSURLProtocol或許是URL加載系統(tǒng)中最功能強(qiáng)大但同時(shí)也是最晦澀的部分了。它是一個(gè)抽象類,你可以通過子類化來定義新的或已經(jīng)存在的URL加載行為。還好我們的需求只是做一個(gè)圖片的緩存需求,不然就抓瞎了,在具體到這個(gè)類的時(shí)候我們要做的也很簡單,攔截圖片加載請求,轉(zhuǎn)為從本地文件加載。
1.我們要認(rèn)識NSURLProtocol,首先它是一個(gè)抽象類,不能夠直接使用必須被子類化之后才能使用。子類化 NSURLProtocol 的第一個(gè)任務(wù)就是告訴它要控制什么類型的網(wǎng)絡(luò)請求。比如說如果你想要當(dāng)本地有資源的時(shí)候請求直接使用本地資源文件,那么相關(guān)的請求應(yīng)該對應(yīng)已有資源的文件名。
這部分邏輯定義在
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
中,如果返回 YES,該請求就會被其控制。返回 NO 則直接跳入下一個(gè)Protocol,一句話,我們可以在里面完成攔截圖片加載請求,轉(zhuǎn)為從本地文件加載的大概邏輯。
2.獲取和設(shè)置一個(gè)請求對象的當(dāng)前狀態(tài),可以在Protocol的各種方法中傳遞當(dāng)前request我們自定義的狀態(tài)。核心方法是:
+ (nullable id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
3.最最重要的方法
-startLoading
-stopLoading
不同的自定義子類在調(diào)用這兩個(gè)方法是會傳入不同的內(nèi)容,但共同點(diǎn)都是要圍繞protocol的client屬性進(jìn)行操作,在對-startLoading 和-stopLoading的實(shí)現(xiàn)中,需要在恰當(dāng)?shù)臅r(shí)候讓client調(diào)用每一個(gè)delegate方法。我們在startloading中初始化NSURLSessionDataTask,在session的代理方法中傳遞數(shù)據(jù)給client的代理方法。在stoploading中結(jié)束當(dāng)前datatask。
4.向系統(tǒng)注冊該NSURLProtocol,當(dāng)請求被加載時(shí),系統(tǒng)會向每一個(gè)注冊過的protocol詢問是否能控制該請求,第一個(gè)通過+canInitWithRequest: 回答為 YES 的protocol就會控制該請求。URLProtocol會被以注冊順序的反序訪問,所以當(dāng)在 -application:didFinishLoadingWithOptions:方法中調(diào)用 [NSURLProtocol registerClass:[BGURLProtocol class]]; 時(shí),你自己寫的protocol比其他內(nèi)建的protocol擁有更高的優(yōu)先級。
4.核心代碼
BGURLProtocol.m:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *urlString = request.URL.absoluteString;
NSString* extension = request.URL.pathExtension;
if([NSURLProtocol propertyForKey:@"ProtocolHandledKey" inRequest:request]) {
return NO;
}
BOOL isImage = [@[@"png", @"jpeg", @"gif", @"jpg"] indexOfObjectPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
return [extension compare:obj options:NSCaseInsensitiveSearch] == NSOrderedSame;
}] != NSNotFound;
if (isImage)
{
NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
filePath = [filePath stringByAppendingPathComponent:[[urlString componentsSeparatedByString:@"/"] lastObject]];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath])
{
return YES;
}
else
{
static NSInteger requestCount = 0;
static NSInteger requestRefresh = 0;
NSMutableURLRequest *newRequest = [request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:@"ProtocolHandledKey" inRequest:newRequest];
NSString *url = [@"http://www.baidu.com/img/" stringByAppendingString:[[urlString componentsSeparatedByString:@"/"] lastObject]];
requestCount++;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
requestRefresh = 1;
if (requestCount == 0)
{
[[NSNotificationCenter defaultCenter] postNotificationName:kImageDownloadNotification object:[[urlString componentsSeparatedByString:@"/"] lastObject]];
}
});
[[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:url] options:SDWebImageDownloaderUseNSURLCache progress:^(NSInteger receivedSize, NSInteger expectedSize) {
} completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
requestCount--;
NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
filePath = [filePath stringByAppendingPathComponent:[[urlString componentsSeparatedByString:@"/"] lastObject]];
[data writeToFile:filePath atomically:YES];
if (requestCount == 0 && requestRefresh == 1)
{
[[NSNotificationCenter defaultCenter] postNotificationName:kImageDownloadNotification object:[[urlString componentsSeparatedByString:@"/"] lastObject]];
}
}];
}
return YES;
}else{
return YES;
}
}
-(void)startLoading
{
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:@"ProtocolHandledKey" inRequest:newRequest];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self
delegateQueue:[[NSOperationQueue alloc] init]];
self.connection = [session dataTaskWithRequest:newRequest];
[self.connection resume];
}
- (void)stopLoading {
[self.connection cancel];
self.connection =nil;
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
NSURLResponse *retResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:response.URL.absoluteString] statusCode:httpResponse.statusCode HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:httpResponse.allHeaderFields];
[self.client URLProtocol:self didReceiveResponse:retResponse cacheStoragePolicy:NSURLCacheStorageNotAllowed];
} else {
NSURLResponse *retResponse = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:response.URL.absoluteString] MIMEType:response.MIMEType expectedContentLength:response.expectedContentLength textEncodingName:response.textEncodingName];
[self.client URLProtocol:self didReceiveResponse:retResponse cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
// 請求完成,成功或者失敗的處理
if (error) {
[self.client URLProtocol:self didFailWithError:error];
}else{
[self.client URLProtocolDidFinishLoading:self];
}
}
webview初始化:
NSString *htmlString = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.baidu.com"] encoding:NSUTF8StringEncoding error:nil];
htmlString = [htmlString stringByReplacingOccurrencesOfString:@"http://www.baidu.com/img/" withString:@""];
_htmlString = htmlString;
NSString *baseUrl = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
[webView loadHTMLString:htmlString baseURL:[NSURL fileURLWithPath:baseUrl]];
5.我們現(xiàn)在實(shí)現(xiàn)了用NSURLProtocol攔截.png和.jpg的網(wǎng)絡(luò)請求,讓UIWebView本身的圖片下載發(fā)不出去,攔截的鏈接通過SDWebImage下載資源到本地目錄,用WebView的loadHTMLString:baseURL:方法來實(shí)現(xiàn)讀取本地目錄的圖片顯示,當(dāng)下載圖片超過2秒,并且請求數(shù)為0時(shí)發(fā)送通知給webView刷新顯示本地資源。但是,現(xiàn)在已經(jīng)iOS11了啊,UIWebView內(nèi)存占用太大已經(jīng)跟不上時(shí)代了,WKWebiview默認(rèn)不支持NSURLProtocol,所以我們得找個(gè)辦法讓W(xué)K支持我們子類話的protocol。所以我找到了這段:
//Class cls = NSClassFromString(@"WKBrowsingContextController");
Class cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 把 http 和 https 請求交給 NSURLProtocol 處理
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];
}
這樣,我們就完成了webview緩存圖片資源的需求。
參考資料:
http://bbs.csdn.net/topics/390831054
http://blog.csdn.net/jason_chen13/article/details/51984823
https://github.com/Yeatse/NSURLProtocol-WebKitSupport
http://blog.csdn.net/xanxus46/article/details/51946432
http://blog.csdn.net/u011661836/article/details/70241061