摘要

在非主线程运用 -[NSObject performSelector:withObject:afterDelay:] 时,需求启动 RunLoop,而且启动时有一些需求留意的地方。

示例

来看 2 段示例代码:

  1. 能成功调用 print。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 会调用 print
    [self performSelector:@selector(print) withObject:nil afterDelay:0];
    [[NSRunLoop currentRunLoop] run];
});
  1. 一向不会调用 print。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [[NSRunLoop currentRunLoop] run];
    // 不会调用 print
    [self performSelector:@selector(print) withObject:nil afterDelay:0];
});

那么问题来了,相同的 2 行代码,只是次序不同,为何一个能够正常调用,另外一个不可呢?

寻觅原因

由于看不到 -performSelector:withObject:afterDelay:源码,google 也没找到答案,所以求助 ChatGPT,它给出第 2 种状况(不调用)的原因是,调用 run 办法后,RunLoop 随即停止了。

是否真的如此?咱们来简单验证下。

给 RunLoop 增加监听,关于上述 2 种状况别离测验,示例代码:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 增加监听
    [self p_addRunLoopObserver];
    // 会调用 print
    [self performSelector:@selector(print) withObject:nil afterDelay:0];
    [[NSRunLoop currentRunLoop] run];
    // 不会调用 print    
    [[NSRunLoop currentRunLoop] run];
    [self performSelector:@selector(print) withObject:nil afterDelay:0];
});
- (void)p_addRunLoopObserver {
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"行将进入 runloop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"行将处理 timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"行将处理 source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"行将进入睡觉");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"刚从睡觉中唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"行将退出");
                break;
            default:
                break;
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);
}

结果显示,关于第 1 种状况(正常调用),能够监听到 RunLoop 的状况变化。

2023-09-19 23:46:51.207098+0800 GCDTestObjc[75287:9741406] 行将进入 runloop
2023-09-19 23:46:51.207210+0800 GCDTestObjc[75287:9741406] 行将处理 timer
2023-09-19 23:46:51.207300+0800 GCDTestObjc[75287:9741406] 行将处理 source
2023-09-19 23:46:51.207382+0800 GCDTestObjc[75287:9741406] 行将进入睡觉
2023-09-19 23:46:51.207476+0800 GCDTestObjc[75287:9741406] 刚从睡觉中唤醒
2023-09-19 23:46:51.207605+0800 GCDTestObjc[75287:9741406] print
2023-09-19 23:46:51.207694+0800 GCDTestObjc[75287:9741406] 行将退出

而第 2 种状况,则监听不到,由此推断 RunLoop 并没有 run 起来。

那为什么关于第 2 种状况,RunLoop 随即停止了呢?

能够从官方文档找到 RunLoop 启动的条件:

Starting the run loop is necessary only for the secondary threads in your application. A run loop must have at least one input source or timer to monitor. If one is not attached, the run loop exits immediately.

翻译过来就是,RunLoop 需求有 timer 或 source 才干启动。

而从 performSelector:withObject:afterDelay: | Apple Developer Documentation 可知其间注册了一个 timer。

This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.

所以,关于第 1 种状况,先增加了 timer,再调用 run 是能够的;而关于第 2 种状况,调用 run 后,RunLoop 随即停止了,再增加 timer 也无济于事了。

小结

在非主线程运用 -performSelector:withObject:afterDelay: 时,需保证 RunLoop 能正常 run 起来,最好是在 RunLoop run 之前调用。