美文网首页第三方库
iOS 里使用WebP(含 WebP 介绍)

iOS 里使用WebP(含 WebP 介绍)

作者: MTSu1e丶 | 来源:发表于2019-04-23 13:38 被阅读0次

    前言

    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 厉害,究竟厉害在哪里?

    相关文章

      网友评论

        本文标题:iOS 里使用WebP(含 WebP 介绍)

        本文链接:https://www.haomeiwen.com/subject/upumgqtx.html