簡體   English   中英

了解objc中塊內存管理的一個邊緣情況

[英]Understand one edge case of block memory management in objc

由於EXC_BAD_ACCESS下面的代碼將崩潰

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);}];
}

代碼在啟用了ARC的iOS 9中運行。 我試圖找出導致崩潰的原因。

通過po tmp in lldb我找到了

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

而在不會崩潰的版本

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

因此,我可能提出的最可能的原因是當ARC發布NSStackBlock時崩潰發生。 但為什么會這樣呢?

簡答

您發現了編譯器錯誤,可能是重新引入的錯誤,您應該在http://bugreport.apple.com上報告。

更長的答案

這並不總是一個錯誤,它曾經是一個功能 ;-)當Apple首次引入塊時,他們還引入了優化它們如何實現它們; 然而,與對代碼基本透明的普通編譯器優化不同,它們要求程序員在各個地方調用特殊函數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

首先,您需要了解如果要將塊存儲在聲明它的范圍之外,則需要復制它並存儲該副本。

這是因為優化,其中捕獲變量的塊最初位於堆棧上,而不是像常規對象那樣動態分配。 (讓我們忽略那些暫時不捕獲變量的塊,因為它們可以作為全局實例實現。)所以當你寫一個塊文字時,比如foo = ^{ ...}; ,這有點像向foo分配一個指向同一范圍內聲明的隱藏局部變量的指針,類似於some_block_object_t hiddenVariable; foo = &hiddenVariable; some_block_object_t hiddenVariable; foo = &hiddenVariable; 在同步使用塊的情況下,此優化減少了對象分配的數量,並且永遠不會超出創建它的范圍。

就像指向局部變量的指針一樣,如果你將指針指向它指向的東西的范圍之外,你有一個懸空指針,並且取消引用它會導致未定義的行為。 如果需要,在塊上執行復制會將堆棧移動到堆,其中它像所有其他Objective-C對象一樣進行內存管理,並返回指向堆副本的指針(如果塊已經是堆塊或全局塊) ,它只返回相同的指針)。

特定編譯器是否在特定情況下使用此優化是一個實現細節,但您不能假設它是如何實現的,因此如果將塊指針存儲在比當前范圍更長的位置,則必須始終復制(例如,在實例或全局變量中,或在可能超出范圍的數據結構中)。 即使您知道它是如何實現的,並且知道在特定情況下不需要復制(例如,它是一個不捕獲變量的塊,或者必須已經完成復制),您不應該依賴它,並且當你將它存放在一個比當前范圍更長的地方時,你應該總是復制,這是一種很好的做法。

將塊作為參數傳遞給函數或方法有點復雜。 如果將塊指針作為參數傳遞給聲明的編譯時類型是塊指針類型的函數參數,那么如果該函數比其范圍更長,則該函數將負責復制它。 因此,在這種情況下,您無需擔心復制它,而無需知道函數的功能。

另一方面,如果將塊指針作為參數傳遞給聲明編譯時類型為非塊對象指針類型的函數參數,則該函數不會對任何塊復制負責,因為所有它知道它只是一個常規對象,如果存儲在比當前范圍更長的地方,則需要保留。 在這種情況下,如果您認為該函數可能存儲超出調用結束的值,則應在傳遞之前復制該塊,然后傳遞該副本。

順便說一下,對於指定塊指針類型或轉換為常規對象指針類型的任何其他情況也是如此; 應該復制塊並分配副本,因為任何獲得常規對象指針值的人都不會進行任何塊復制注意事項。


ARC使情況有些復雜化。 ARC規范指定了隱式復制塊的一些情況。 例如,當存儲到編譯時塊指針類型的變量(或ARC要求保留編譯時塊指針類型的值的任何其他位置)時,ARC要求復制傳入值而不是保留,因此程序員不必擔心在這些情況下顯式復制塊。

除了在初始化__strong參數變量或讀取__weak變量時執行的保留之外,每當這些語義調用保留塊指針類型的值時,它都具有Block_copy的效果。

但是,作為例外,ARC規范不保證僅在復制參數時傳遞塊。

當優化器看到結果僅用作調用的參數時,可以刪除這些副本。

因此,是否要將作為參數傳遞的塊顯式復制到函數中仍然是程序員必須考慮的事情。

現在,Apple最新版本的Clang編譯器中的ARC實現具有一個未記錄的功能,它將隱式塊副本添加到作為參數傳遞塊的某些位置,即使ARC規范不需要它。 (“未記錄”,因為我找不到任何Clang文檔來實現這種效果。)特別是,當將塊指針類型的表達式傳遞給非塊對象指針類型的參數時,它似乎總是在防御性地添加隱式副本。 實際上,正如CRD所證明的那樣,它在從塊指針類型轉換為常規對象指針類型時也會添加隱式副本,因此這是更一般的行為(因為它包含參數傳遞大小寫)。

但是,當將塊指針類型的值傳遞為varargs時,似乎當前版本的Clang編譯器不會添加隱式副本。 C varargs不是類型安全的,調用者不可能知道函數期望的類型。 可以說,如果Apple想要在安全方面出錯,因為無法知道函數的期望,他們也應該在這種情況下添加隱式副本。 但是,既然這件事都是無證件的,我不會說這是一個錯誤。 在我看來,程序員不應該依賴於僅作為參數傳遞的塊,而這些塊首先被隱式復制。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM