3

塊存儲管理的一個邊緣殼體下面的代碼會崩潰,因爲EXC_BAD_ACCESS理解objc

typedef void(^myBlock)(void); 

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    NSArray *tmp = [self getBlockArray]; 
    myBlock block = tmp[0]; 
    block(); 
} 

- (id)getBlockArray { 
    int val = 10; 
//crash version 
    return [[NSArray alloc] initWithObjects: 
      ^{NSLog(@"blk0:%d", val);}, 
      ^{NSLog(@"blk1:%d", val);}, nil]; 
//won't crash version 
// return @[^{NSLog(@"block0: %d", val);}, ^{NSLog(@"block1: %d", val);}]; 
} 

代碼在IOS 9 ARC運行啓用。我試圖找出導致崩潰的原因。

通過po tmp在LLDB我發現

(lldb) po tmp 
<__NSArrayI 0x7fa0f1546330>(
<__NSMallocBlock__: 0x7fa0f15a0fd0>, 
<__NSStackBlock__: 0x7fff524e2b60> 
) 

而在不會崩潰版本

(lldb) po tmp 
<__NSArrayI 0x7f9db481e6a0>(
<__NSMallocBlock__: 0x7f9db27e09a0>, 
<__NSMallocBlock__: 0x7f9db2718f50> 
) 

所以最可能的原因,我能想出是當ARC釋放NSStackBlock崩潰發生。但爲什麼會這樣呢?

回答

1

首先,你需要明白,如果你想存儲塊過去​​在那裏的申報範圍,則需要將其複製並保存複印件。

由於優化的原因,其中捕獲變量的塊最初位於堆棧上,而不是像常規對象那樣動態分配。 (讓我們忽略那些暫時不捕獲變量的塊,因爲它們可以作爲一個全局實例來實現)。因此,當你編寫一個塊文字時,如foo = ^{ ...};,這就好像爲foo賦值一個指向隱藏局部變量的指針在同樣的範圍內,像some_block_object_t hiddenVariable; foo = &hiddenVariable;這樣的優化可以減少很多情況下對象分配的數量,在這種情況下,塊被同步使用,永遠不會超出創建它的範圍。

與指向局部變量的指針一樣,如果您將指針放在指向它的事物範圍之外,則您有一個懸掛指針,並且解除引用會導致未定義的行爲。如果需要,在塊上執行復制將堆棧移到堆上,像所有其他Objective-C對象一樣進行內存管理,並返回指向堆副本的指針(並且如果塊已經是堆塊或全局塊它只是返回相同的指針)。

特定編譯器在特定情況下是否使用這種優化是一種實現細節,但您不能假定它是如何實現的,因此如果您將塊指針存儲在一個將超過當前範圍(例如,在實例或全局變量中,或者在可能超過範圍的數據結構中)。即使你知道它是如何實現的,並且知道在特定情況下複製不是必需的(例如,它是一個不捕獲變量的塊,或者必須已經完成複製),你不應該依賴它,並且作爲良好的做法,當你將它存儲在一個會超過當前範圍的地方時,你仍應該始終進行復制。

將塊作爲參數傳遞給函數或方法有點複雜。如果將塊指針作爲參數傳遞給其聲明的編譯時類型爲塊指針類型的函數參數,那麼該函數將負責複製該函數,以使其超出範圍。所以在這種情況下,你不需要擔心複製它,而不需要知道函數做了什麼。

另一方面,如果您將塊指針作爲參數傳遞給聲明的編譯時類型爲非塊對象指針類型的函數參數,那麼該函數將不承擔任何塊的責任複製,因爲它知道它只是一個普通的對象,如果存儲在超過當前範圍的地方,只需要保留。在這種情況下,如果您認爲該函數可能將值存儲在調用結束之後,則應該在傳遞該塊之前先複製該塊,然後傳遞該副本。順便說一下,對於塊指針類型被分配或轉換爲常規的對象指針類型的情況,這也是正確的。應該複製該塊並分配副本,因爲任何獲得常規對象指針值的人都不希望執行任何塊複製注意事項。


ARC在某種程度上使情況複雜化。 ARC規範specifies某些情況下塊被隱式複製。例如,當存儲到編譯時塊指針類型的變量(或ARC需要在編譯時塊指針類型的值上保留的任何其他位置)時,ARC要求複製傳入值而不是保留,所以程序員不必擔心在這些情況下顯式複製塊。

隨着保留做作爲初始化 __strong參數變量或閱讀__weak變量的一部分外,每當 這些語義調用用於保持塊指針類型的值,它 具有Block_copy的效果。

但是,作爲一個例外,ARC規範並不保證僅當參數被複制時才傳遞塊。

當它看到結果是 僅用作調用的參數時,優化程序可能會刪除這些副本。

因此,無論明確複製作爲參數傳遞給一個功能塊卻仍然是程序員必須考慮的問題。

現在,在最近版本的Apple Clang編譯器中的ARC實現有一個未公開的功能,它將向塊的一些地方添加隱式塊拷貝作爲參數,即使ARC規範不需要它。 (「無證」,因爲我找不到任何Clang文檔來達到這種效果)。特別是,在將塊指針類型的表達式傳遞給非塊對象指針類型的參數時,它似乎總是在防禦中添加隱式副本。實際上,如CRD所示,它在從塊指針類型轉換爲常規對象指針類型時也添加了隱式副本,所以這是更一般的行爲(因爲它包含參數傳遞大小寫)。

但是,看起來Clang編譯器的當前版本在將塊指針類型的值作爲可變參數傳遞時不會添加隱式副本。 C可變參數不是類型安全的,調用者不可能知道函數期望的類型。可以說,如果蘋果想要在安全方面犯錯誤,由於無法知道函數期望的是什麼,所以在這種情況下,它們總是應該添加隱式副本。然而,由於這整個事情無論如何都是無證的功能,我不會說這是一個錯誤。在我看來,程序員不應該依賴只能作爲參數被隱式複製的塊傳遞。

+0

感謝您的詳細解答。我相信C可變參數不是類型安全的,這是對這種情況更準確的解釋。 – dopcn

+0

@dopcn - newacct我傾向於不同意規範在塊和ARC方面的狀態。不幸的是,蘋果的文檔並不總是那麼清晰和全面,並且不僅僅是公平的,而且涉及到一定程度的解釋。請將您的案例作爲一個錯誤提交給Apple;他們可以修復它,說它按預期工作,或者什麼都不說;但你會提醒他們。如果他們確實回覆了有用的回覆,可以將其添加到上面的問題中作爲幫助其他人的附錄。 – CRD

3

簡答

你已經找到一個編譯器缺陷,可能重新引進一個,你應該在http://bugreport.apple.com報告。

再回應

這並非總是一個錯誤,它曾經是一個功能 ;-)當蘋果首次推出塊他們也介紹了他們是如何實現他們的優化;然而,與對代碼基本透明的普通編譯器優化不同,它們要求程序員在各個地方將調用分配給特殊函數block_copy()以使優化工作。

多年來,Apple取消了對此的需求,但僅限於使用ARC的程序員(儘管他們本可以爲MRC用戶也這樣做),而今天的優化應該只是這樣,程序員不再需要幫助編譯器一起。

但是你剛剛發現了一個編譯器錯誤的例子。

技術上必須的情況下的類型損失,在這種情況下一些已知爲塊作爲id傳遞 - 減少了已知類型的信息,並在特定類型的損失,涉及在一個變量的第二個或後續的參數參數列表。當你用po tmp來查看你的數組時,你會發現第一個值是正確的,儘管存在類型丟失,編譯器仍然可以正確地判斷這個值,但是它在下一個參數上失敗。

數組的字面語法不依賴於可變參數函數,並且生成的代碼是正確的。但initWithObjects:確實會出錯,並且會出錯。

解決方法

如果強制轉換爲id添加到第二(和任何隨後的)塊則編譯器產生正確的代碼:

return [[NSArray alloc] initWithObjects: 
     ^{NSLog(@"blk0:%d", val);}, 
     (id)^{NSLog(@"blk1:%d", val);}, 
     nil]; 

這似乎是足以喚醒編譯起來。

HTH

+0

解決方法已通過驗證。感謝您的回答。但我想知道更多關於碰撞原因的信息。如果沒有類型轉換,'NSStackBlock'也是有效的,並在調用時運行。爲什麼釋放它會導致崩潰?還是別的會導致崩潰? – dopcn

+0

'NSStackBlock'不是一個普通的對象 - 它是上面提到的優化的結果 - 它永遠不應該存儲在一個數組(或任何其他對象)中。它僅作爲參數傳遞給方法,並且只在調用方(即創建它傳遞給另一方的方法)在調用堆棧上仍然有效時才起作用。違反任何這些規則,所有的賭注都沒有了,編譯器不會幫助你。 – CRD