緩存webview圖片資源的兩種方式

在使用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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容