在iOS 客户端基于 WebP 图片格式的流量优化(上)这篇文章中,已经介绍了WebP格式图片的下载使用,仅仅只有这样还远远不够,还需要对已经下载的图片数据进行缓存。

曾经有句名言『计算机世界有两大难题,第一是起名字,第二是写一个缓存』,鄙人不能同意更多。

在iOS上,重写一份图片缓存是不现实的,而直接修改SDWebImage框架也是不太好的。所以,在SDWebImage的基础上添加一个中间层CacheManager比较好。

我感觉,缓存的难度在于,如何准确命中。的确在开发的时候,一大半时间都是在测试缓存命中情况,测试本身就挺麻烦,需要在模拟器的沙盒里面看文件,同时断网测试,需要一些调试技巧,很多技巧并么有办法详尽表述出来,需要所谓的悟性去理解。

一、SDWebImage缓存处理

这一部分,由于SD下载图片的方法中,url被替换,所以要看懂SD本身的代码,是什么时候给缓存一个确定的key。发现在

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)urloptions:(SDWebImageOptions)optionsprogress:(SDWebImageDownloaderProgressBlock)progressBlockcompleted:(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.URLMIMEType:[request.URL.absoluteString qd_MIMEType]expectedContentLength:data.lengthtextEncodingName: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.URLMIMEType:[request.URL.absoluteString qd_MIMEType]expectedContentLength:data.lengthtextEncodingName: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请求的数据统一,让两种图片请求公用一套缓存,进一步重用。

思路大致如此,其他的问题,就需要靠代码能力了。

iOS 客户端基于 WebP 图片格式的流量优化(下)相关推荐

  1. 关于webp图片格式初探

    前言 不管是 PC 还是移动端,图片一直是流量大头,以苹果公司 Retina 产品为代表的高 PPI 屏对图片的质量提出了更高的要求,如何保证在图片的精细度不降低的前提下缩小图片体积,成为了一个有价值 ...

  2. WebP图片格式处理和兼容使用

    WebP图片格式处理和兼容使用 用了这么久的WebP图片,最近在项目中发现对其了解得还是很不够,便以此文整理记录WebP相关知识点. (github:https://github.com/Michea ...

  3. 如何在移动端使用WebP图片格式

    前言 在移动端,图片一直是流量大头,一个简单的运营网页,图片大小动不动就以MB为单位,为了加快网页呈现的速度,我们必须使用最适合图片质量,这里所说的合适指图片的清晰度和大小达到合格的要求. 前端常常会 ...

  4. 什么是WebP图片格式?如何在线把Webp格式转换为JPEG格式?

    我们有时候从互联网上下载图片会发现图片是WebP格式而不是常见的JPEG或者是PNG格式,用自带的图片处理软件无法打开,那么什么是WebP格式呢?我们该如何打开WebP格式的图片文件?需要进行图片处理 ...

  5. webp图片格式在手持设备性能测试

    cocos2d-x最新支持了webp图片格式,google在2010年发布的这个图片格式具备比jpg和png更高的压缩比,并且支持alpha通道. 图片体积对比: 原始图片(map_008_BG_2. ...

  6. 如何让Ubuntu系统支持WebP图片格式

    如何让Ubuntu系统支持WebP图片格式 本文主要向大家介绍如何让 Ubuntu 系统支持查看 WebP 图片格式,以及如何将 WebP 转为 JPEG 或 PNG 图片格式的方法. 什么是WebP ...

  7. 【Android 安装包优化】WebP 图片格式性能测试 ( 测试 WebP 图片解码速度 | 测试 WebP 图片编码速度 )

    文章目录 一.测试 WebP 图片解码速度 二.测试 WebP 图片编码速度 三.参考资料 测试结果 : WebP 格式图片 , 解码快 , 编码慢 , 占用空间小 ; 在解码速度上 , WebP 格 ...

  8. 【Android 安装包优化】WebP 图片格式兼容与性能 ( Android 中的 WebP 图片格式兼容问题 | Android 中的 WebP 图片格式性能 )

    文章目录 一.Android 中的 WebP 图片格式兼容问题 二.Android 中的 WebP 图片格式性能 三.参考资料 一.Android 中的 WebP 图片格式兼容问题 在 Android ...

  9. 【Android 安装包优化】WebP 图片转换 ( 使用 iSparta 转换 WebP 图片格式 | Google 提供的 libwebp 库 )

    文章目录 一.使用 iSparta 转换 WebP 图片格式 二.Google 提供的 libwebp 库 三.参考资料 一.使用 iSparta 转换 WebP 图片格式 isparta 工具已经停 ...

最新文章

  1. PyTorch 源码解读之 torch.serialization torch.hub
  2. 使程序变为后台运行代码
  3. Qt Designer中部件的tabletTracking和mouseTracking属性
  4. 对PostgreSQL源代码中的build_jion_rel的理解
  5. 应用filestream设置时存在未知错误_开机黑屏?常见启动黑屏错误的中文解释!学习电脑知识电脑小匠...
  6. 固定时间减当前时间有没有超72小时_上海龙湖英迪格酒店正式部署畅捷固定资产管理系统...
  7. python 位运算符与逻辑运算符(字符串的逻辑运算)
  8. WebRTC之linux ARM64交叉编译(七)
  9. 没有博士学位,照样玩转TensorFlow深度学习
  10. c语言编写dnf辅助,DNF辅助脚本怎么制作?游戏简易脚本制作教程
  11. 浅学transcad(交通小区的划分)
  12. Linux搭建Redis集群(搭建集群必看)
  13. vue 里面的slot属性
  14. 2022还不知道如何申请注册公司域名邮箱,个人域名邮箱怎么弄?详解域名邮箱
  15. html鼠标手状态,css鼠标样式cursor介绍(鼠标手型)
  16. 基于SABR模型的期权波动率曲线套利策略
  17. 自动驾驶仿真软件SCANeR studio(初级练习1):scenario构建之driver模式理解
  18. linux 文件转换ascii,linux 小技巧(查找替换文件中的ascii编码字符)
  19. 学计算机容易得什么病,长时间看电脑容易引起哪些常见的眼科疾病
  20. 树状数组 区间加 区间求和_EXCEL统计区间个数的专属函数

热门文章

  1. NSIS:迅雷5.8.6.600自由定制版脚本及下载
  2. stk6.1安装方法
  3. C++语法(二十一)友元的三种实现
  4. 波特率9600是什么意思 串口通信为什么要设置波特率
  5. mybatis jar下载
  6. php多表查询性能优化,MSSQL_SQL Server多表查询优化方案集锦,SQL Server多表查询的优化方案是 - phpStudy...
  7. BATCH: A Scalable Asymmetric DiscreteCross-Modal Hashing--文献翻译
  8. 「实战」谷歌广告账户可以退款吗?怎么退款?
  9. 华南理工大学php,华南理工大学网络教育平台v3
  10. 电视盒子 android tv6,电视盒子到底是什么?智能电视/盒子究竟究竟该选谁?