《編寫高質(zhì)量iOS與OS X代碼的52個有效方法》--第六章 第42條
(ps:此乃讀書筆記惊橱,加深記憶,僅供大家參考)
第42條:多用GCD箭昵,少用performSelector系列方法
Objective-C本質(zhì)上是一門非常動態(tài)的語言(參見第11條)税朴,NSObject定義了幾個方法,令開發(fā)者可以隨意調(diào)用任何方法家制。這幾個方法可以推遲執(zhí)行方法調(diào)用正林,也可以指定運行方法所用的線程。這些功能原來很有用颤殴,但是在出現(xiàn)了大中樞派發(fā)及塊這樣的新技術之后觅廓,就顯得不那么必要了。雖說有些代碼還是會經(jīng)常用到它們涵但,但筆者勸你還是避開為妙杈绸。
這其中最簡單的是“performSelector:”。該方法與直接調(diào)用選擇子等效矮瘟。所以下面兩行代碼的執(zhí)行效果相同:
[self performSelector:@selector(selectorName)];
[self selectorName];
這種方式看上去似乎多余瞳脓。如果選擇子是在運行期決定的,那么就能體現(xiàn)出此方式的強大之處了澈侠。這就等于在動態(tài)綁定之上再次使用動態(tài)綁定劫侧,因而可以實現(xiàn)出下面這種功能:
SEL selector;
if (/* some condition */) {
selector = @selector(foo);
}else if (/* some other condition */){
selector = @selector(bar);
}else{
selector = @selector(baz);
}
[object performSelector:selector];
這種編程方式極為靈活,經(jīng)成诳校可用來簡化復雜的代碼烧栋。還有一種用法,就是先把選擇子保存起來拳球,等某個事件發(fā)生之后再調(diào)用审姓。不管哪種用法,編譯器都不知道要執(zhí)行的選擇子是什么醇坝,這必須到了運行期才能確定邑跪。然而,使用此特性的代價是呼猪,如果在ARC下編譯代碼,那么編譯器會發(fā)出如下警示信息:
warning:PerformSelector may cause a leak because its selector is unknown
你可能沒料到會出現(xiàn)這種警告砸琅。這條消息看上去可能比較奇怪宋距,而且令人納悶:為什么其中會提到內(nèi)存泄漏問題呢?原因在于症脂,編譯器并不知道將要調(diào)用的選擇子是什么谚赎,因此也就不了解其方法簽名及返回值淫僻,甚至連是否有返回值都不清楚。而且壶唤,由于編譯器不知道方法名雳灵,所以就沒辦法運用ARC的內(nèi)存管理規(guī)則來判定返回值是不是應該釋放,鑒于此闸盔,ARC采用了比較謹慎的做法悯辙,就是不添加釋放操作。然而這么做可能導致內(nèi)存泄漏迎吵,因為方法在返回對象時 可能已經(jīng)將其保留了躲撰。
考慮下面這段代碼:
SEL selector;
if (/* some condition */) {
selector = @selector(newObject);
}else if (/* some other condition */){
selector = @selector(copy);
}else{
selector = @selector(someProperty);
}
id ret = [object performSelector:selector];
如果調(diào)用的是兩個選擇子之一,那么ret對象應由這段代碼來釋放击费,如果是第三個選擇子拢蛋,則無須釋放。不僅在ARC環(huán)境下應該如此蔫巩,而且在非ARC環(huán)境下也應該這么做谆棱,這樣才算嚴格遵循了方法的命名規(guī)范。如果不使用ARC(此時編譯器也就不發(fā)警告信息了)圆仔,那么在前兩種情況下需要手動釋放ret對象础锐,而在后一種情況下則不需要釋放。這個問題很容易忽視荧缘,而且就算用靜態(tài)分析器皆警,也很難偵測到隨后的內(nèi)存泄漏。performSelector系列的方法之所以要謹慎使用截粗,這就是其中一個原因信姓。
這些方法不甚理想,另一個原因在于:返回值只能是void或?qū)ο箢愋统衤蕖1M管所要執(zhí)行的選擇子也可以返回void意推,但是performSelector方法的返回值類型畢竟是id。如果想返回整數(shù)或浮點數(shù)等類型的值珊蟀,那么就需要執(zhí)行一些復雜的轉(zhuǎn)換操作了菊值,而這種轉(zhuǎn)換很容易出錯。由于id類型表示指向任意Objective-C對象的指針育灸,所以從技術上來講腻窒,只要返回值的大小和指針所占大小相同就行。若返回值的類型為C語言的結構體磅崭,則不可使用performSelector方法儿子。
performSelector還有如下幾個版本,可以再發(fā)消息時順便傳遞參數(shù):
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
這些方法貌似有用砸喻,但其實局限頗多柔逼。由于參數(shù)類型是id蒋譬,所以傳入的參數(shù)必須是對象才行。如果選擇子所接受的參數(shù)是整數(shù)或浮點數(shù)愉适,那就不能采用這些方法了犯助。此外,選擇子最多只能接受兩個參數(shù)维咸,而在參數(shù)不止兩個的情況下剂买,則沒有對應的performSelector方法能夠執(zhí)行此種選擇子。
performSelector系列方法還有個功能腰湾,就是可以延后執(zhí)行選擇子雷恃,或?qū)⑵浞旁诹硪粋€線程上執(zhí)行。
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
然而很快就會發(fā)現(xiàn)费坊,這些方法太過局限了倒槐。如果要用這些方法,就得把許多參數(shù)都打包到字典中附井,然后在受調(diào)用的方法里將其提取出來讨越,這樣會增加開銷,而且還可能出bug永毅。
如果改用其他替代方案把跨,那就不受這些限制了。最主要的替代方案就是使用塊(參見第37條)沼死。而且着逐,performSelector系列方法所提供的線程功能,都可以通過在大中樞派發(fā)機制中使用塊來實現(xiàn)意蛀。延后執(zhí)行可以用dispatch_after來實現(xiàn)耸别,在另一個線程上執(zhí)行任務則可通過dispatch_sync及dispatch_async來實現(xiàn)。
例如县钥,延后執(zhí)行某項任務:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
[self doSomething];
});
想把任務放在主線程上執(zhí)行:
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
要點
- performSelector系列方法在內(nèi)存管理方面容易有疏失秀姐。它無法確定將要執(zhí)行的選擇子具體是什么,因而ARC編譯器也就無法插入適當?shù)膬?nèi)存管理方法若贮。
- performSelector系列方法所能處理的選擇子太過局限了省有,選擇子的返回值類型及發(fā)送給方法的參數(shù)個數(shù)都受到限制。
- 如果想把任務放在另一個線程上執(zhí)行谴麦,那么最好不要用performSelector系列方法蠢沿,而是應該把任務封裝到塊里,然后調(diào)用大中樞派發(fā)機制的相關方法來實現(xiàn)细移。