摘要:在客户端基于图片格式的流量优化上这篇文章中,已经介绍了格式图片的下载使用,仅仅只有这样还远远不够,还需要对已经下载的图片数据进行缓存。二图片缓存关于的缓存,系统提供了一个类,。而且,既然是全局影响,肯定要用包起来,防止误伤其他缓存。
在iOS 客户端基于 WebP 图片格式的流量优化(上)这篇文章中,已经介绍了WebP格式图片的下载使用,仅仅只有这样还远远不够,还需要对已经下载的图片数据进行缓存。
曾经有句名言『计算机世界有两大难题,第一是起名字,第二是写一个缓存』,鄙人不能同意更多。
在iOS上,重写一份图片缓存是不现实的,而直接修改SDWebImage框架也是不太好的。所以,在SDWebImage的基础上添加一个中间层CacheManager比较好。
我感觉,缓存的难度在于,如何准确命中。的确在开发的时候,一大半时间都是在测试缓存命中情况,测试本身就挺麻烦,需要在模拟器的沙盒里面看文件,同时断网测试,需要一些调试技巧,很多技巧并么有办法详尽表述出来,需要所谓的悟性去理解。
一、SDWebImage缓存处理这一部分,由于SD下载图片的方法中,url被替换,所以要看懂SD本身的代码,是什么时候给缓存一个确定的key。发现在
- (id)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的图片缓存。
二、WebView图片缓存关于WebView的缓存,系统提供了一个类,NSURLCache。这个类可以在所有的网络请求前查看缓存,并且决定是否缓存(注意:是所有请求)。具体的NSURLCache用法,动动勤劳的小手Google一下,很多文章可以参考。
我们自己的实现,直接上代码
@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图片问题,所以,其他缓存处理就不再展开。
三、中间层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对象,可以直接取出来用。
这里其实仍然并没有什么好讲的,还是基本的逻辑问题,需要比较严谨地处理。
四、其他情况的特别处理我们的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请求的数据统一,让两种图片请求公用一套缓存,进一步重用。
思路大致如此,其他的问题,就需要靠代码能力了。
一口气写完,有不完善的地方,可能日后会有部分修改。肯定有很多大神的代码写得更好,或者写了更好的方案,也希望多多交流,共同进步。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/61789.html
摘要:一了解,是一种同时提供了有损压缩与无损压缩的图片文件格式,是新推出的影像技术,它可让网页图档有效进行压缩,同时又不影响图片格式兼容与实际清晰度,进而让整体网页下载速度加快。这里建议所有基于的流量优化都最好用的判断包住,避免带来问题。 首先,这是一个基于具体业务的组件优化方案,我尽量把业务逻辑从代码中抽离出来,部分地方代码可能有删减。 现在这个方案是用于一个多图片的新闻类应用,粗略估计过...
摘要:开启验证上传一张新图片,使用手安卓版本访问已支持域名的图片,如果请求带了,检查返回图片格式是否为如果旧的图片未按预期返回,返回了或原图可能是结点缓存,正常天后过期回源则会返回图片。 对于图片较多的网站,本文结合具体案例给出了如何基于CDN的sharpP自适应图片无痛接入方案,据统计效果可在原图基础上节省60%-75%的流量。作者:陈忱 出处:腾云阁文章 目前移动端运营素材大部分依赖图...
摘要:的支持程度实际上比你想的可能要好得多。的安卓浏览器从版本起开始官方支持最初发布于年月,版本起开始部分支持。安卓版从起开始支持。而且目前并无添加支持的任何打算。浏览器市场份额截至年月的数据显示,占有约的市场份额,以约位居第二。 本文转载自:众成翻译译者:文蔺链接:http://www.zcfy.cc/article/862原文:https://optimus.keycdn.com/sup...
阅读 775·2021-11-24 09:38
阅读 941·2021-11-11 11:01
阅读 3148·2021-10-19 13:22
阅读 1490·2021-09-22 15:23
阅读 2806·2021-09-08 09:35
阅读 2745·2019-08-29 11:31
阅读 2102·2019-08-26 11:47
阅读 1543·2019-08-26 11:44