子线程中WKWebView调用evaluateJavaScript同步返回潜在问题
业务背景
UIWebView执行JS是同步返回,WKWebView执行JS是异步返回。
这里通过死循环阻塞主线程,以达到WKWebView执行JS同步返回的效果。同时在While循环中手动执行NSRunLoop保证界面不被卡死。
在业务代码中,当调用该代码片段的业务代码是在网络请求回调或者扫描二维码回调中时,就需要从子线程切换主线程。我遇到的情况就是在扫描二维码界面,需要切换主线程。
示例代码
WKWebView *sampleWebView;
-(void)errorDemo {
dispatch_async(dispatch_get_main_queue(), ^{
sampleWebView = [[WKWebView alloc] initWithFrame:CGRectZero];
__block BOOL finished = NO;
[sampleWebView evaluateJavaScript:@"" completionHandler:^(id result, NSError *error) {
finished = YES; // 该行代码未执行
}];
while (!finished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
});
}
出现问题
但是当外部业务代码使用dispatch_async和dispatch_get_main_queue切换主线程执行时,就会出现evaluateJavaScript阻塞无法调用completionHandler的情况。
根本原因
GCD提供的dispatch_async()
接口也用到了 RunLoop。当调用 dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()
里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
而WKWebView的evaluateJavaScript
方法在执行完准备调用completionHandler
时,估计也是使用了dispatch_async(dispatch_get_main_queue(), block)
,从而导致了CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
队列竞争,最后死锁。
演示代码如下:
dispatch_async(dispatch_get_main_queue(), ^{
__block BOOL finished = NO;
NSLog(@"evaluateJavaScript");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// evaluateJavaScript内部代码
// ...
// 调用completionHandler
dispatch_async(dispatch_get_main_queue(), ^{
finished = YES; // 该行代码未执行
NSLog(@"completionHandler");
});
});
while (!finished) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
});
但CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
队列的占用只是dispatch_async(dispatch_get_main_queue(), block)
方法调用导致的,正常的主线程代码不会在该队列里执行,所以不会死锁。
解决方案
- 业务代码
若本来是在业务代码中使用了dispatch_async(dispatch_get_main_queue(), block)
,则使用performSelectorOnMainThread替代。
该方法未占用CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
队列,所以不会有问题。 - 系统代码
若使用dispatch_async(dispatch_get_main_queue(), block)
方法的是系统代码,比如业务背景中描述的扫码二维码回调,或者NSNotification的回调,他们的回调方法中都隐含调用了,同样会造成上面的死锁问题。
像这样的情况就暂时无法解决了,建议还是老老实实的使用异步方式调用evaluateJavaScript
方法。
参考文章
iOS面试全解2:Runloop
https://www.jianshu.com/p/37025d0612e8
RunLoop 运行机制原理逻辑与GCD及线程关系剖析
https://www.jianshu.com/p/efc4dcaf4c05