performSelector:withObject:afterDelay:使用tips

1,834 阅读3分钟

摘要

在非主线程使用 -[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 之前调用。