原文:橘子不酸丶
轉(zhuǎn)載:https://juejin.im/post/5ea283cff265da480d6191b0
前言
最近在項目開發(fā)中遇到需要阻塞主線程的開發(fā)場景,記錄下來過程苛吱,以及在此過程中的理解皮璧。
一充边、場景
在使用WKWebView獲取UserAgent時橱野,需要同步獲取到UA术幔,然而WKWebView的evaluateJavaScript:方法又是異步的椅寺,因此就需要阻塞主線程,等待獲取到UA之后再往下繼續(xù)執(zhí)行兢哭。
先上代碼方便理解。
+ (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的創(chuàng)建以及執(zhí)行JS的方法evaluateJavaScript:都必須要在主線程操作夫嗓,因此這里并不能dispatch到子線程來處理迟螺。
其次我們來看一下 evaluateJavaScript:completionHandler: 的執(zhí)行過程以及回調(diào)。
通過堆棧我們可以看到舍咖,因為WKWebView是單獨的進程處理矩父,所以這里涉及到了進程之間的通信;我們的主線程在需要執(zhí)行evaluateJavaScript:時會調(diào)度到WKWebView的進程來執(zhí)行并獲取到結(jié)果排霉,之后再通過IPC進程間通信回調(diào)給我們app的主線程來處理結(jié)果窍株。
如果我想等待獲取到UA之后再繼續(xù)往下執(zhí)行,這時就需要阻塞主線程了攻柠;首先dispatch_semaphore球订、dispatch_group是不行的,因為這里在evaluateJavaScript的前后都是在我們的主線程瑰钮,因此一旦加鎖就會造成死鎖冒滩。while(YES) {} ?
也是不行的,這樣會占滿CPU并且同樣會死鎖浪谴。
此時RunLoop的 runModel:beforeDate: 就發(fā)揮了作用开睡。
二、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 一次苟耻,阻塞當(dāng)前線程以等待處理一次輸入源篇恒。在處理了一次到達的輸入源或設(shè)定的beforeDate到時間后,runLoop 會 exit凶杖。
其實每一個app啟動后都會開啟RunLoop通過run方法開啟runloop循環(huán)胁艰。通過RunLoop的run方法注釋我們可以看到run方法是通過循環(huán)重復(fù)調(diào)用runMode:beforeDate:來實現(xiàn)的。
我們再看一下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:的回調(diào)之后end狀態(tài)被修改走出循環(huán)哮翘,繼續(xù)執(zhí)行。
需要注意這里的evaluateJavaScript:completionHandler:和dispatch_after都會作為input source來處理的毛秘。因此處理完input source之后狀態(tài)被改變就走出了while循環(huán)饭寺。繼續(xù)原來的方法繼續(xù)執(zhí)行阻课。
結(jié)語
以上只是本猿對RunLoop冰山一角的小理解。CFRunLoop的源碼也是開源的艰匙,也有YYKit作者的深入理解runloop對RunLoop的理解應(yīng)用限煞。