Runloop與performSelector

一修Grace發表於2019-02-23

自己平常開發中比較少用到performSelector相關的API,但是平常看些第三方的時候,發現第三方作者用到performSelector相關的API比較多。自己理解的是,可以在一定程度上解耦,不必引入相關類。但是最近在用到時,遇到了一些問題。由此,檢視了一些部落格,自己也做了驗證,在此記錄一下。

先看一段程式碼:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"1");
        [self performSelector:@selector(testPerform) withObject:nil afterDelay:0];//
        NSLog(@"3");
    });
}
- (void)testPerform{
    NSLog(@"2");
}
複製程式碼

執行結果如下:沒有列印出2,只列印出了1和3。

結果
看文件中對這個API的註釋是說,這個方法呼叫後,在當前runloop裡設定了一個timer,來觸發這個方法執行。而當前這個方法是在子執行緒中呼叫的,在子執行緒中runloop不是自動建立並跑起來的,需要手動呼叫,才會建立。因為這個在子執行緒中的呼叫沒有建立runloop,所以就沒有執行testPerform

官方註釋:

Runloop與performSelector
那按照官方文件說明在子執行緒中加入runloop,看下執行效果。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"1");
        NSRunLoop *runloop = [NSRunLoop currentRunLoop];
        [self performSelector:@selector(testPerform) withObject:nil afterDelay:0];//
        NSLog(@"3");
    });
}
複製程式碼

通過獲取當前的runloop,系統就會返回當前的runloop,如果沒有的話,會建立後返回,但是加入了runloop的時候,執行結果,依然是隻有列印出來1和3,沒有列印2。在[self performSelector:@selector(testPerform) withObject:nil afterDelay:0]方法呼叫前後,通過控制檯列印runloop物件,確實看到了呼叫方法後,runloop裡多了一個timer源。

前後runloop對比:

Runloop與performSelector
有了runloop也有了觸發方法testPerform執行的timer,為什麼還依然沒有執行。因為runloop沒有跑起來。 所以建立完runloop後,還需要runloop跑起來。【通過給當前runloop新增觀察者,檢視runloop的狀態,runloop沒有跑起來】當我們呼叫[runloop run];方法後,將runloop跑起來後,testPerform才會執行。列印結果為1,2,3。

但,問題又來了,既然加入了runloop,並且跑起來了,為什麼3還會列印出來,runloop不是相當於死迴圈嗎?迴圈外的3為什麼會列印出來?這個問題,通過加入的runloop的觀察者的列印情況可以看出來,是因為,runloop在執行完testPerform後,就退出了。所以下邊的3頁列印出來了。

觀察者列印:

Runloop與performSelector
可以看出,3是在runloop退出後,列印出來的。【在testPerform方法內列印runloop,看到此時runloop物件的timers陣列裡邊已經是空的了。runloop的mode裡沒有source1、沒有source0、也沒有timer源,所以就退出了】由此,也可以猜測:在runloop裡設定的timer觸發[self performSelector:@selector(testPerform) withObject:nil afterDelay:0]方法後,該timer就銷燬了。

怎樣讓runloop不退出呢?給當前runloop加入事件源或定時器temers,當前runloop就不會退出了,只是在不需要執行任務的時候進入休眠。

Runloop與performSelector
我在子執行緒中加入了timer後,通過觀察者的列印結果來看,該runloop一直沒有退出,所以3也就沒有列印出來。【注意,repeats引數要設定為YES,否則執行完timer之後,runloop就不再持有timer,runloop就退出來了。還可以通過[runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];加入事件源的方法,使runloop一直不退出。】

還有一個方法是- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;這個方法多了一個設定mode的引數,可以通過這個引數設定在timer在哪個mode下執行,讀者可自己檢測。

新增runloop觀察者的程式碼:

- (void)addObserver
{
    /*
     kCFRunLoopEntry = (1UL << 0),1
     kCFRunLoopBeforeTimers = (1UL << 1),2
     kCFRunLoopBeforeSources = (1UL << 2), 4
     kCFRunLoopBeforeWaiting = (1UL << 5), 32
     kCFRunLoopAfterWaiting = (1UL << 6), 64
     kCFRunLoopExit = (1UL << 7),128
     kCFRunLoopAllActivities = 0x0FFFFFFFU
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case 1:
            {
                NSLog(@"進入runloop");
            }
                break;
            case 2:
            {
                NSLog(@"timers");
            }
                break;
            case 4:
            {
                NSLog(@"sources");
            }
                break;
            case 32:
            {
                NSLog(@"即將進入休眠");
            }
                break;
            case 64:
            {
                NSLog(@"喚醒");
            }
                break;
            case 128:
            {
                NSLog(@"退出");
            }
                break;
            default:
                break;
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);//將觀察者新增到common模式下,這樣當default模式和UITrackingRunLoopMode兩種模式下都有回撥。
    self.obsever  = observer;
    CFRelease(observer);
}

複製程式碼

本篇記錄算是自己的理解,水平有限,如果有錯誤的地方,請批評指正,會盡快修改。


參考致謝:

iOS底層原理總結 - RunLoop

關於 performSelector 的一些小探討

NSRunLoop的退出方式

相關文章