iOS 客戶端基於 WebP 圖片格式的流量優化(下)

iOS 客戶端基於 WebP 圖片格式的流量優化(上)這篇文章中,已經介紹了WebP格式圖片的下載使用,僅僅只有這樣還遠遠不夠,還須要對已經下載的圖片數據進行緩存。web

曾經有句名言『計算機世界有兩大難題,第一是起名字,第二是寫一個緩存』,鄙人不能贊成更多。segmentfault

在iOS上,重寫一份圖片緩存是不現實的,而直接修改SDWebImage框架也是不太好的。因此,在SDWebImage的基礎上添加一箇中間層CacheManager比較好。緩存

我感受,緩存的難度在於,如何準確命中。的確在開發的時候,一大半時間都是在測試緩存命中狀況,測試自己就挺麻煩,須要在模擬器的沙盒裏面看文件,同時斷網測試,須要一些調試技巧,不少技巧並麼有辦法詳盡表述出來,須要所謂的悟性去理解。網絡

1、SDWebImage緩存處理

這一部分,因爲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

2、WebView圖片緩存

關於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圖片問題,因此,其餘緩存處理就再也不展開。

3、中間層CacheManager處理

關於這個中間層,主要處理的實際就是緩存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對象,能夠直接取出來用。

這裏其實仍然並無什麼好講的,仍是基本的邏輯問題,須要比較嚴謹地處理。

4、其餘狀況的特別處理

咱們的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請求的數據統一,讓兩種圖片請求公用一套緩存,進一步重用。

思路大體如此,其餘的問題,就須要靠代碼能力了。


一口氣寫完,有不完善的地方,可能往後會有部分修改。確定有不少大神的代碼寫得更好,或者寫了更好的方案,也但願多多交流,共同進步。

相關文章
相關標籤/搜索