2010-11-10 90 views
67

我開始使用塊了很多,很快就注意到了零塊導致總線錯誤:爲什麼nil/NULL塊在運行時會導致總線錯誤?

typedef void (^SimpleBlock)(void); 
SimpleBlock aBlock = nil; 
aBlock(); // bus error 

這似乎違背的Objective-C的日常行爲,忽略消息無對象:

NSArray *foo = nil; 
NSLog(@"%i", [foo count]); // runs fine 

因此,我不得不求助於通常爲零檢查之前,我使用塊:

if (aBlock != nil) 
    aBlock(); 

或者使用虛擬塊:

aBlock = ^{}; 
aBlock(); // runs fine 

還有其他的選擇嗎?爲什麼零塊不能簡單地是一個nop?

回答

134

我想解釋一下這個,有一個更完整的答案。首先,讓我們考慮下面的代碼:

#import <Foundation/Foundation.h> 
int main(int argc, char *argv[]) {  
    void (^block)() = nil; 
    block(); 
} 

如果你運行這個,那麼你會看到在block()線,看起來像這樣的碰撞(當在32位架構上運行 - 這一點很重要):

EXC_BAD_ACCESS(代碼= 2,地址= 0xC的)

那麼,這是爲什麼?那麼,0xc是最重要的一點。崩潰意味着處理器試圖讀取內存地址爲0xc的信息。這幾乎肯定是一件完全不正確的事情。這不太可能。但爲什麼它試圖讀取這個內存位置?那麼,這是由於實際上在遮罩下構建塊的方式。

當一個塊被定義,則編譯器實際上在堆棧上產生一個結構,這種形式的:

struct Block_layout { 
    void *isa; 
    int flags; 
    int reserved; 
    void (*invoke)(void *, ...); 
    struct Block_descriptor *descriptor; 
    /* Imported variables. */ 
}; 

塊是則將指向該結構。第四個成員,invoke,這個結構是有趣的。它是一個函數指針,指向塊的實現保存的代碼。所以當一個塊被調用時,處理器會嘗試跳轉到該代碼。請注意,如果您計算invoke成員之前的結構中的字節數,則會發現十進制數爲12,或十六進制數爲C.

所以當一個塊被調用時,處理器將獲取該塊的地址,加上12並嘗試加載該存儲器地址處的值。然後它試圖跳到那個地址。但如果該塊爲零,那麼它將嘗試讀取地址0xc。這是一個duff地址,很清楚,所以我們得到了分段錯誤。

現在,它必須是像這樣崩潰的原因,而不是像Objective-C消息調用那樣靜靜地失敗確實是一個設計選擇。由於編譯器正在完成確定如何調用塊的工作,因此必須在調用塊的任何位置注入零檢查代碼。這會增加代碼的大小並導致性能不佳。另一種選擇是使用蹦牀進行零檢查。但是這也會導致性能損失。 Objective-C消息已經通過蹦牀,因爲它們需要查找實際將被調用的方法。運行時允許延遲注入方法和更改方法實現,因此無論如何它已經經歷了一個蹦牀。在這種情況下,進行零檢查的額外處罰並不重要。

我希望能幫助一點點來解釋理由。

欲瞭解更多信息,請參閱我的blogposts

8

警告:我不是塊的專家。

objective-c objects但調用block is not a message,但你仍然可以嘗試[block retain]荷蘭國際集團一nil塊或其他消息。

希望這(和鏈接)有幫助。

+0

謝謝你,有趣的鏈接。我知道調用一個塊與發送一條消息並不相同,但從概念上講,如果nil塊與nil對象一樣寬容,那將會很好。 – zoul 2010-11-10 14:22:42

+0

您可以將類別添加到'__block'類型中......但我不確定。 '#define nilBlock^{}'也可能讓你的生活更輕鬆。 – 2010-11-10 14:25:05

+0

我想過'nilBlock'方法,不幸的是打字遇到了困難 - 爲每個塊類型創建一個單獨的nil值並不是很有趣。 – zoul 2010-11-10 14:30:17

2

這是我最簡單最好的解決方案......也許有可能用這些c var-args編寫一個通用運行函數,但我不知道該怎麼寫。

void run(void (^block)()) { 
    if (block)block(); 
} 

void runWith(void (^block)(id), id value) { 
    if (block)block(value); 
} 
38

馬特加洛韋的答案是完美的!很棒的閱讀!

我只想補充說,有一些方法可以讓生活更輕鬆。你可以像這樣定義一個宏:

#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil 

它可以帶0到n個參數。使用示例

typedef void (^SimpleBlock)(void); 
SimpleBlock simpleNilBlock = nil; 
SimpleBlock simpleLogBlock = ^{ NSLog(@"working"); }; 
BLOCK_SAFE_RUN(simpleNilBlock); 
BLOCK_SAFE_RUN(simpleLogBlock); 

typedef void (^BlockWithArguments)(BOOL arg1, NSString *arg2); 
BlockWithArguments argumentsNilBlock = nil; 
BlockWithArguments argumentsLogBlock = ^(BOOL arg1, NSString *arg2) { NSLog(@"%@", arg2); }; 
BLOCK_SAFE_RUN(argumentsNilBlock, YES, @"ok"); 
BLOCK_SAFE_RUN(argumentsLogBlock, YES, @"ok"); 

如果你想獲得該塊的返回值,你如果塊存在,或不知道沒有,那麼你可能最好只是打字:

block ? block() : nil; 

通過這種方式,您可以輕鬆定義故障預置值。在我的例子'零'。

+1

__VA_ARGS__在.mm文件中產生問題 – BergP 2013-10-22 08:06:49

+0

@PavelKatunin我從來沒有測試過 – hfossli 2013-10-22 09:02:04

相關問題