Runloop與performSelector
自己平常開發中比較少用到 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。

testPerform
。
官方註釋:

那按照官方文件說明在子執行緒中加入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對比:

testPerform
執行的timer,為什麼還依然沒有執行。因為runloop沒有跑起來。 所以建立完runloop後,還需要runloop跑起來。【通過給當前runloop新增觀察者,檢視runloop的狀態,runloop沒有跑起來】當我們呼叫
[runloop run];
方法後,將runloop跑起來後,
testPerform
才會執行。列印結果為1,2,3。
但,問題又來了,既然加入了runloop,並且跑起來了,為什麼3還會打印出來,runloop不是相當於死迴圈嗎?迴圈外的3為什麼會打印出來?這個問題,通過加入的runloop的觀察者的列印情況可以看出來,是因為,runloop在執行完 testPerform
後,就退出了。所以下邊的3頁打印出來了。
觀察者列印:

testPerform
方法內列印runloop,看到此時runloop物件的timers數組裡邊已經是空的了。runloop的mode裡沒有source1、沒有source0、也沒有timer源,所以就退出了】由此,也可以猜測:在runloop裡設定的timer觸發
[self performSelector:@selector(testPerform) withObject:nil afterDelay:0]
方法後,該timer就銷燬了。
怎樣讓runloop不退出呢?給當前runloop加入事件源或定時器temers,當前runloop就不會退出了,只是在不需要執行任務的時候進入休眠。

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); } 複製程式碼
本篇記錄算是自己的理解,水平有限,如果有錯誤的地方,請批評指正,會盡快修改。
參考致謝: