Sometimes we have to interact with WK WebView. Although WK WebView has high performance, there are still many pits.
For example, we can get the js context in UIWebview in the following way, but WKWebView is error-prone
let context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext context.evaluateScript(theScript)
The company server customizes some patterns, such as: custom://action?param=1 To control the client, we need to intercept and request custom schemas, but the following will not only hook to intercept custom schemas, but also to intercept https and http requests
Extra stuff:
In fact, WKWebView comes with some interfaces to interact with JS.
-
WKUser Content Controller and WKUserScript
JS is controlled by -(void) addUserScript:(WKUserScript*) userScript; interface
JS sends messages to native via window. webkit. messageHandlers. <name>. postMessage (<messageBody>).
Native then responds to requests in the following ways
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
-
evaluateJavaScript:completionHandler: Method
WKWebview comes with an interface for asynchronous invocation of js code
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
Then, through the WKScript MessageHandler protocol method
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
To handle requests from JS
Some knowledge about native JavaScript Core and JS interaction can be found in my other blog. JavaScript Core and JS Interactive Notes
Having talked so much, let's get to the point.
Personally, I think it's more flexible to handle requests by intercepting custom patterns. The next steps are to solve several problems.
- Custom Interception Request Protocol (https, http, customProtocol, etc.)
- Processing the intercepted WKWebView request not only takes over the request, but also returns the result of the request to WKWebView.
So, let's get started.
During UI Web view, network requests can be intercepted using NSURLProtocol, but
WKWebView executes network requests in a process independent of the App Process process, and requests do not pass through the main process. Therefore, direct use of NSURLProtocol on WKWebView cannot intercept requests.
But then we'll use NSURLProtocol to intercept, but we need some tirick.
We can use the private WKBrowsingContextController class to register the global custom scheme to WebProcessPool through the registerSchemeForCustomProtocol method to achieve our goal.
In the application:didFinishLaunchingWithOptions method, execute the following statement to register the protocol that needs to be intercepted
- (void)registerClass { // Prevent Apple static checks from splitting up WK Browsing Context Controller and then piecing it together 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]) { // Register Custom Protocol // [(id)cls performSelector:sel withObject:@"CustomProtocol"]; // Register http protocol [(id)cls performSelector:sel withObject:HttpProtocolKey]; // Register https protocol [(id)cls performSelector:sel withObject:HttpsProtocolKey]; } } // SechemaURLProtocol custom class inherits from NSURLProtocol [NSURLProtocol registerClass:[SechemaURLProtocol class]]; }
The above uses a custom class SechemaURLProtocol that inherits NSURLProtocol
We mainly need to copy the following methods
// Determine whether a request enters a custom NSURLProtocol loader + (BOOL)canInitWithRequest:(NSURLRequest *)request; // Resetting the information of NSURLRequest allows us to customize requests, such as adding a unified request header. + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request; // Where the intercepted request begins to execute - (void)startLoading; // End loading URL request - (void)stopLoading;
Complete code
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { NSString *scheme = [[request URL] scheme]; if ([scheme caseInsensitiveCompare:HttpProtocolKey] == NSOrderedSame || [scheme caseInsensitiveCompare:HttpsProtocolKey] == NSOrderedSame) { //See if it's already handled to prevent infinite loops if ([NSURLProtocol propertyForKey:kURLProtocolHandledKey inRequest:request]) { return NO; } } return YES; } + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request { NSMutableURLRequest *mutableReqeust = [request mutableCopy]; return mutableReqeust; } // Weight determination + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } - (void)startLoading { NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy]; // Markup change request has been processed to prevent infinite loops [NSURLProtocol setProperty:@YES forKey:kURLProtocolHandledKey inRequest:mutableReqeust]; } - (void)stopLoading { }
Now we have solved the first problem.
- Custom Interception Request Protocol (https, http, customProtocol, etc.)
However, if we hook an http or https request for a WK Webview, we take over the request. We need to manually control its request declaration cycle and return it back to the WK Webview at an appropriate time, otherwise the WK Webview will never be able to display the load result requested by the hook.
Next, we use NSURLSession to send and manage requests. The PS author tried to use NSURLConnection, but the request was unsuccessful.
Prior to that, NSURLProtocol had a client attribute that followed the NSURLProtocolClient protocol.
/*! @abstract Returns the NSURLProtocolClient of the receiver. @result The NSURLProtocolClient of the receiver. */ @property (nullable, readonly, retain) id <NSURLProtocolClient> client;
We need this client to communicate with WK Webview
NSURLProtocolClient Protocol Method
@protocol NSURLProtocolClient <NSObject> // Redirecting Requests - (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse; // Availability of Response Cache - (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse; // Response response has been received - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy; // Successful loading of data - (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data; // Successful request has been lodged and loaded - (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol; // Failure of request loading - (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error; @end
We need to have the client call the NSURLProtocolClient protocol method at the appropriate location in the NSURLSession Delegate proxy method
We send requests in -(void) start Loading
NSURLSessionConfiguration *configure = [NSURLSessionConfiguration defaultSessionConfiguration]; self.session = [NSURLSession sessionWithConfiguration:configure delegate:self delegateQueue:self.queue]; self.task = [self.session dataTaskWithRequest:mutableReqeust]; [self.task resume];
NSURLSession Delegate request proxy method
- (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: Redirection - (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]]; }
So far, we have solved the second problem.
Processing the intercepted WKWebView request not only takes over the request, but also returns the result of the request to WKWebView.
The author encapsulates the above code into a simple Demo, implements the request of Hook WK WebView, and displays it in Label at the bottom of the interface.
DEMO Github address: https://github.com/madaoCN/WKWebViewHook
Passing classmates like to order and walk again.