有些時(shí)候我們難免需要和 WKWebView 做一些交互浴捆,雖然WKWebView性能高,但是坑還是不少的
例如:我們?cè)?strong>UIWebview ,可以通過如下方式獲取js上下文菠红,但是在WKWebView是會(huì)報(bào)錯(cuò)的
let context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext
context.evaluateScript(theScript)
公司服務(wù)端自定義了一些模式雇逞,例如:custom://action?param=1 來對(duì)客戶端做些控制吗伤,那么我們就需要對(duì)自定義的模式進(jìn)行攔截和請(qǐng)求,但是下文不僅會(huì)hook攔截自定義模式的畴,還會(huì)攔截
https
和http
的請(qǐng)求
額外的玩意兒:
其實(shí) WKWebView 自帶了一些和 JS 交互的接口
-
WKUserContentController 和 WKUserScript
通過- (void)addUserScript:(WKUserScript *)userScript;
接口對(duì) JS 做控制
JS 通過window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
來給原生發(fā)送消息
然后原生通過以下方法來響應(yīng)請(qǐng)求
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
-
evaluateJavaScript:completionHandler: 方法
WKWebview 自帶了異步調(diào)用 js代碼的接口
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
然后渊抄,通過 WKScriptMessageHandler 協(xié)議方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
來處理 JS 給過來的請(qǐng)求
還有一些原生JavaScriptCore 和 JS 交互的一些知識(shí)請(qǐng)看本人另一篇博客 JavaScriptCore與JS交互筆記
扯了這么多,進(jìn)入正題吧
個(gè)人覺得通過攔截自定義模式的方式來處理請(qǐng)求會(huì)靈活一些丧裁,接下來的內(nèi)容要解決幾個(gè)問題
- 自定義攔截請(qǐng)求協(xié)議(https护桦,http,customProtocol等等)
- 對(duì)攔截的 WKWebView 請(qǐng)求做處理煎娇,不僅接管請(qǐng)求還要將請(qǐng)求結(jié)果返還給WKWebView.
那么二庵,開始吧
在 UIWebview 時(shí)期贪染,使用 NSURLProtocol 可以攔截到網(wǎng)絡(luò)請(qǐng)求, 但是
WKWebView 在獨(dú)立于 App Process 進(jìn)程之外的進(jìn)程中執(zhí)行網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求數(shù)據(jù)不經(jīng)過主進(jìn)程催享,因此杭隙,在WKWebView 上直接使用 NSURLProtocol 無法攔截請(qǐng)求
但是 接下來我們還是要用 NSURLProtocol 來攔截,但是需要一些 tirick
我們可以使用私有類 WKBrowsingContextController 通過 registerSchemeForCustomProtocol 方法向 WebProcessPool 注冊(cè)全局自定義 scheme 來達(dá)到我們的目的
在 application:didFinishLaunchingWithOptions 方法中執(zhí)行如下語句因妙,對(duì)需要攔截的協(xié)議進(jìn)行注冊(cè)
- (void)registerClass
{
// 防止蘋果靜態(tài)檢查 將 WKBrowsingContextController 拆分痰憎,然后再拼湊起來
NSArray *privateStrArr = @[@"Controller", @"Context", @"Browsing", @"K", @"W"];
NSString *className = [[[privateStrArr reverseObjectEnumerator] allObjects] componentsJoinedByString:@""];
Class cls = NSClassFromString(className);
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if (cls && sel) {
if ([(id)cls respondsToSelector:sel]) {
// 注冊(cè)自定義協(xié)議
// [(id)cls performSelector:sel withObject:@"CustomProtocol"];
// 注冊(cè)http協(xié)議
[(id)cls performSelector:sel withObject:HttpProtocolKey];
// 注冊(cè)https協(xié)議
[(id)cls performSelector:sel withObject:HttpsProtocolKey];
}
}
// SechemaURLProtocol 自定義類 繼承于 NSURLProtocol
[NSURLProtocol registerClass:[SechemaURLProtocol class]];
}
上述用到了一個(gè)繼承 NSURLProtocol 的自定義類 SechemaURLProtocol
我們主要需要復(fù)寫如下幾個(gè)方法
// 判斷請(qǐng)求是否進(jìn)入自定義的NSURLProtocol加載器
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// 重新設(shè)置NSURLRequest的信息, 這方法里面我們可以對(duì)請(qǐng)求做些自定義操作,如添加統(tǒng)一的請(qǐng)求頭等
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request;
// 被攔截的請(qǐng)求開始執(zhí)行的地方
- (void)startLoading;
// 結(jié)束加載URL請(qǐng)求
- (void)stopLoading;
完整的代碼
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *scheme = [[request URL] scheme];
if ([scheme caseInsensitiveCompare:HttpProtocolKey] == NSOrderedSame ||
[scheme caseInsensitiveCompare:HttpsProtocolKey] == NSOrderedSame)
{
//看看是否已經(jīng)處理過了攀涵,防止無限循環(huán)
if ([NSURLProtocol propertyForKey:kURLProtocolHandledKey inRequest:request]) {
return NO;
}
}
return YES;
}
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
return mutableReqeust;
}
// 判重
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
// 標(biāo)示改request已經(jīng)處理過了信殊,防止無限循環(huán)
[NSURLProtocol setProperty:@YES forKey:kURLProtocolHandledKey inRequest:mutableReqeust];
}
- (void)stopLoading
{
}
現(xiàn)在我們已經(jīng)解決了第一個(gè)問題
- 自定義攔截請(qǐng)求協(xié)議(https,http汁果,customProtocol等等)
但是涡拘,如果我們 hook 了 WKWebview 的 http 或者 https請(qǐng)求,就等于我們接管了該請(qǐng)求据德,我們需要手動(dòng)控制它的請(qǐng)求聲明周期鳄乏,并在適當(dāng)?shù)臅r(shí)候返還回放給 WKWebview, 否則 WKWebview 將始終無法顯示被hook請(qǐng)求的加載結(jié)果
那么棘利,接下來我們使用 NSURLSession 來發(fā)送和管理請(qǐng)求橱野,PS 筆者嘗試過使用 NSURLConnection 但是沒有請(qǐng)求成功
在這之前, NSURLProtocol 有個(gè)遵循了 NSURLProtocolClient 協(xié)議的 client 屬性
/*!
@abstract Returns the NSURLProtocolClient of the receiver.
@result The NSURLProtocolClient of the receiver.
*/
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;
我們需要通過這個(gè) client 來向 WKWebview 溝通消息
NSURLProtocolClient 協(xié)議方法
@protocol NSURLProtocolClient <NSObject>
// 重定向請(qǐng)求
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
// 響應(yīng)緩存是否可用
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
// 已經(jīng)接收到Response響應(yīng)
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
// 成功加載數(shù)據(jù)
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
// 請(qǐng)求成功 已經(jīng)借宿加載
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
// 請(qǐng)求加載失敗
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;
@end
我們需要在 NSURLSessionDelegate 代理方法中合適的位置讓client 調(diào)用 NSURLProtocolClient 協(xié)議方法
我們?cè)?- (void)startLoading 中發(fā)送請(qǐng)求
NSURLSessionConfiguration *configure = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configure delegate:self delegateQueue:self.queue];
self.task = [self.session dataTaskWithRequest:mutableReqeust];
[self.task resume];
NSURLSessionDelegate 請(qǐng)求代理方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
if (error != nil) {
[self.client URLProtocol:self didFailWithError:error];
}else
{
[self.client URLProtocolDidFinishLoading:self];
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse * _Nullable))completionHandler
{
completionHandler(proposedResponse);
}
//TODO: 重定向
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
{
NSMutableURLRequest* redirectRequest;
redirectRequest = [newRequest mutableCopy];
[[self class] removePropertyForKey:kURLProtocolHandledKey inRequest:redirectRequest];
[[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response];
[self.task cancel];
[[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
}
到此,我們已經(jīng)解決了第二個(gè)問題
對(duì)攔截的 WKWebView 請(qǐng)求做處理善玫,不僅接管請(qǐng)求還要將請(qǐng)求結(jié)果返還給WKWebView.
筆者水援,將以上代碼封裝成了一個(gè)簡(jiǎn)單的Demo,實(shí)現(xiàn)了Hook WKWebView 的請(qǐng)求茅郎,并顯示在界面最下層的Label中
DEMO Github地址:https://github.com/madaoCN/WKWebViewHook
有路過的同學(xué)點(diǎn)個(gè)喜歡再走唄