WKWebView中NSURLProtocol的使用以及對(duì)H5的緩存

前言

出于節(jié)省hybrid app的性能,以及加載時(shí)間,對(duì)app內(nèi)的一些資源做緩存處理,包括:圖片、js文件。
首先,我們需要能攔截到這些請(qǐng)求,實(shí)驗(yàn)之后,發(fā)現(xiàn)NSURLProtocolWKWebViwe中不生效。
本文分一下兩個(gè)部分:

  • 讓W(xué)KWebView支持NSURLProtocol,摘抄了這篇博客大篇幅內(nèi)容,其中有關(guān)于蘋果對(duì)私有API的監(jiān)測(cè)以及開發(fā)人員的應(yīng)對(duì)措施
  • 對(duì)資源做緩存措施

讓W(xué)KWebView支持NSURLProtocol

在UIWebView中,只需要一行代碼[NSURLProtocol registerClass:[customeURLProtocol class]];,就可以對(duì)app內(nèi)所有的網(wǎng)絡(luò)請(qǐng)求進(jìn)行攔截處理。
但是在WKWebView中,除了一開始會(huì)調(diào)用一下 + [NSURLProtocol canInitWithRequest:] 方法,之后就全攔截不到了。網(wǎng)上查,說(shuō)是WKWebView 的請(qǐng)求是在單獨(dú)的進(jìn)程里,所以不走 NSURLProtocol。

# registerSchemeForCustomProtocol

從方法名來(lái)猜測(cè),它的作用的應(yīng)該是注冊(cè)一個(gè)自定義的 scheme,這樣對(duì)于 WebKit 進(jìn)程的所有網(wǎng)絡(luò)請(qǐng)求,都會(huì)先檢查是否有匹配的 scheme,有的話再走主進(jìn)程的 NSURLProtocol 這一套流程
博客作者猜測(cè)這么做可能是為了保證效率 (NSURLRequest 的 HTTPBody 屬性在 WKWebView 中被忽略了應(yīng)該也出于這個(gè)原因!!! --- 這是個(gè)大坑 不能發(fā)POST請(qǐng)求了!),畢竟 IPC 代價(jià)挺高的。詳細(xì)可以看: WebKit::CustomProtocolManager 和 WebKit::WebProcessPool 等相關(guān)源碼

解決方案:

Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 把 http 和 https 請(qǐng)求交給 NSURLProtocol 處理
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];

此時(shí),[NSURLProtocol registerClass:[customeURLProtocol class]]就可以生效了

# 優(yōu)化 - 私有API檢測(cè)

## 關(guān)于私有 API

按照 @sunnyxx 的總結(jié),Apple 檢查私有 API 的使用,大概會(huì)采取下面幾種手段:

  • 是否 link 了私有 framework 或者公開 framework 中的私有符號(hào),這可以防止開發(fā)者把私有 header 都 dump 出來(lái)供程序直接調(diào)用。
  • 同上,使用@selector(_private_sel)加上-performSelector:的方式直接調(diào)用私有 API。
  • 掃描所有符號(hào),查看是否有繼承自私有類,重載私有方法,方法名是否有重合。
  • 掃描所有string,看字符串常量段是否出現(xiàn)和私有 API 對(duì)應(yīng)的。
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");

上面兩行代碼非常之符合第四條。

解決方案:
查詢 WKWebView.h 可以看到,有個(gè)方法 - browsingContextController 的方法名跟 WKBrowsingContextController 長(zhǎng)得很像,通過(guò) KVC 取出來(lái)(沒(méi)錯(cuò),KVC 不但可以取 property 取 ivar,還可以取無(wú)入?yún)?selector 的返回值)發(fā)現(xiàn)它就是 WKBrowsingContextController 的一個(gè)實(shí)例,這樣一來(lái)這個(gè)私有類就可以通過(guò) KVC 的方式來(lái)得到了:

Class cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];

valueForKey 比直接使用 NSClassFromString安全了許多。
其他解決方案:這些字符串也可以不明著寫出來(lái),只要運(yùn)行時(shí)算出來(lái)就行,比如用 base64 編碼啊,圖片資源里藏一段啊,甚至通過(guò)服務(wù)器下發(fā)……

## 使用私有 API 的另一風(fēng)險(xiǎn)是兼容性問(wèn)題

比如上面的 browsingContextController 就只能在 iOS 8.4 以后才能用,反注冊(cè) scheme 的方法 unregisterSchemeForCustomProtocol:也是在 iOS 8.4 以后才被添加進(jìn)來(lái)的。

要支持 iOS 8.0 ~ 8.3 機(jī)型的話,只能通過(guò)動(dòng)態(tài)生成字符串的方式拿到 WKBrowsingContextControlle,而且還不能反注冊(cè),不過(guò)這些問(wèn)題都不大。至于向后兼容,這個(gè)也不用太擔(dān)心,因?yàn)?iOS 發(fā)布新版本之前都會(huì)有開發(fā)者預(yù)覽版的,那個(gè)時(shí)候可以提前關(guān)注測(cè)一下。對(duì)于以上的例子來(lái)說(shuō),如果將來(lái)哪個(gè) iOS 版本移除了這個(gè) API,那很可能是因?yàn)楣俜教峁┝送暾慕鉀Q方案,到那時(shí)候自然也不需要以上的方法了。

對(duì)資源做緩存措施

# 1. 圖片資源

通過(guò)response.MIMEType判斷如果是: image/gif image/jpeg image/jpg image/png利用SDWebImage提供的緩存api來(lái)存儲(chǔ)數(shù)據(jù):

- (void)storeImage:(nullable UIImage *)image
     imageData:(nullable NSData *)imageData
        forKey:(nullable NSString *)key
        toDisk:(BOOL)toDisk
    completion:(nullable SDWebImageNoParamsBlock)completionBlock;    

# 2. 文件

文件之類的我們是使用本地存儲(chǔ)來(lái)做緩存

遇到的問(wèn)題

  1. 這么做有隱患,自定義NSURLProtocol 會(huì)影響WKWebView中POST請(qǐng)求,所以使用起來(lái)還得根據(jù)場(chǎng)景來(lái)看

    • 控制自定義的協(xié)議的開關(guān)時(shí)機(jī)
    • 如果實(shí)在沖突可以再創(chuàng)建一個(gè)WKWebView、UIWebView的控制器,根據(jù)場(chǎng)景分開使用
  2. 有一陣,頁(yè)面上有一些圖片資源無(wú)法正常加載,下拉刷新無(wú)效,懷疑是緩存了錯(cuò)誤資源,兩個(gè)方面:1. CDN資源有問(wèn)題 2. 緩存策略有問(wèn)題。當(dāng)時(shí)由于緊急修復(fù),后來(lái)由于求穩(wěn)...功能暫時(shí)擱置了

  3. 不會(huì)攔截原生AFN請(qǐng)求(也不算問(wèn)題,一般也沒(méi)必要)

    如果監(jiān)控網(wǎng)絡(luò)是通過(guò)注冊(cè)NSURLProtocol來(lái)進(jìn)行網(wǎng)絡(luò)監(jiān)控的,而且是用的AFN3.0,那么是攔截不到的,通過(guò) sessionWithConfiguration:delegate:delegateQueue:
    得到的session,他的configuration中已經(jīng)有一個(gè)NSURLProtocol,
    所以不會(huì)走自定義的protocol(通過(guò)share得到的session沒(méi)這個(gè)問(wèn)題)

解決方案:

    1. 我們將NSURLSessionConfiguration的屬性protocolClasses的get方法hook掉,
      通過(guò)返回自定義的protocol,這樣,
      我們就能夠監(jiān)控到通過(guò) sessionWithConfiguration:delegate:delegateQueue:
      得到的session的網(wǎng)絡(luò)請(qǐng)求
    1. 在AFHTTPSessionManager中注冊(cè)
NSMutableArray *protocols = [NSMutableArray arrayWithArray:manager.session.configuration.protocolClasses];
[protocols insertObject:[customeURLProtocol class] atIndex:0];
manager.session.configuration.protocolClasses = [protocols copy];

manager是你發(fā)送請(qǐng)求時(shí)的AFHTTPSessionManager類,注意不能用[AFHTTPSessionManager manager]代替
[AFHTTPSessionManager manager]其實(shí)不是單例,每次調(diào)用的時(shí)候都會(huì)init出一個(gè)新的manager,因此只能在每次初始化好manager之后都注冊(cè)一次NSURLProtocol
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容