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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容