首先,這是一個基於具體業務的組件優化方案,我儘可能把業務邏輯從代碼中抽離出來,部分地方代碼可能有刪減。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適配的相關文章博客,都只是介紹簡單的功能性適配,因此,並無獲得什麼好的思路。緩存
因而,在三週的時間裏,我一直邊測試邊優化,在沒有初步方案的狀況下,一點點完成功能,最終整理代碼,解耦組件,整理出一套效果很是理想,而且使用方便的解決方案。服務器
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的跡象,暫時還不太明朗。
因爲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圖片的獲取。相關規則能夠參考七牛開發文檔。
首先考慮,請求的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都是全局網絡控制。
篇幅略長,具體緩存處理放在下一篇介紹。