前言
出于節(jié)省hybrid app的性能,以及加載時(shí)間,對(duì)app內(nèi)的一些資源做緩存處理,包括:圖片、js文件。
首先,我們需要能攔截到這些請(qǐng)求,實(shí)驗(yàn)之后,發(fā)現(xiàn)NSURLProtocol在WKWebViwe中不生效。
本文分一下兩個(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)題
-
這么做有隱患,自定義
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)景分開使用
有一陣,頁(yè)面上有一些圖片資源無(wú)法正常加載,下拉刷新無(wú)效,懷疑是緩存了錯(cuò)誤資源,兩個(gè)方面:1. CDN資源有問(wèn)題 2. 緩存策略有問(wèn)題。當(dāng)時(shí)由于緊急修復(fù),后來(lái)由于
求穩(wěn)...功能暫時(shí)擱置了-
不會(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)題)
解決方案:
- 我們將NSURLSessionConfiguration的屬性
protocolClasses的get方法hook掉,
通過(guò)返回自定義的protocol,這樣,
我們就能夠監(jiān)控到通過(guò)sessionWithConfiguration:delegate:delegateQueue:
得到的session的網(wǎng)絡(luò)請(qǐng)求
- 我們將NSURLSessionConfiguration的屬性
- 在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