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都是全局網絡控制。

篇幅略長,具體緩存處理放在下一篇介紹。

相關文章
相關標籤/搜索