在iOS 客戶端基於 WebP 圖片格式的流量優化(上)這篇文章中,已經介紹了WebP格式圖片的下載使用,僅僅只有這樣還遠遠不夠,還須要對已經下載的圖片數據進行緩存。web
曾經有句名言『計算機世界有兩大難題,第一是起名字,第二是寫一個緩存』,鄙人不能贊成更多。segmentfault
在iOS上,重寫一份圖片緩存是不現實的,而直接修改SDWebImage框架也是不太好的。因此,在SDWebImage的基礎上添加一箇中間層CacheManager比較好。緩存
我感受,緩存的難度在於,如何準確命中。的確在開發的時候,一大半時間都是在測試緩存命中狀況,測試自己就挺麻煩,須要在模擬器的沙盒裏面看文件,同時斷網測試,須要一些調試技巧,不少技巧並麼有辦法詳盡表述出來,須要所謂的悟性去理解。網絡
這一部分,因爲SD下載圖片的方法中,url被替換,因此要看懂SD自己的代碼,是何時給緩存一個肯定的key。發如今session
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock { if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } if (![url isKindOfClass:NSURL.class]) { url = nil; } url = [url qd_replaceToWebPURLWithScreenWidth]; ......
方法中,肯定了緩存的key值多線程
NSString *key = [self cacheKeyForURL:url]; operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
這也就是以前,爲何要在這個方法的最前面把URL替換掉,這樣,SD的key值已是保護WebP格式的圖片URL,這一部分的緩存均可以正常使用,不須要修改。架構
因此,難度仍是在WebView的圖片緩存中,由於以前雖然是用SD託管WebView中WebP圖片的下載,然而WebView讀緩存卻不能自動從SDImageCache中讀取。這樣,須要用NSURLCache來接管WebView的圖片緩存。app
關於WebView的緩存,系統提供了一個類,NSURLCache。這個類能夠在全部的網絡請求前查看緩存,而且決定是否緩存(注意:是全部請求)。具體的NSURLCache用法,動動勤勞的小手Google一下,不少文章能夠參考。框架
咱們本身的實現,直接上代碼async
@implementation QDURLCache /** * 請求完成決定是否要將response進行存儲 */ - (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request { NSString* ua = [request valueForHTTPHeaderField:@"User-Agent"]; if (!EmptyString(ua) && [ua lf_containsSubString:@"AppleWebKit"]) { //判斷本次請求是否是請求圖片 if ([[QDCacheManager defaultManager] isImageRequest:request]) { [[QDCacheManager defaultManager] storeImageResponse:cachedResponse forRequest:request]; return; } //其餘請求 if ([[QDCacheManager defaultManager] shouldCacheExceptImageResponseForRequest:request]) { if (![[QDCacheManager defaultManager] storeCachedResponse:cachedResponse forRequest:request]) { [super storeCachedResponse:cachedResponse forRequest:request]; return; } else { return; } } } [super storeCachedResponse:cachedResponse forRequest:request]; } /** * 每次發請求以前會調此方法,查看本地是否有緩存 */ - (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request { NSString* ua = [request valueForHTTPHeaderField:@"User-Agent"]; if (!EmptyString(ua) && [ua lf_containsSubString:@"AppleWebKit"]) { if ([[QDCacheManager defaultManager] isImageRequest:request]) { //圖片 //從本地取圖片 NSCachedURLResponse *imageCacheResponse = [[QDCacheManager defaultManager] retrieveImageCacheResponseForRequest:request]; if (imageCacheResponse) { return imageCacheResponse; } else { return [super cachedResponseForRequest:request]; } } if ([[QDCacheManager defaultManager] shouldCacheExceptImageResponseForRequest:request]) { //其它緩存的東西 //判斷本地自定義緩存目錄是否存在 if (![[QDCacheManager defaultManager] cacheAvaliableForRequest:request]) { NSCachedURLResponse *response = [super cachedResponseForRequest:request]; //判斷本地系統緩存目錄是否存在 if (response.data) { BOOL contentLengthValid = [((NSHTTPURLResponse *)response.response) expectedContentLength] == [response.data length]; //判斷是不是有效的文件 if (!contentLengthValid) { return response; } //將系統緩存放到自定義的緩存目錄中 [[QDCacheManager defaultManager] storeCachedResponse:response forRequest:request]; } else { } return response; } //從本地緩存中取出對應的緩存 NSCachedURLResponse *cachedResponse = [[QDCacheManager defaultManager] retrieveCachedResponseForRequest:request]; if (cachedResponse) { return cachedResponse; } } } return [super cachedResponseForRequest:request]; } - (void)removeCachedResponseForRequest:(NSURLRequest *)request { if ([[QDCacheManager defaultManager] cacheAvaliableForRequest:request]) { if (![[QDCacheManager defaultManager] removeCachedResponseForRequest:request]) { LogI(@"Failed to remove local cache for request: %@", request.URL); } } else { [super removeCachedResponseForRequest:request]; } } @end
這段代碼並無多麼難以理解的地方,能夠看出來,咱們是新建了一箇中間層QDCacheManager,來管理WebView的全部緩存。
並且,既然是全局影響,確定要用UA包起來,防止誤傷其餘緩存。
這一段代碼在調試的時候有個技巧,就是全部super方法的調用,在測試階段,所有直接return,防止WebView自身的緩存干擾調試結果。這個方法在不少緩存處理的地方都須要注意,別的地方但凡出現了調用super方法的,調試中也一概是直接return的。
既然已經用QDCacheManager託管了緩存,URLCache類的任務就已經完成,儲存Response由
- (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request
而下面:
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
在NSURLProtocol的startLoading方法執行以前,就調用了。很好理解,由於這個方法就是取緩存的方法,天然是先取,沒有再去Loading。
這裏的邏輯,必須經過大量調試,反覆驗證,不能簡單套用別人的結論,甚至官方文檔也要懷疑的態度來看。由於,不少第三方框架,會影響NSURLCache類,我在調試時,就發現,JSPatch,React Native還有咱們的一個放劫持服務,都有可能影響這個類中方法的調用。
下面就轉入咱們本身的緩存管理方法中去,因爲如今關注的是WebP圖片問題,因此,其餘緩存處理就再也不展開。
關於這個中間層,主要處理的實際就是緩存key的問題,由於請求的時候,request裏的URL仍然是沒有替換WebP的,因此,須要先用以前qd_defultWebPURLCacheKey方法來獲取真實圖片緩存key值。
思路的關鍵就是換key,再取cache,代碼自己就只能靠功底了。
直接上代碼,沒什麼好解釋的。
- (BOOL)isImageRequest:(NSURLRequest *)request { if (![request.URL.absoluteString qd_isQdailyHost]) { return NO; } NSArray *extensions = @[@".jpg", @".jpeg", @".png", @".gif"]; for (NSString *extension in extensions) { if ([request.URL.absoluteString.lowercaseString lf_containsSubString:extension]){ return YES; } } return NO; } - (void)storeImageResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request { NSString *key = [request.URL qd_defultWebPURLCacheKey]; if ([_imageCache imageFromDiskCacheForKey:key]) { return; } dispatch_async([_imageCache currentIOQueue], ^{ // 硬盤緩存直接存data,webp格式;內存緩存爲UIImage,能夠直接使用 [_imageCache storeImageDataToDisk:cachedResponse.data forKey:key]; }); } - (NSCachedURLResponse *)retrieveImageCacheResponseForRequest:(NSURLRequest *)request { NSString *key = [request.URL qd_defultWebPURLCacheKey]; NSString *defaultPath = [_imageCache defaultCachePathForKey:key]; NSData *data = nil; if ([_imageCache imageFromMemoryCacheForKey:key]) { UIImage * image = [_imageCache imageFromMemoryCacheForKey:key]; if ([key lf_containsSubString:@".png"]) { data = UIImagePNGRepresentation(image); } else { data = UIImageJPEGRepresentation(image, 1.0); } } if (data && data.length != 0) { NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:[request.URL.absoluteString qd_MIMEType] expectedContentLength:data.length textEncodingName:nil]; return [[NSCachedURLResponse alloc] initWithResponse:response data:data]; } data = [NSData dataWithContentsOfFile:defaultPath]; if (data == nil) { data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]]; } if (data == nil || data.length == 0) { [_imageCache removeImageForKey:key fromDisk:YES]; return nil; } NSURLResponse *response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:[request.URL.absoluteString qd_MIMEType] expectedContentLength:data.length textEncodingName:nil]; return [[NSCachedURLResponse alloc] initWithResponse:response data:data]; }
其中currentIOQueue方法,是修改了一下SDImageCache,暴露這個IOQueue,原來的框架是沒有這個方法的。
至於爲何圖片硬盤緩存直接用data,由於這裏考慮是性能問題,取緩存的時候,返回的NSURLResponse所攜帶的,確定仍是NSData,若是當時存了UIImage格式,內部同樣是轉碼成了NSData,而取的時候,仍是按UIImage格式取,再轉成NSData返回,至關於多了兩次轉碼。
內存緩存卻沒有這個問題,由於SD的內存緩存,用的NSCache,存的就是UIImage對象,能夠直接取出來用。
這裏其實仍然並無什麼好講的,仍是基本的邏輯問題,須要比較嚴謹地處理。
咱們的app是實現了wifi預加載了,然而這一部分也須要與上面完成的緩存體系通用,否則,wifi預加載的意義就不大。
首先,咱們的wifi預加載,是本身寫了一個URLSession,因此在下載前替換URL就能夠
for (NSString *urlString in resourcesArray) { if ([urlString isKindOfClass:[NSString class]]) { NSURL *theURL = [NSURL URLWithString:urlString]; if ([[QDCacheManager defaultManager] isImageRequest:[NSURLRequest requestWithURL:theURL]]) { theURL = [theURL qd_replaceToWebPURLWithScreenWidth]; } if(![[QDCacheManager defaultManager] cacheAvaliableForURL:theURL] && ![[SDImageCache sharedImageCache] diskImageExistsWithKey:theURL.absoluteString]) { __weak QDPrefetcher* weakSelf = self; [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { ......
部分代碼如上,關鍵也在於替換URL時機和判斷緩存狀況。而下載以後的文件存到哪,是須要處理的。
#pragma mark NSURLSession Delegate - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSError *error = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *destinationPath = nil; if ([[QDCacheManager defaultManager] isImageRequest:downloadTask.originalRequest]) { NSString *key = downloadTask.originalRequest.URL.absoluteString; destinationPath = [[SDImageCache sharedImageCache] defaultCachePathForKey:key]; } else { destinationPath = [[QDCacheManager defaultManager] localCachedWebContentPathWithRequest:downloadTask.originalRequest]; } if ([fileManager fileExistsAtPath:destinationPath]) { [fileManager removeItemAtPath:destinationPath error:nil]; } [fileManager copyItemAtPath:location.path toPath:destinationPath error:&error]; }
我是在finish的方法裏面,把圖片下載的目錄直接copy給SDImageCache的緩存目錄。這樣,SD的緩存裏面就有了這些WebP格式的NSData,與以前的代碼邏輯統一,格式統一。
首先有了一個心得,看上去很複雜的功能,可能實際代碼並不須要本身寫多少,學會在前人的基礎上再加工,好比咱們如今這套WebP適配,底層仍然是SDWebImage的基本邏輯,咱們只不過在上層,加一些判斷和處理,來適應業務層豐富的功能。
並且,代碼是一步步寫出來的,提早設想的方案,並不必定能實現,先實現功能,再優化架構,纔是正確的方向。當時在WebURLProtocol裏面,繞了很大的彎子,甚至還涉及到了多線程問題,不當心發現了iOS8,9,10三個版本的內部實現都在變化,繞開了一個個坑,才逐步清晰了整個邏輯。
總結整個方案的邏輯,其實比較清晰:
首先肯定是否是須要被替換的圖片URL,而後全部的替換都採用統一方法,與之配套的key,也用這套方法處理獲得他被替換後的URL,保證命中。
而後,不管Native請求仍是WebView請求,都用SD託管,避免兩套處理邏輯形成的種種不肯定性;
而WebView的緩存,經過一箇中間層處理,再交給SDImageCache,使之與Native請求的數據統一,讓兩種圖片請求公用一套緩存,進一步重用。
思路大體如此,其餘的問題,就須要靠代碼能力了。
一口氣寫完,有不完善的地方,可能往後會有部分修改。確定有不少大神的代碼寫得更好,或者寫了更好的方案,也但願多多交流,共同進步。