有些時候咱們不免須要和 WKWebView 作一些交互,雖然__WKWebView__性能高,可是坑仍是很多的java
例如:咱們在__UIWebview__ ,能夠經過以下方式獲取js上下文,可是在__WKWebView__是會報錯的git
let context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext
context.evaluateScript(theScript)
複製代碼
公司服務端自定義了一些模式,例如:custom://action?param=1 來對客戶端作些控制,那麼咱們就須要對自定義的模式進行攔截和請求,可是下文不只會hook攔截自定義模式,還會攔截
https
和http
的請求github
額外的玩意兒:web
其實 WKWebView 自帶了一些和 JS 交互的接口swift
- (void)addUserScript:(WKUserScript *)userScript;
接口對 JS 作控制 JS 經過window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
來給原生髮送消息 而後原生經過如下方法來響應請求- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
複製代碼
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
複製代碼
而後,經過 WKScriptMessageHandler 協議方法緩存
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
複製代碼
來處理 JS 給過來的請求網絡
還有一些原生__JavaScriptCore__ 和 JS 交互的一些知識請看本人另外一篇博客 JavaScriptCore與JS交互筆記session
扯了這麼多,進入正題吧app
我的以爲經過攔截自定義模式的方式來處理請求會靈活一些,接下來的內容要解決幾個問題異步
- 自定義攔截請求協議(https,http,customProtocol等等)
- 對攔截的 WKWebView 請求作處理,不只接管請求還要將請求結果返還給__WKWebView.__
在 UIWebview 時期,使用 NSURLProtocol 能夠攔截到網絡請求, 可是
WKWebView 在獨立於 App Process 進程以外的進程中執行網絡請求,請求數據不通過主進程,所以,在__WKWebView__ 上直接使用 NSURLProtocol 沒法攔截請求
可是 接下來咱們仍是要用 NSURLProtocol 來攔截,可是須要一些 tirick
咱們可使用私有類 WKBrowsingContextController 經過 registerSchemeForCustomProtocol 方法向 WebProcessPool 註冊全局自定義 scheme 來達到咱們的目的
在 application:didFinishLaunchingWithOptions 方法中執行以下語句,對須要攔截的協議進行註冊
- (void)registerClass
{
// 防止蘋果靜態檢查 將 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]) {
// 註冊自定義協議
// [(id)cls performSelector:sel withObject:@"CustomProtocol"];
// 註冊http協議
[(id)cls performSelector:sel withObject:HttpProtocolKey];
// 註冊https協議
[(id)cls performSelector:sel withObject:HttpsProtocolKey];
}
}
// SechemaURLProtocol 自定義類 繼承於 NSURLProtocol
[NSURLProtocol registerClass:[SechemaURLProtocol class]]; } 複製代碼
上述用到了一個繼承 NSURLProtocol 的自定義類 SechemaURLProtocol
咱們主要須要複寫以下幾個方法
// 判斷請求是否進入自定義的NSURLProtocol加載器
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// 從新設置NSURLRequest的信息, 這方法裏面咱們能夠對請求作些自定義操做,如添加統一的請求頭等
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request;
// 被攔截的請求開始執行的地方
- (void)startLoading;
// 結束加載URL請求
- (void)stopLoading;
複製代碼
完整的代碼
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *scheme = [[request URL] scheme];
if ([scheme caseInsensitiveCompare:HttpProtocolKey] == NSOrderedSame ||
[scheme caseInsensitiveCompare:HttpsProtocolKey] == NSOrderedSame)
{
//看看是否已經處理過了,防止無限循環
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];
// 標示改request已經處理過了,防止無限循環
[NSURLProtocol setProperty:@YES forKey:kURLProtocolHandledKey inRequest:mutableReqeust];
}
- (void)stopLoading
{
}
複製代碼
如今咱們已經解決了第一個問題
- 自定義攔截請求協議(https,http,customProtocol等等)
可是,若是咱們 hook 了 WKWebview 的 http 或者 __https__請求,就等於咱們接管了該請求,咱們須要手動控制它的請求聲明週期,並在適當的時候返還回放給 WKWebview, 不然 WKWebview 將始終沒法顯示被hook請求的加載結果
那麼,接下來咱們使用 NSURLSession 來發送和管理請求,PS 筆者嘗試過使用 NSURLConnection 可是沒有請求成功
在這以前, NSURLProtocol 有個遵循了 NSURLProtocolClient 協議的 client 屬性
/*! @abstract Returns the NSURLProtocolClient of the receiver. @result The NSURLProtocolClient of the receiver. */
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;
複製代碼
咱們須要經過這個 client 來向 WKWebview 溝通消息
NSURLProtocolClient 協議方法
@protocol NSURLProtocolClient <NSObject> // 重定向請求 - (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse; // 響應緩存是否可用 - (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse; // 已經接收到Response響應 - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy; // 成功加載數據 - (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data; // 請求成功 已經借宿加載 - (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol; // 請求加載失敗 - (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error; @end 複製代碼
咱們須要在 NSURLSessionDelegate 代理方法中合適的位置讓__client__ 調用 NSURLProtocolClient 協議方法
咱們在 - (void)startLoading 中發送請求
NSURLSessionConfiguration *configure = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configure delegate:self delegateQueue:self.queue];
self.task = [self.session dataTaskWithRequest:mutableReqeust];
[self.task resume];
複製代碼
NSURLSessionDelegate 請求代理方法
- (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]]; } 複製代碼
到此,咱們已經解決了第二個問題
對攔截的 WKWebView 請求作處理,不只接管請求還要將請求結果返還給__WKWebView.__
筆者,將以上代碼封裝成了一個簡單的Demo,實現了Hook WKWebView 的請求,並顯示在界面最下層的Label中
DEMO Github地址:https://github.com/madaoCN/WKWebViewHook
有路過的同窗點個喜歡再走唄