iOS 里使用WebP(含 WebP 介绍)

前言

webp格式,谷歌开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器宽带资源和数据空间。是一种同时提供了有损压缩无损压缩(可逆压缩)的图片档案格式,衍生自影像编码格式VP8,被认为是WebM多媒体格式的姊妹项目,是由Google以BSD授权条款释出。VP8编解码器的其中一个强大特性是帧内预测压缩,或者说,视频的每一帧都被压缩,后续帧与帧之间的差异也会被压缩。这就是WebP的由来:WebM文件里单个被压缩的帧。更精确的说WebP的核心来则WebM。
webp最初在2010年释出,目标是减少档案大小,达到和JPEG格式相同的图片品质,希望能够减少图片档在网路上的传送时间。

原理

一、有损压缩
1.宏块(MacroBlocking)

图片编码第一个阶段的任务是把图片分割成不同的宏块。常见的就是包括一个‘16×16’的亮度像素块和两个‘8×8’的色度像素块,如下图:

宏块

亮度像素块(luma)工作原理:指的是像素抠像,把一幅含有RGB通道的图像转换为单通道的黑白图像,这幅黑白图像就代表着这幅图像的亮部和暗部区域。
色度像素块(chroma)工作原理:指的是RGB空间,就是把 R、G、B 放到数学里面的立体坐标系上,然后可以把对应的RGB 表示的图像放到这个坐标系上,就可以通过这个坐标系来观察和分析整幅图像的红绿蓝颜色分布了,因为引入了立体坐标系,所以可以使用研究立体几何的方法来研究和处理图像了。

2.预测

宏块里每个4x4的子块都有一个预测模型。(又名过滤)。在PNG里过滤用得非常多,它对每一行都做同样的事,而WebP过滤的是每一块。它是这样处理的,在一个块周围定义两组像素:有一行在它上面为A,在它左边那一列为L。如图:
预测模型

利用 A 和 L,编码器会将他们放在一个4x4的测试像素块填满,并确定哪一个生成了最接近原始块的值。这些用不同方法填满的块叫做"预测块"。

WebP 编码器四种帧内预测模式:
(1)Horiz prediction(水平预测):将块的每列使用左列L数据的副本进行填充。
(2)Vertical Prediction(垂直预测):将块的每行使用上列A数据的副本进行填充。
(3)DC Prediction(DC 预测):将块使用 A 上列的像素与 L 左列的像素的平均值作为宏块唯一的值进行填充。
(4)True Motion (TrueMotion 预测):除了行 A 和列 L 之外,用宏块上方和左侧的像素P、A(从P开始)中像素块之间的水平差异以列 L 为基准拓展每一行。

预测方式
3.处理 DCT

当图片处理到此处时,还剩下小的残差,通过 FDCT (正向离散余弦变换),让变换后的数据低频部分分布在数据块左上方,而高频部分集中于右下方实现更高效的压缩。在DCT阶段输入的数据不是原始的数据块本身,而是预测后的数据。WebP的预测阶段相比JPG是最大的优势,它减少了特殊颜色,使得在以后的处理阶段能更有效的压缩图片数据。WebP只是比JPG所有处理过程多了一个预测模式,在数据压缩方面就比JPG优秀很多。

4.有损压缩图片与 jpg 体积比较
webp有损压缩与jpg比较

总结:当WebPJPG 压缩到相当于原图 90% 质量时,图片体积减少了 50% 左右。当WebPJPG 压缩到相当于原图 80% 质量时,图片体积则减少了 60%~80%。
有损 WebP 压缩性能优于JPG 的原因主要是其预测编码技术先进,并且宏块自适应量化也带来了压缩效率的提升。

二、无损压缩

WebP无损压缩采用了预测变换、颜色变换、减去绿色变换、彩色缓存编码、LZ77 反向参考等不同技术来处理图像,之后对变换图像数据和参数进行熵编码。

1.预测变换

预测空间变换通过利用相邻像素的数据相关性减少熵[shāng]。在预测变换中,对已解码的像素预测当前像素值,并且仅对差值(实际预测)进行编码。预测变换有 13 种不同的模式,使用较多的是左、上、左上以及右上的像素预测模式,其余为左、上、左上和右上组合的平均值预测模式。

2.颜色变换

借助颜色变换去除每个像素的 R,G 和 B 值。彩色变换时保持绿色(G)值原样,根据绿色(G)值变换红色(R)值,再根据绿色值转换蓝色(B)值,最后根据红色(R)值进行转换。
如果与预测变换的情况一样,就需要将图像划分为宏块,并且对于宏块中的所有像素使用相同的变换模式。变换模式分为 3 种:green_to_red,green_to_blue和red_to_blue。

3.减去绿色变换

减去绿色变换从每个像素的红色、蓝色值中减去绿色值。当此变换存在时,解码器需要将绿色值添加到红色和蓝色。

4.彩色缓存编码

无损 WebP 压缩使用已经看到的图像片段来重构新的像素。如果没有找到对应的匹配值,可以使用本地调色板,同时本地调色板也会不断更新最近使用的颜色。

5.无损压缩图片与PNG体积比较
无损webp与PNG对比

总结:WebP 无损对 PNG 图片的优化到达了 20%~40% 。

三、WebP 与主流的图片格式功能对比
WebP 与主流的图片格式功能对比
四、iOS 里面原生控件对 webp 的支持
1.SDWebImage 可以直接支持

$ pod 'SDWebImage/WebP'

2.遇到的问题及解决办法

问题:
查看SDWebImage.podspec pod 配置文件,可知道 SDWebImage 支持 webp 格式图片是依赖于其 Webp子库,而子库进一步依赖于 libwebp 库。libwebp 是来自谷歌的被墙的,源地址为 https://chromium.googlesource.com/webm/libwebp

解决办法:

添加终端翻墙:
~ git config --global http.proxy 'socks5://127.0.0.1:1086' `socks5://127.0.0.1:1086要查看你翻墙软件里面的配置`
~ git config --global https.proxy 'socks5://127.0.0.1:1086'

然后 pod install

另外提供删除终端翻墙
~ git config --global --unset http.proxy
~ git config --global --unset https.proxy
3.原生控件使用

(1)使用方式
直接使用 SDWebImagesd_setImageWithURL系列方法即可加载 webp 图片。
(2)原理
pod之后看SDWebImageCodersManagerSDWebImageWebPCoder

SDWebImageCodersManager类里面

初始化:
- (instancetype)init {
    if (self = [super init]) {
        // initialize with default coders
        NSMutableArray<id<SDWebImageCoder>> *mutableCoders = [@[[SDWebImageImageIOCoder sharedCoder]] mutableCopy];
#ifdef SD_WEBP
        [mutableCoders addObject:[SDWebImageWebPCoder sharedCoder]];
#endif
        _coders = [mutableCoders copy];
        _codersLock = dispatch_semaphore_create(1);
    }
    return self;
}
注:在`SDWebImageCodersManager`初始化的地方,判断如果当前环境支持 webp就会加上`[SDWebImageWebPCoder sharedCoder]`
这个`SDWebImageWebPCoder `是内置的加载 webp 图片或者webp动画的编码器
把 data 解码成 image
- (UIImage *)decodedImageWithData:(NSData *)data {
    LOCK(self.codersLock);
    NSArray<id<SDWebImageCoder>> *coders = self.coders;
    UNLOCK(self.codersLock);
    for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
        if ([coder canDecodeFromData:data]) {
            return [coder decodedImageWithData:data];
        }
    }
    return nil;
}
注:判断如果 webp 的 coder 可以解码此 data 的话就进入 code 里面进行解码

SDWebImageWebPCoder类里面

解码成 image
- (UIImage *)decodedImageWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    
    WebPData webpData;
    WebPDataInit(&webpData);//初始化一个有缺省值的 webp_data
    webpData.bytes = data.bytes;
    webpData.size = data.length;
    WebPDemuxer *demuxer = WebPDemux(&webpData);//解析“data”给出的完整WebP文件,成功解析后返回WebPDemuxer对象,否则返回NULL。WebPDemuxer包含图的宽高,colorSpace(解码之后图片的像素格式)等属性
    if (!demuxer) {
        return nil;
    }
    
    uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS);//获取 webpFeatureFlags 的位操作组合
    
    CGColorSpaceRef colorSpace = [self sd_colorSpaceWithDemuxer:demuxer];//获取解码之后图片的像素格式
    
    if (!(flags & ANIMATION_FLAG)) {
        // for static single webp image
        UIImage *staticImage = [self sd_rawWebpImageWithData:webpData colorSpace:colorSpace];
        WebPDemuxDelete(demuxer);
        CGColorSpaceRelease(colorSpace);
        staticImage.sd_imageFormat = SDImageFormatWebP;
        return staticImage;
    }
    
    int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT);//WEBP_FF_LOOP_COUNT ->用于动画文件
    int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH);
    int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
    CGBitmapInfo bitmapInfo;//指定bitmap是否包含alpha通道,像素中alpha通道的相对位置,像素组件是整形还是浮点型等信息的字符串。
    // `CGBitmapContextCreate` does not support RGB888 on iOS. Where `CGImageCreate` supports.
    if (!(flags & ALPHA_FLAG)) {
        // RGBX8888
        bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
    } else {
        // RGBA8888
        bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
    }
    
    CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo);//绘制图片上下文,第一个参数创建BitmapContext的内存空间,第二个图片的宽度,第三个图片的高度,第四个内存中像素的每个组件的位数.例如,对于32位像素格式和RGB 颜色空间,应该将这个值设为8.,第五个是每一行在内存所占的比特数,第六个是colorSpace
    if (!canvas) {
        WebPDemuxDelete(demuxer);
        CGColorSpaceRelease(colorSpace);
        return nil;
    }
    
    // for animated webp image
    WebPIterator iter;
    if (!WebPDemuxGetFrame(demuxer, 1, &iter)) {
        WebPDemuxReleaseIterator(&iter);
        WebPDemuxDelete(demuxer);
        CGContextRelease(canvas);
        CGColorSpaceRelease(colorSpace);
        return nil;
    }
    
    NSMutableArray<SDWebImageFrame *> *frames = [NSMutableArray array];
    
    do {
        @autoreleasepool {
            UIImage *image = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace];
            if (!image) {
                continue;
            }
            
            int duration = iter.duration;
            if (duration <= 10) {
                // WebP standard says 0 duration is used for canvas updating but not showing image, but actually Chrome and other implementations set it to 100ms if duration is lower or equal than 10ms
                // Some animated WebP images also created without duration, we should keep compatibility
                duration = 100;
            }
            SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:image duration:duration / 1000.f];
            [frames addObject:frame];
        }
        
    } while (WebPDemuxNextFrame(&iter));
    
    WebPDemuxReleaseIterator(&iter);
    WebPDemuxDelete(demuxer);
    CGContextRelease(canvas);
    CGColorSpaceRelease(colorSpace);
    
    UIImage *animatedImage = [SDWebImageCoderHelper animatedImageWithFrames:frames];
    animatedImage.sd_imageLoopCount = loopCount;
    animatedImage.sd_imageFormat = SDImageFormatWebP;
    
    return animatedImage;
}
注:整个解码功能的实现是依赖于 libwebp 库。

4.WebView 使用

在以 Native 方式开发的 App 中也会大量使用的 UIWebView 来展示一些简单页面,然而 Safari 及 UIWebView 当前并不支持 WebP 格式。若是想在 UIWebView 中也把图片显示出来,一个解决思路就是:拦截替换。拦截 WebP 图片然后转换为 jpg 或者 png 再交给 UIWebView 进行渲染和展示。
(1)使用方式:
使用MagicWebViewWebP.framework导入到项目中。
引用头文件:#import <MagicWebViewWebP/MagicWebViewWebPManager.h>

在 webview 加载之前,注册MagicURLProtocol

_web = [[UIWebView alloc]initWithFrame:CGRectMake(0, 200, ScreenWidth, 300)];
[self.view addSubview:_web];

[[MagicWebViewWebPManager shareManager] registerMagicURLProtocolWebView:_web];

NSURLRequest *req = [NSURLRequest requestWithURL:[NSURLURLWithString:@"http://isparta.github.io/compare-webp/index.html#12345"]];
[_web loadRequest:req];

dealloc中销毁MagicURLProtocol

// 销毁
-(void)dealloc{
    [[MagicWebViewWebPManager shareManager] unregisterMagicURLProtocolWebView: _web];
}
注:若有特殊需求,需要在web页面退出时销毁MagicURLProtocol,否则会拦截整个app的网络请求。

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

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

代码如下:

//在 `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 *imgBase = [UIImagePNGRepresentation(image) base64EncodedStringWithOptions:0];
    NSString *base = [NSString stringWithFormat:@"data:image/png;base64,%@",imgBase];

    NSString *js = [NSString stringWithFormat:@"YongChe.replaceWebPImg('%@','%@')",imageURL,base];

    [weakSelf.webView stringByEvaluatingJavaScriptFromString:js];

  }];
}

+ (void)downloadWithURL:(NSURL *)url progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {

  if (url) {

    id operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:0 progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

      dispatch_main_sync_safe(^{

        if (image && completedBlock){

          completedBlock(image, error, cacheType, url);

          return;

        }else if (image) {  //没有回调,但是图片下载完成了

        } else {    //image 下载失败

        }

        if (completedBlock && finished) {

          completedBlock(image, error, cacheType, url);

        }

      });

    }];

  //这一步,将这个 View 之前的下载操作全部取消,然后将这次的操作放进去

  } 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);

      }

    });

  }

}

方法2(推荐):
NSURLProtocol可以拦截的网络请求包括NSURLSession,NSURLConnection以及UIWebVIew。

步骤:

注册—>拦截—>转发—>回调—>结束
参考MagicWebViewWebP.framework的实现方式

注:NSURLProtocol 的全局性质,影响范围大,这种方式存在潜在的风险,需要严格的过滤和限制 WebP 请求的拦截。NSURLProtocol 作用的叠加性质,也无法保证与其它第三方代码的兼容。每次只能只有一个protocol进行处理,如果有多个自定义protocol,系统将采取你registerClass的倒序进行调用,一旦你需要对这个请求进行处理,那么接下来的所有相关操作都需要这个protocol进行管理。

参考文章

app图片优化-webp格式图片原理及在Android、IOS中的应用
MagicWebViewWebP
WebP 极限压缩及ios实现
都说 WebP 厉害,究竟厉害在哪里?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容