自己平常開發中比較少用到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,看下執行效果。- (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也有了觸發方法testPerform
執行的timer,為什麼還依然沒有執行。因為runloop沒有跑起來。
所以建立完runloop後,還需要runloop跑起來。【通過給當前runloop新增觀察者,檢視runloop的狀態,runloop沒有跑起來】當我們呼叫[runloop run];
方法後,將runloop跑起來後,testPerform
才會執行。列印結果為1,2,3。
但,問題又來了,既然加入了runloop,並且跑起來了,為什麼3還會列印出來,runloop不是相當於死迴圈嗎?迴圈外的3為什麼會列印出來?這個問題,通過加入的runloop的觀察者的列印情況可以看出來,是因為,runloop在執行完testPerform
後,就退出了。所以下邊的3頁列印出來了。
觀察者列印:
可以看出,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就不會退出了,只是在不需要執行任務的時候進入休眠。
我在子執行緒中加入了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);
}
複製程式碼
本篇記錄算是自己的理解,水平有限,如果有錯誤的地方,請批評指正,會盡快修改。
參考致謝: