有些时候咱们不免须要和 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
有路过的同窗点个喜欢再走呗