iOS 客户端基于 WebP 图片格式的流量优化(上)

首先,这是一个基于具体业务的组件优化方案,我尽可能把业务逻辑从代码中抽离出来,部分地方代码可能有删减。css

如今这个方案是用于一个多图片的新闻类应用,粗略估计过,用户在浏览完第一页全部新闻(共48篇),会消耗流量达100M,其中98M为图片,这里值得优化的空间很是大。html

针对这种状况,咱们前后使用过的优化包含:wifi条件下预载全部文章、图片和js、css数据;重用全部已经下载的js、css和图片的缓存;后台图片的压缩。android

后台压缩和WebP化依赖第三方多媒体处理服务器,已知比较好的国内服务有腾讯优图和七牛。这里咱们采用的七牛的服务。web

咱们的后台经过七牛的图片压缩(包含质量和分辨率),咱们将首页流量由100m减小到了80m,依然有极大的提高空间。所以客户端采用基于WebP的流量压缩方案,将流量由80m压缩到了20m,减小了75%!相对于最初的处理,流量减小了80%!(android大多数机型支持WebP animated,压缩能达到80%,但iOS的解码对于WebP animated图片支持并很差,常常会出现失败的状况,因此iOS最终压缩率取决于首页中gif图的个数和大小,实际测试结果,优化幅度大概60%-80%之间)segmentfault

在准备作这项优化以前,查阅过不少资料,发现WebP适配的相关文章博客,都只是介绍简单的功能性适配,因此,并无获得什么好的思路。缓存

因而,在三周的时间里,我一直边测试边优化,在没有初步方案的状况下,一点点完成功能,最终整理代码,解耦组件,整理出一套效果很是理想,而且使用方便的解决方案。服务器

1、了解 WebP

WebP,是一种同时提供了有损压缩与无损压缩的图片文件格式,是Google新推出的影像技术,它可以让网页图档有效进行压缩,同时又不影响图片格式兼容与实际清晰度,进而让总体网页下载速度加快。网络

  • WebP 无损压缩的图片能够比一样大小的 PNG 小 26%;工具

  • WebP 有损压缩的图片能够比一样大小的 JPEG 小 25-34%;测试

  • WebP 支持无损的透明图层通道,代价只需增长 22% 的字节存储空间;

  • WebP 有损透明图像能够比一样大小的 PNG 图像小3倍。

WebP在Native支持方面上,早已比较成熟,听说淘宝客户端在两年前就使用了WebP(主要是Native使用),后来H5全面使用,WebView的WebP采用插件的方式支持。

在安卓上,WebP的支持是很是简单的,毕竟都是谷歌的东西,本身固然要支持,可是在iOS的WebKit内核(UIWebView和WKWebView)上,是不能直接支持的。不过最近传言macOS 10.12上的Safari有测试WebP的迹象,暂时还不太明朗。

2、准备工做

因为OS X不支持原生WebP解码,因此,能够先安装一个工具。推荐使用Homebrew,具体使用参考 http://brew.sh/index_zh-cn.html

安装完成后,使用命令

$brew install webp

就能够安装libwebp了。

客户端方面,Native图片加载使用的SDWebImage,该组件直接支持WebP的解码。须要在将预编译宏’WebP’置为1,并在pod中引入’iOS-WebP’便可。

服务端方面,咱们采用七牛图片服务器,默认传给客户端的参数是一张jpg或者png的图片连接,经过修改url的请求参数实现对WebP图片的获取。相关规则能够参考七牛开发文档。

3、具体方案实现

首先考虑,请求的webp图片是经过url参数拼接完成的,因此,须要对客户端内请求的全部图片URL作处理,必须所有命中。并且,未来的缓存也应基于此URL进行处理,因此,添加一个NSURL分类,URL的处理由这个分类统一处理,全部的URL替换最终都会指向这个分类中的方法,耦合度基本能够将至最低。

@interface NSURL (ReplaceWebP)
- (NSURL *)qd_replaceToWebPURLWithScreenWidth;
- (NSString *)qd_defultWebPURLCacheKey;
- (BOOL)qd_isShouldReplaceImageFormat;
@end

下面是替换URL和缓存key的核心处理代码

static NSString * const qdHost = @"img.host.com";
@implementation NSURL (ReplaceWebP)
- (NSString *)qd_defultWebPURLCacheKey {
    if (![self qd_isShouldReplaceImageFormat]) {
        return self.absoluteString;
    }
    NSString *key;
    if ([self isWebPURL]) {
        key = self.absoluteString;
    } else {
        key = [self qd_replaceToWebPURLWithScreenWidth].absoluteString;
    }
    return key;
}
- (NSURL *)qd_replaceToWebPURLWithImageWidth:(int)width {
  
    if ([self qd_isShouldReplaceImageFormat]) {
        NSString *urlStr;
        
        if ([self URLStringcontainFomartString:@"?"]) {
            if ([self URLStringcontainFomartString:@"format/jpg"]) {
                urlStr = [self.absoluteString stringByReplacingOccurrencesOfString:@"format/jpg" withString:@"format/webp"];
            } else {
                NSString *suffixStr = @"imageView2/0/format/webp/ignore-error/1";
                urlStr = [NSString stringWithFormat:@"%@/%@", self.absoluteString, suffixStr];
            }
        } else {
            NSString *pathExtension = [[self.absoluteString.pathExtension componentsSeparatedByString:@"-"] firstObject];
            urlStr = [NSString stringWithFormat:@"%@.%@-WebPiOSW%d",self.absoluteString.stringByDeletingPathExtension, pathExtension, width];
        }
        return [NSURL URLWithString:urlStr];
    }
    return self;
}
- (NSURL *)qd_replaceToWebPURLWithScreenWidth {
    
    int width = (int)([UIScreen mainScreen].bounds.size.width * [UIScreen mainScreen].scale);
    return [self qd_replaceToWebPURLWithImageWidth:(int)width];
}

全部的URL替换,最终都会到 - (NSURL *)qd_replaceToWebPURLWithImageWidth:(int)width 这个方法中来

下面是条件过滤,确保100%命中全部须要替换的图片格式

- (BOOL)isQDHost {
    NSString *nsModel = [UIDevice currentDevice].model;
    BOOL s_isiPad = [nsModel hasPrefix:@"iPad"];
    if (s_isiPad) return NO;
    return [self URLStringcontainFomartString:qdHost];
}
- (BOOL)qd_isShouldReplaceImageFormat {
    
    if (![self isQDHost]) {
        return NO;
    }
    if ([self isWebPURL]) {
        return NO;
    }
    NSArray *extensions = @[@".jpg", @".jpeg", @".png"];
    for (NSString *extension in extensions) {
        if ([self.absoluteString.lowercaseString rangeOfString:extension options:NSCaseInsensitiveSearch].location != NSNotFound){
            return YES;
        }
    }
    return NO;
}
- (BOOL)URLStringcontainFomartString:(NSString *)string {
    return ([self.absoluteString.lowercaseString rangeOfString:string options:NSCaseInsensitiveSearch].location != NSNotFound);
}
- (BOOL)isWebPURL {
    return [self URLStringcontainFomartString:@"-webp"] || [self URLStringcontainFomartString:@"/webp"];
}
@end

因此,替换URL这个功能,被彻底抽离出来,以后的代码,只须要考虑具体逻辑的问题了。

2. Native 图片请求替换

Native图片加载使用的SDWebImage,首先须要理解SD的代码,肯定是最终的图片下载是调用的哪一个方法

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                 options:(SDWebImageOptions)options
                                progress:(SDWebImageDownloaderProgressBlock)progressBlock
                               completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

全部的图片下载,最终都走到了这个方法中,因此,替换URL应该在这个方法的最前面实现。

{
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    url = [url qd_replaceToWebPURLWithScreenWidth];
    ...
    ...
}

因为在评估了难度以后,咱们果断地把SDWebImage从Pods中移除,手动添加一个子工程,这样能够比较方便地修改内部实现,而不至于用swizzling这种黑魔法来修改传入参数。这个技能虽然炫酷,然而不少状况下,杀敌一万,自损两万,不建议常用。

因修改了url值,若在上层经过SDImageCache判断是否有本地缓存时,也须要对url先作qd_defultWebPURLCacheKey来获取其真实缓存的key。这一部分比较简单。

3. WebView 图片请求替换

这一部分是这个方案的难度所在。

webkit内核如今都不支持解析WebP格式的图片,这里主要采用的iOS系统的NSURLProtocol来替换其网络请求(不了解NSURLProtocol,能够动动本身勤劳的小手Google一下),再将网络回包数据进行转码成jpg或者png(为了透明度),再返回给webview进行渲染的。

友情连接,NSURLProtocol用法,大神文章

一样的,iOS在此处依然不对gif进行任何处理。

另外,NSURLProtocol会拦截全局的网络流量,为避免误伤,这里须要单独识别是不是WebView发起的请求,能够经过识别request中的UA是否包含”AppleWebKit”来实现。

@implementation QDWebURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    
    NSString *ua = [request valueForHTTPHeaderField:@"User-Agent"];
    if ([request.URL qd_isShouldReplaceImageFormat] && [ua lf_containsSubString:@"AppleWebKit"]) {
        return YES;
    }
}

这里能够接管全部WebView中须要替换的图片URL。

下面,会自动调用startLoading方法,这里采用了一个很是特别的方式处理

- (void)startLoading {
    if ([self.request.URL qd_isShouldReplaceImageFormat]) {
        [[SDWebImageManager sharedManager] downloadImageWithURL:self.request.URL
                                                        options:0
                                                       progress:nil
                                                      completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
                                                    {
                                                          NSData *data;
                                                          if ([imageURL.absoluteString.lowercaseString lf_containsSubString:@".png"]) {
                                                              data = UIImagePNGRepresentation(image);
                                                          } else {
                                                              data = UIImageJPEGRepresentation(image, 1);
                                                          }
                                                          [self.client URLProtocol:self didLoadData:data];
                                                          [self.client URLProtocolDidFinishLoading:self];
                                                      }];
        return;
    }
    self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
}

是否是很奇特,由SDWebImageManager直接接管图片请求,手动finishLoading。

首先须要明确,WebP节约流量,到底是怎么样的原理:

所谓图片格式,是采用何种解码编码方式决定的,全部数据最终必定是变成二进制数据,NSData;
既然UIWebView不支持解码WebP,咱们可让图片在网络中以WebP格式的NSData传递,本地收到data后,解码成UIWebView能够识别的UIImage;事实上,Native方面就是这么作就能够达到目标了,然而在WebView的请求中,不管咱们本地作了何种处理,最终交给WebView的也必定是NSData,因此,须要再把UIImage编码成jpg或者png(之因此咱们没有把gif也转WebP,就是由于从WebP的动图UIImage,转码成NSData这条路走不通,因而咱们放弃了gif转WebP)。

因此,大体的数据路径以下:

本地发送WebP请求 ---> Server ---> 返回WebP格式Data ---> Data经谷歌的WebP decode获得UIImage ---> 将UIImage对象编码成JPG或PNG格式NSData ---> 替换本应交给WebView的WebP格式Data ---> WebView接收JPG或PNG格式Data ---> 渲染图片

在最开始,这里并非这么写的,当时是在系统的

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data

方法中转码处理。按这个思路写,代码越写越散,BUG也愈来愈多。因此,换了个思路,既然SD能够支持WebP,为何不用他来全面托管呢?

这样的话,原生请求和WebView的图片缓存也能够经由SD统一块儿来,因此,这应该是一个好的方案。

这样的话,WebP的全部请求都已经能够处理(wifi预加载暂时无论,由于是本身写的downloader,替换URL后直接改把缓存指向修改就能够),以后要处理缓存的问题

4. 图片缓存处理

之前的代码已经实现了内部文章的缓存,包含js、css以及image等。这里经过NSURLCache来实现。相应的,基于WebP的图片缓存的读取也应该在NSURLCache中处理,在先处理完URL后,用新的Key来进行映射。

这里建议全部基于WebView的流量优化都最好用UA的判断包住,避免带来问题。由于不管NSURLProtocol仍是NSURLCache都是全局网络控制。

篇幅略长,具体缓存处理放在下一篇介绍。

相关文章
相关标签/搜索