有些时候我们难免需要和 WKWebView 做一些交互,虽然WKWebView性能高,但是坑还是不少的
例如:我们在UIWebview ,可以通过如下方式获取js上下文,但是在WKWebView是会报错的
let context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext
context.evaluateScript(theScript)
公司服务端自定义了一些模式,例如:custom://action?param=1 来对客户端做些控制,那么我们就需要对自定义的模式进行拦截和请求,但是下文不仅会hook拦截自定义模式,还会拦截
https
和http
的请求
额外的玩意儿:
其实 WKWebView 自带了一些和 JS 交互的接口
- WKUserContentController 和 WKUserScript
通过- (void)addUserScript:(WKUserScript *)userScript;
接口对 JS 做控制
JS 通过window.webkit.messageHandlers.
来给原生发送消息.postMessage( )
然后原生通过以下方法来响应请求
- (void)addScriptMessageHandler:(id )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交互笔记
扯了这么多,进入正题吧
个人觉得通过拦截自定义模式的方式来处理请求会灵活一些,接下来的内容要解决几个问题
- 自定义拦截请求协议(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 client;
我们需要通过这个 client 来向 WKWebview 沟通消息
NSURLProtocolClient 协议方法
@protocol NSURLProtocolClient
// 重定向请求
- (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
有路过的同学点个喜欢再走呗