Create by Kunming
寫在最前面
血與淚的教訓(xùn)烟瞧,這篇文章所涉及的方法翻車了绣溜。按照這個(gè)方法是可以實(shí)現(xiàn)WKWebView展示W(wǎng)ebp文件竞漾,但致命的問題在于在一旦注冊(cè) http(s) scheme 后放钦。由于 WKWebView 在獨(dú)立進(jìn)程里執(zhí)行網(wǎng)絡(luò)請(qǐng)求岩榆。一旦注冊(cè) http(s) scheme 后错负,網(wǎng)絡(luò)請(qǐng)求將從 Network Process 發(fā)送到 App Process,這樣 NSURLProtocol 才能攔截網(wǎng)絡(luò)請(qǐng)求勇边。在 webkit2 的設(shè)計(jì)里使用 MessageQueue 進(jìn)行進(jìn)程之間的通信犹撒,Network Process 會(huì)將請(qǐng)求 encode 成一個(gè) Message,然后通過 IPC 發(fā)送給 App Process。出于性能的原因粒褒,encode 的時(shí)候 HTTPBody 和 HTTPBodyStream 這兩個(gè)字段被丟棄掉了识颊。這樣就導(dǎo)致由 WKWebView 發(fā)起的所有 http(s)請(qǐng)求都會(huì)通過 IPC 傳給主進(jìn)程 NSURLProtocol 處理,導(dǎo)致 post 請(qǐng)求 body 被清空!你沒聽錯(cuò)^确亍祥款!是清空。這個(gè)問題導(dǎo)致了我們短暫的需要把某些前端的POST請(qǐng)求短暫更改為GET月杉。因此刃跛,本來WKWebView都是沒有開放對(duì)NSURLProtocol的支持,強(qiáng)制讓W(xué)KWebView支持可能會(huì)出現(xiàn)不可預(yù)料的問題苛萎。老實(shí)聽一句勸桨昙,不要往坑里跳。
以下內(nèi)容被證實(shí)過有坑
留個(gè)念想腌歉,但不要按以下方法實(shí)踐蛙酪。
背景
今天收到一個(gè)反饋,我們基于WKWebView開發(fā)的瀏覽沒有辦法展示包含Webp格式的網(wǎng)頁翘盖。但是安卓端展示是沒有問題的桂塞。這就問到了一個(gè)直擊靈魂的問題”為什么安卓仔可以“。因此馍驯,特地研究一下為何WKWebView不能展示webp格式文件以及如何讓W(xué)KWebView正常展示webp文件藐俺。
什么是webp
WebP格式炊甲,谷歌開發(fā)的一種旨在加快圖片加載速度的圖片格式(怪不得安卓仔可以支持,原來是同一個(gè)老爸生的)欲芹。圖片壓縮體積大約只有JPEG的2/3卿啡,并能節(jié)省大量的服務(wù)器寬帶資源和數(shù)據(jù)空間。Facebook Ebay等知名網(wǎng)站已經(jīng)開始測試并使用WebP格式菱父。同樣的圖片質(zhì)量但是占用空間小2/3颈娜,這無疑對(duì)開發(fā)者和設(shè)計(jì)來說都是福音。但很遺憾浙宜,這個(gè)格式iOS并不支持官辽。
實(shí)現(xiàn)思路
正是因?yàn)閕OS不支持WebP格式的圖片,因此我們就得考慮通過其他方法來展示粟瞬。通過攔截請(qǐng)求過濾出Webp文件的請(qǐng)求鏈接同仆,將文件下載后通過重定向?qū)⑾螺d下來的數(shù)據(jù)轉(zhuǎn)換成PNG或JPEG格式然后交付給瀏覽器渲染。
如何攔截請(qǐng)求
首先攔截請(qǐng)求我們第一時(shí)間肯定想到的是通過NSURLProtocol裙品。聲明一個(gè)繼承于NSURLProtocol的類俗批,在該類中實(shí)現(xiàn)+ (BOOL)canInitWithRequest:(NSURLRequest*)request
進(jìn)行攔截。這個(gè)方法是自定義NSURLProtocol的入口市怎。如果在這個(gè)方法內(nèi)返回YES岁忘,URL loading system
會(huì)把這個(gè)請(qǐng)求操作都交由這個(gè)自定義NSURLProtocol處理
NSString*const kWebPprefix =@".webp";
//該方法返回YES則表示 需要進(jìn)行處理,返回NO区匠,則 不做任何處理
+ (BOOL)canInitWithRequest:(NSURLRequest*)request {
//攔截.webp后綴文件
if([request.URL.absoluteString hasSuffix:kWebPprefix]) {
//如果是webp自定義協(xié)議干像,則不需要過濾
if([NSURLProtocol propertyForKey:@"URLProtocolHandleKey"inRequest:request]) {
return NO;
}
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
這個(gè)方法主要是用來返回格式化好的request。這個(gè)方法必須實(shí)現(xiàn)驰弄,不然會(huì)直接閃退麻汰。如果自己沒有特殊需求的話,直接返回當(dāng)前的request就好了戚篙。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
接下來需要重寫- (void)startLoading
方法五鲫,顧名思義在自定義的NSURLProtocol
拿到了request的操作權(quán)之后,在這個(gè)方法里就是執(zhí)行攔截請(qǐng)求后的操作了已球。
同理- (void)stopLoading
這個(gè)就是請(qǐng)求結(jié)束時(shí)的方法臣镣。
如何下載webP文件
其實(shí)有挺多第三方庫實(shí)現(xiàn)了webP文件的下載辅愿,我們平時(shí)熟悉的SDWebImage
智亮、YYImage
都對(duì)webP文件進(jìn)行支持適配。以SDWebImage為例点待,SDWebImage 的子庫SDWebImage/WebP
就是專門為支持WebP開發(fā)的阔蛉。但值得注意的是,SDWebImage本身不會(huì)幫我們加入項(xiàng)目中癞埠,需要我們另外引入才能使用状原。
// 通過pod引入
pod'SDWebImage/WebP'
引入項(xiàng)目后結(jié)合上面攔截請(qǐng)求的流程聋呢,在自定義的NSURLProtocol
中聲明一個(gè)@property (nonatomic , strong) id <SDWebImageOperation> downOperation;
屬性管理webP文件的下載。
//該方法中對(duì)webp圖片進(jìn)行相應(yīng)的處理
- (void)startLoading {
// 重定向請(qǐng)求
NSMutableURLRequest*mRequest = [[self request]mutableCopy];
self.downOperation= [[SDWebImageManager sharedManager]loadImageWithURL:[self request].URL options:0 progress:nil completed:^(UIImage*_Nullable image,NSData*_Nullable data,NSError*_Nullable error,SDImageCacheType cacheType,BOOL finished,NSURL*_Nullable imageURL) {
// 攔截webp, 將webp格式轉(zhuǎn)為data數(shù)據(jù)進(jìn)行重定向加載
// 通知 client 收到響應(yīng)
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mRequest.URL MIMEType:@"image/jpeg" expectedContentLength:data.length textEncodingName:nil];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
// 通知 client 已經(jīng)加載完數(shù)據(jù)
[self.client URLProtocol:self didLoadData:UIImagePNGRepresentation(image)];
// 通知 client 請(qǐng)求完成, 成功或者失敗的處理
if(!error) {
//成功
[self.client URLProtocolDidFinishLoading:self];
}else{
//失敗
[self.client URLProtocol:self didFailWithError:error];
}
}];
}
- (void)stopLoading {
// 終止任務(wù)
if ([self.downOperation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[self.downOperation cancel];
}
}
到這里NSURLProtocol部分就實(shí)現(xiàn)了颠区。接下來就是在WKWebView初始化之后調(diào)用注冊(cè)自定義NSURLProtocol的方法削锰。
[NSURLProtocol registerClass:[LMWKWebPURLProtocol class]];
但我們跑代碼運(yùn)行,卻發(fā)現(xiàn)+ (BOOL)canInitWithRequest:(NSURLRequest*)request
這個(gè)方法并沒有走毕莱。這又是涉及另一個(gè)問題了器贩。
WKWebView不支持 NSURLProtocol ?
在 UIWebView 時(shí)代朋截,按照上面的方式注冊(cè)一個(gè)自定義的 NSURLProtocol 是完全沒有問題的蛹稍。但在 WKWebView 中的請(qǐng)求卻完全不遵從這一規(guī)則,網(wǎng)絡(luò)上文章一般都解釋說 WKWebView 的請(qǐng)求是在單獨(dú)的進(jìn)程里部服,所以不走 NSURLProtocol唆姐。經(jīng)過zyl04401大神的文章得出的結(jié)論:WKWebView不走NSURLProtocol的原因,最后得出的結(jié)論是WebKit是支持NSURLProtocol的廓八,只是WebKit還不夠完成奉芦。
FOUNDATION_STATIC_INLINE Class ContextControllerClass() {
static Class cls;
if (!cls) {
// 這樣的寫法其實(shí)是避免因?yàn)橹苯邮褂?Class cls = NSClassFromString(@"WKBrowsingContextController"); 這樣的寫法,導(dǎo)致審核的時(shí)候被認(rèn)為使用了私有api
cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
}
return cls;
}
FOUNDATION_STATIC_INLINE SEL RegisterSchemeSelector() {
return NSSelectorFromString(@"registerSchemeForCustomProtocol:");
}
FOUNDATION_STATIC_INLINE SEL UnregisterSchemeSelector() {
return NSSelectorFromString(@"unregisterSchemeForCustomProtocol:");
}
+ (void)wk_registerScheme:(NSString *)scheme {
Class cls = ContextControllerClass();
SEL sel = RegisterSchemeSelector();
if ([(id)cls respondsToSelector:sel]) {
// 放棄編輯器警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[(id)cls performSelector:sel withObject:scheme];
#pragma clang diagnostic pop
}
}
從 registerSchemeForCustomProtocol: 這個(gè)方法名來猜測瘫想,它的作用的應(yīng)該是注冊(cè)一個(gè)自定義的 scheme仗阅,這樣對(duì)于 WebKit 進(jìn)程的所有網(wǎng)絡(luò)請(qǐng)求,都會(huì)先檢查是否有匹配的 scheme国夜,有的話再走主進(jìn)程的 NSURLProtocol 這一套流程减噪。
配套的,大神通過源碼還扒出了注銷的方法:
+ (void)wk_unregisterScheme:(NSString *)scheme {
Class cls = ContextControllerClass();
SEL sel = UnregisterSchemeSelector();
if ([(id)cls respondsToSelector:sel]) {
// 放棄編輯器警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[(id)cls performSelector:sel withObject:scheme];
#pragma clang diagnostic pop
}
}
加上這兩個(gè)注冊(cè)scheme的代碼车吹,整體的流程就可以跑通了筹裕。
最后
因?yàn)镹SURLProtocol 是全局生效的。如果所有請(qǐng)求都走一遍這個(gè)攔截方法窄驹,這樣的做法不太合理也比較耗性能朝卒。因此我們把scheme的注冊(cè)實(shí)際寫在了WKWebViewController的初始化和Dealloc里,確保只對(duì)WKWebView的生命周期內(nèi)生效乐埠。
@implementation LMWebViewController
// MARK:- 注冊(cè)URLProtocol 及scheme
- (void)registerIntercept
{
// LMWKWebPURLProtocol實(shí)現(xiàn)WebP圖片攔截及重定向
[NSURLProtocol registerClass:[LMWKWebPURLProtocol class]];
// WKWebView不走 NSURLProtocol抗斤,得實(shí)現(xiàn)分類方法后才能攔截
[NSURLProtocol wk_registerScheme:@"http"];
[NSURLProtocol wk_registerScheme:@"https"];
}
// MARK:- 注消URLProtocol 及scheme
- (void)unregisterIntercept
{
[NSURLProtocol unregisterClass:[LMWKWebPURLProtocol class]];
[NSURLProtocol wk_unregisterScheme:@"http"];
[NSURLProtocol wk_unregisterScheme:@"https"];
}
- (void)viewDidLoad {
[super viewDidLoad];
// 注冊(cè)URLProtocol 及scheme
[self registerIntercept];
// 初始化WKWebViewController的相關(guān)代碼
……
}
- (void)dealloc
{
// 注銷攔截相關(guān)的方法
[self unregisterIntercept];
}