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请求的数据统一,让两种图片请求公用一套缓存,进一步重用。

思路大体如此,其余的问题,就须要靠代码能力了。


一口气写完,有不完善的地方,可能往后会有部分修改。确定有不少大神的代码写得更好,或者写了更好的方案,也但愿多多交流,共同进步。

相关文章
相关标签/搜索