關於performSelector:afterDelay:的一個坑及思考

霖溦發表於2018-11-16

原文連結: kukumalucn.github.io/blog/2018/1…

前言

剛在群裡看到這樣一段程式碼,很有意思:

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

這段程式碼的執行結果會是什麼呢? 是列印“1、2”,還是“1、3、2”,或者是“1、2、3”?

內容

1.問題探究

這其實是一道很有意思的面試題,內容涉及runloop這個知識點。 答案是隻列印:“1、2”。 原因群裡的大神給瞭解答:

因為[self performSelector:@selector(test) withObject:nil afterDelay:.0]實際在runloop裡面,是一個定時器,但是因為在子執行緒,runloop是預設沒有開啟的。

這除了涉及runloop,還有多執行緒的問題,有興趣的可以深究。 其實我們只要仔細閱讀蘋果API的註釋,就能解釋這個問題:

關於performSelector:afterDelay:的一個坑及思考

想要執行-test方法,註釋裡也提供瞭解決辦法:

[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
複製程式碼

其實針對上述的邏輯,更簡單的是:

[self performSelector:@selector(test) withObject:nil];
複製程式碼

2.引發的思考

2.1.不要懶

之所以要提上述的問題,除了這個面試的“考點”,其實在平時的開發過程中也要注意自己程式碼的嚴謹性。 我發現自己在閱讀別人的程式碼時,就見過同樣的寫法,其實甚至那些比較有名的三方庫,例如“YYText”中,也有類似的程式碼存在:

[self performSelector:@selector(test) withObject:nil afterDelay:0];
複製程式碼

寫這段程式碼的人只是為了通過selector來立刻執行某一方法,delay並不是他們的需求,為什麼還要“多此一舉”呢? 這裡一大部分原因,很可能還是因為我們被xcode的自動提示給“慣壞了”:

關於performSelector:afterDelay:的一個坑及思考

畢竟當你寫程式碼時,羅列的一堆提示,只是按照API相似度排列出來的,很多人看到了自己需要的就直接回車了,不需要delay,直接寫0,就行了,反正“都一樣”…… 其實這是一個誤區,看起來很相似的API,實則並不一樣,而且很不一樣:

  • 我們常用的這個perform,是NSObject.h這個標頭檔案下的方法:

    關於performSelector:afterDelay:的一個坑及思考

  • 可以delay的,是NSRunLoop.h下的方法:

    關於performSelector:afterDelay:的一個坑及思考

  • 而之前提到的回撥主執行緒的,是NSThread.h裡的方法:

    關於performSelector:afterDelay:的一個坑及思考

雖然他們都是NSObject的方法或者是分類補充方法,但實際上,是隸屬於不同的模組的。

2.2.更深刻的原因

但是“YYText”的作者應該是不會犯這種低階錯誤的,那就應該還有更深刻的原因了:

關於performSelector:afterDelay:的一個坑及思考

我們很多人應該總是會被上述的警告所困擾,大多數人的解決方式,就是利用類似相面的方式去遮蔽警告,這種做法雖然簡單,但實際是有風險的:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
//code
#pragma clang diagnostic pop
複製程式碼

其實除了利用IMP或者NSInvocation那種比較“高階”的方式,更多的情況下,在方法沒有返回值時,或者我們不需要返回值時,我們可以用:

[self performSelector:@selector(test) withObject:nil afterDelay:0];
複製程式碼

這種方式去避免警告的,看上面的那三個對比你就會發現,後兩類API,同樣是performSelector,卻沒有返回值,這其實也是有官方註釋的依據的:

關於performSelector:afterDelay:的一個坑及思考

但其實你也要注意到了,官方的建議還是很嚴謹的,是用performSelectorOnMainThread,而不是delay0的方式,至於原因,我們又回到了文章一開頭的討論了。

總結

通過上面看似無意義的探究,我們還是可以得到很深刻的教訓的:“蘋果霸霸”還是很嚴謹的,多看API的註釋,總是沒錯的。


本文作者: 霖溦
本文連結: kukumalucn.github.io/blog/2018/1…
版權宣告: 本部落格所有文章除特別宣告外,均採用 CC BY-NC-ND 4.0 許可協議。轉載請註明出處!

相關文章