webP 格式图片在 iOS 中的应用

96
KevinMK
0.1 2015.12.18 18:23* 字数 1189

本篇来源于15年双11手淘前端技术巡演 - H5性能最佳实践(点我打开),它们在采用了 webp格式(点我打开) 的图片后,网络数据传输大幅减少.

以及腾讯发布的 webp探寻之路(点我打开)都可以拿出非常漂亮的数据让我们转到 webP 的怀抱(至少部分)

下面是 taobao 的优化对比图

下面是 腾讯的 图片格式对比图

可以看到有非常的实用价值,下面我们看看如何引入到具体项目中

webp 在 iOS中 的应用

本地采用 webP 图片

下载第三方库

很幸运,SDWebImage里面有个webP 框架,可以将webp-->NSData-->UIImage最后变为可识别的图片格式直接给控件调用

但是我在SDWebImage 的 github(点击跳转)上看见了下面这段话

There are 3 subspecs available now: Core, MapKit and WebP (this means you can install only some of the SDWebImage modules. By default, you get just Core, so if you need WebP, you need to specify it).

Podfile example:

$pod 'SDWebImage/WebP'

也就是说我们需要额外下载这个webP库,注意,这个会从 google 下载,如果失败,开 VPN 试试,下载完成后,文件结构如下

使用

SDWebImage 的源码如下,我们直接使用转化出的 UIImage 即可

#ifdef SD_WEBP

#import <UIKit/UIKit.h>

// Fix for issue #416 Undefined symbols for architecture armv7 since WebP introduction when deploying to device
void WebPInitPremultiplyNEON(void);

void WebPInitUpsamplersNEON(void);

void VP8DspInitNEON(void);

@interface UIImage (WebP)

+ (UIImage *)sd_imageWithWebPData:(NSData *)data;

@end

#endif

别的办法

据了解,还有部分三方库也可以...但是我没有进行研究了,我觉得原理应该都差不多,下面重点要说的是和 taobao 一样,如何在 webView 和网络传输中如何使用webp

在 webView 中实用 webP

先给一个 webP 测试 html: http://testing.yongche.org/webp.html

html 的代码如下

<head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <title>webp</title>
</head>
<body>
        <img src="http://testing.yongche.org/images/150X1501.webp” width = 50 ,height = 20>
        ![](http://testing.yongche.org/images/150X1501.webp)
</body>

第一个办法 和 JS 协作

webView 等网页上如果用的是 webP 图片,iOS 直接解析的话,会显示?图片.


后来决定这么搞:

A. 在网页加载出后截取到 HTML 及内部的 JS 后,调用 JS 预先准备好的方法获取需要转码的 webP 格式图片下载地址(其实一个一个的遍历也行).
B. 在App 本地开启线程下载图片,下载完成后,将图片转码由 webP—> png—>Base64(因为实验出直接用 png/jpg 的话 没用)
C. 将 Base64及原图片下载地址一一对应调用 JS 准备好的方法进行替换
D. 将下载后的图片进行缓存,并进行管理

注意点:
A. 图片在最终显示成功前会显示成?,此处为了用户体验应该采用占位图片
B. 图片显示成功前应该保持网页布局不调整,需要由 JS 预先设置好布局
C. 图片在本地的缓存需要管理

相应的,HTML需要更改代码,如下

<head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <title>webp</title>
        <script type="text/javascript">
            var YongChe = {};
            YongChe.getAllWebPImg = function() {
                var images = document.getElementsByTagName('img'); 
                var srcList = [];
                var patt1 = new RegExp("\.webp$");
                for(var i = 0; i < images.length; i++) {
                    if(patt1.test(images[i].src)) {
                        srcList.push(images[i].src);
                    }
                }
                console.log(srcList);
                return JSON.stringify(srcList);
            }
            YongChe.replaceWebPImg = function(src, localPath) {
                alert(localPath);
                elementList = document.querySelectorAll('img[src="'+src+'"]');
                console.log(elementList);
                for(element in elementList) {
                    elementList[element].src = localPath;
                }
            }
</script>
</head>
<body>
        <img src="http://testing.yongche.org/images/150X1501.webp” width = 50 ,height = 20>
        ![](http://testing.yongche.org/images/150X1501.webp)
        <script type="text/javascript">
            console.log(YongChe.getAllWebPImg());
            YongChe.replaceWebPImg('http://testing.yongche.org/images/logo.png','http://testing.yongche.org/images/150X1501.webp')
        </script>
</body>

App本地也需要做出相应的措施

//在 `webView` 加载完 `HTML` 后,解析源码,执行相应的 `JS` 方法
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
    //获取`HTML`代码
    NSString *lJs = @"document.documentElement.innerHTML";
    NSString *str = [webView stringByEvaluatingJavaScriptFromString:lJs];

    //执行约定好的方法,获取需要下载的 webp 图片
    NSString *imgs = [self.webView stringByEvaluatingJavaScriptFromString:@"YongChe.getAllWebPImg();"];
    NSArray *array = [NSJSONSerialization JSONObjectWithData:[imgs dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
    
    //此处,做示范,只转换第一个,将图片下载下来,并且转为 PNG 后,再转成 Base64,传给 JS 脚本执行
    NSString *imgUrl = array.firstObject;
        
    __weak typeof (self) weakSelf = self;
    
    [SDWebImageCustomeDownLoad downloadWithURL:[NSURL URLWithString:imgUrl] progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
        
        NSString *base = [NSString stringWithFormat:@"data:image/png;base64,%@",imgBase];
        
        NSString *js = [NSString stringWithFormat:@"YongChe.replaceWebPImg('%@','%@')",imageURL,base];
        
        [weakSelf.webView stringByEvaluatingJavaScriptFromString:js];
    }];
       
}

下面这个方法是修改 SDWebImage 自带的,本来是属于 UIImage的分类方法,此处只是提取出下载这个操作,并没有深入更改,大家别介意

+ (void)downloadWithURL:(NSURL *)url progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
{
//    [self sd_cancelCurrentImageLoad];
//    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    if (url) {
//        __weak __typeof(self)wself = self;
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:0 progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
//            if (!wself) return;
            dispatch_main_sync_safe(^{
//                if (!wself) return;
                if (image && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {   //没有回调,但是图片下载完成了
                    
                    
                } else {    //image 下载失败
                    

                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        
        //这一步,将这个 View 之前的下载操作全部取消,然后将这次的操作放进去
        
//        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        dispatch_main_async_safe(^{
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
            if (completedBlock) {
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }

}

效果

只能说成功了,但是看着效果明显很龊,下面说说其他办法

第二种办法 NSURLProtocol

分析

采用 webP 的目的就是减少网络传输数据量,加快请求整体速度,现在的问题就在于 webView 不识别 webP 这种格式而已,再想想 iOS内的所有网络请求/响应我们都可以控制,那我们获取到数据时,转化成它识别的给它就可以了,而可以帮我们在拿到数据时进行某些操作,操作完以后又正常进行网络流程的,就是接下来的主角了 --- NSURLProtocol,如果大家和我一样平时用的少的话,可以看看 JamesYu 的这篇简书 iOS中的 NSURLProtocol

再看 taobao 前辈们的说法:

估计也是这样做的, OK. 那开撸

NSURLProtocol

自定义一个 YCCustomURLProtocol 继承自 NSURLProtocol
#import <Foundation/Foundation.h>

@interface YCCustomURLProtocol : NSURLProtocol

@end
在 delegate 注册下这个协议
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    //注册protocol
    [NSURLProtocol registerClass:[CustomURLProtocol class]];
    return YES;
}
实现协议内容

有几个注意点:

  1. NSURLConnection 活跃的iOS版本,NSURLSession 可以酌情代替之
  2. 不论 NSURLConnection 还是 NSURLSession,尽量把 URLProtocol didFailWithError URLProtocol DidFinishLoading URLProtocol didLoadData URLProtocol didReceiveResponse 几个方法写全,避免网络请求产生不可预知的 BUG
  3. 暂时就这些
NSURLConnection
#import "YCCustomURLProtocol.h"
#import "UIImage+WebP.h"

static NSString *URLProtocolHandledKey = @"URLHasHandle";

@interface YCCustomURLProtocol()<NSURLSessionDelegate,NSURLSessionDataDelegate>

@property (nonatomic,strong) NSURLConnection *connection;

@end

@implementation YCCustomURLProtocol

#pragma mark 初始化请求

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    //只处理http和https请求
    NSString *scheme = [[request URL] scheme];
    if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
          [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
    {
        //看看是否已经处理过了,防止无限循环
        if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        return YES;
    }
    return NO;
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

#pragma mark 通信协议内容实现

- (void)startLoading
{
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    //标示改request已经处理过了,防止无限循环
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];

    if (![[self.request.URL absoluteString] hasSuffix:@"webp"]) {
        NSLog(@"正常请求,让它 goon ");
    }else{
        NSLog(@"检测到了 webp---%@,拦截它",self.request.URL);
    }
    
    self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];    
}

- (void)stopLoading
{
    [self.connection cancel];
}

#pragma mark 请求管理 connection

- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {

    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
    if ([connection.currentRequest.URL.absoluteString hasSuffix:@"webp"]) {
        NSLog(@"webp---%@---替换它",connection);
        UIImage *imgData = [UIImage sd_imageWithWebPData:data];
        NSData *jpgData = UIImageJPEGRepresentation(imgData, 1.0f);
        [self.client URLProtocol:self didLoadData:jpgData];
    }else{
        [self.client URLProtocol:self didLoadData:data];
    }
}

- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

NSURLSession
#import "YCCustomURLProtocol.h"
#import "UIImage+WebP.h"

static NSString *URLProtocolHandledKey = @"URLHasHandle";

@interface YCCustomURLProtocol()<NSURLSessionDelegate,NSURLSessionDataDelegate>

@property (nonatomic,strong) NSURLSession *session;

@end

@implementation YCCustomURLProtocol

#pragma mark 初始化请求

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    //只处理http和https请求
    NSString *scheme = [[request URL] scheme];
    if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
          [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame))
    {
        //看看是否已经处理过了,防止无限循环
        if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        return YES;
    }
    return NO;
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

#pragma mark 通信协议内容实现

- (void)startLoading
{
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    //标示改request已经处理过了,防止无限循环
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];

    if (![[self.request.URL absoluteString] hasSuffix:@"webp"]) {
       NSLog(@"是一个正常的,%@",self.request.URL);
    }else{
        NSLog(@"检测到了 webp---%@,拦截它",self.request.URL);
    }
    
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue currentQueue]];
    [[self.session dataTaskWithRequest:mutableReqeust] resume];
    
}

- (void)stopLoading
{
    [self.session invalidateAndCancel];
}

#pragma mark dataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{    
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
    
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    
    NSData *transData = data;

    if ([dataTask.currentRequest.URL.absoluteString hasSuffix:@"webp"]) {
        NSLog(@"webp---%@---替换它",dataTask.currentRequest.URL);
        //采用 SDWebImage 的转换方法
        UIImage *imgData = [UIImage sd_imageWithWebPData:data];
        transData = UIImageJPEGRepresentation(imgData, 1.0f);
    }

    [self.client URLProtocol:self didLoadData:transData];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error{

    if (error) {
        
        [self.client URLProtocol:self didFailWithError:error];
    }else{
        
        [self.client URLProtocolDidFinishLoading:self];
    }
    
}

实际效果

结尾语

ok, 至此已经全部成功,不过 NSURLProtocol 会影响 App 内所有请求,稍有遗漏的话后果比较严重,大家一定要慎重.不过相比效果,电商类 App 采用大量图片的话还是值得转换到 webP 阵营的

谢谢观看,个人水平有限,如有遗漏/不妥,请指出

PS:原创文章,转载请注明地址: https://kevinmky.github.io

iOS-二三事
Web note ad 1