iOS - 康康就行了 之 WKWebView支持Webp文件展示

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];
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市丈咐,隨后出現(xiàn)的幾起案子瑞眼,更是在濱河造成了極大的恐慌,老刑警劉巖棵逊,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伤疙,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)徒像,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門黍特,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锯蛀,你說我怎么就攤上這事灭衷。” “怎么了旁涤?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵今布,是天一觀的道長。 經(jīng)常有香客問我拭抬,道長部默,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任造虎,我火速辦了婚禮傅蹂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘算凿。我一直安慰自己份蝴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布氓轰。 她就那樣靜靜地躺著婚夫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪署鸡。 梳的紋絲不亂的頭發(fā)上案糙,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音靴庆,去河邊找鬼时捌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛炉抒,可吹牛的內(nèi)容都是我干的奢讨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼焰薄,長吁一口氣:“原來是場噩夢啊……” “哼拿诸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起塞茅,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤亩码,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后凡桥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蟀伸,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蚀同,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年缅刽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了啊掏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡衰猛,死狀恐怖迟蜜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情啡省,我是刑警寧澤娜睛,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站卦睹,受9級(jí)特大地震影響畦戒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜结序,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一障斋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧徐鹤,春花似錦垃环、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至劲赠,卻和暖如春涛目,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凛澎。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國打工泌绣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人预厌。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓阿迈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親轧叽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子苗沧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354