iOS 遇到的问题

iOS开发-阻塞主线程实现

2020-04-24  本文已影响0人  橘子不酸丶

原文:橘子不酸丶
转载:https://juejin.im/post/5ea283cff265da480d6191b0

前言

最近在项目开发中遇到需要阻塞主线程的开发场景,记录下来过程,以及在此过程中的理解。

一、场景

在使用WKWebView获取UserAgent时,需要同步获取到UA,然而WKWebView的evaluateJavaScript:方法又是异步的,因此就需要阻塞主线程,等待获取到UA之后再往下继续执行。

先上代码方便理解。

+ (NSString *)getUserAgent {
    if (userAgentStr) {
        return userAgentStr;
    }
    uaWebView = [[WKWebView alloc] initWithFrame:CGRectZero];
    [uaWebView loadHTMLString:@"<html></html>" baseURL:nil];
    __block BOOL end = NO;
    [uaWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
        userAgentStr = obj;
        end = YES;
    }];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        end = YES;
    });
    while (!end) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    if (userAgentStr == nil) {
        userAgentStr = @"Mozilla/5.0 (iPhone; CPU iPhone OS 12_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148";
    }
    
    return userAgentStr;
}

首先要明确WKWebView的创建以及执行JS的方法evaluateJavaScript:都必须要在主线程操作,因此这里并不能dispatch到子线程来处理。

其次我们来看一下 evaluateJavaScript:completionHandler: 的执行过程以及回调。

image

通过堆栈我们可以看到,因为WKWebView是单独的进程处理,所以这里涉及到了进程之间的通信;我们的主线程在需要执行evaluateJavaScript:时会调度到WKWebView的进程来执行并获取到结果,之后再通过IPC进程间通信回调给我们app的主线程来处理结果。

如果我想等待获取到UA之后再继续往下执行,这时就需要阻塞主线程了;首先dispatch_semaphore、dispatch_group是不行的,因为这里在evaluateJavaScript的前后都是在我们的主线程,因此一旦加锁就会造成死锁。while(YES) {} ?也是不行的,这样会占满CPU并且同样会死锁。

此时RunLoop的 runModel:beforeDate: 就发挥了作用。

二、runModel:beforeDate:

首先来看一下该方法的官方注释:

Summary

Runs the loop once, blocking for input in the specified mode until a given date.

Declaration

-(BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

Discussion

If no input sources or timers are attached to the run loop, this method exits immediately and returns NO; otherwise, it returns after either the first input source is processed or limitDate is reached. Manually removing all known input sources and timers from the run loop does not guarantee that the run loop will exit immediately. macOS may install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

Note

A timer is not considered an input source and may fire multiple times while waiting for this method to return

Parameters

mode
The mode in which to run. You may specify custom modes or use one of the modes listed in Run Loop Modes.

limitDate
The date until which to block.

Returns

YES if the run loop ran and processed an input source or if the specified timeout value was reached; otherwise, NO if the run loop could not be started.

运行runLoop 一次,阻塞当前线程以等待处理一次输入源。在处理了一次到达的输入源或设定的beforeDate到时间后,runLoop 会 exit。

其实每一个app启动后都会开启RunLoop通过run方法开启runloop循环。通过RunLoop的run方法注释我们可以看到run方法是通过循环重复调用runMode:beforeDate:来实现的。

我们再看一下React中的runloop开启

- (void)main {
  @autoreleasepool {
    _runLoop = [NSRunLoop currentRunLoop];
    dispatch_group_leave(_waitGroup);

    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:self selector:@selector(step) userInfo:nil repeats:NO];
    [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode];

    while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { }
    assert(NO);
  }
}

因此再回到我们我们的场景中就可以理解了。通过

while (!end) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

while(!end)我们在这里接管了外边的runloop处理事件,如果有input source就处理input source并返回NO,然后就挂起等待。直到evaluateJavaScript:completionHandler:的回调之后end状态被修改走出循环,继续执行。

需要注意这里的evaluateJavaScript:completionHandler:和dispatch_after都会作为input source来处理的。因此处理完input source之后状态被改变就走出了while循环。继续原来的方法继续执行。

结语

以上只是本猿对RunLoop冰山一角的小理解。CFRunLoop的源码也是开源的,也有YYKit作者的深入理解runloop对RunLoop的理解应用。

上一篇下一篇

猜你喜欢

热点阅读