iOS使用NSURLProtocol來Hook攔截WKWebview請求並回放的一種姿(ti)勢(wei)

有些時候咱們不免須要和 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攔截自定義模式,還會攔截httpshttp的請求github

額外的玩意兒:web

其實 WKWebView 自帶了一些和 JS 交互的接口swift

  • WKUserContentControllerWKUserScript 經過- (void)addUserScript:(WKUserScript *)userScript;接口對 JS 作控制 JS 經過window.webkit.messageHandlers.<name>.postMessage(<messageBody>)來給原生髮送消息 而後原生經過如下方法來響應請求
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
複製代碼
  • evaluateJavaScript:completionHandler: 方法 WKWebview 自帶了異步調用 js代碼的接口
- (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 了 WKWebviewhttp 或者 __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中

Untitled.gif

DEMO Github地址:https://github.com/madaoCN/WKWebViewHook

有路過的同窗點個喜歡再走唄

相關文章
相關標籤/搜索