《Objective-C高級編程 iOS與OS X多線程與內(nèi)存管理》13

Blocks篇:4.Blocks的存儲域

在上一節(jié)中我們知道,在Block捕獲不同種類的變量時,生成的Block對象的類型(isa指針)分為三種:

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

此三種類型的Block對象分別存儲在棧區(qū)、全局(數(shù)據(jù)區(qū))和堆區(qū)

我們知道,由于Block對象的函數(shù)體定義在Block實例化的生命周期外部,故其執(zhí)行時早已不在原作用域內(nèi)。況且,由于在函數(shù)中,定義的Block對象也是局部變量,超出作用域也會被自動回收。所以,要保證Block超出原作用域仍然可以存在的方式,就是將其轉(zhuǎn)化為全局Block,或者復(fù)制到堆內(nèi)存中,這樣才可以保證其內(nèi)存可控并正確執(zhí)行Block的函數(shù)。在ARC環(huán)境下,LLVM在編譯期已經(jīng)可以在絕大多數(shù)情況下正確處理這種情況。通過測試,編碼時定義的局部Block變量(即_NSConcreteStackBlock對象),在運行時可以得到如下結(jié)果:

捕獲變量情況 運行期生成的Block對象
_NSConcreteGlobalBlock
全局或靜態(tài)變量 _NSConcreteGlobalBlock
普通局部變量 _NSConcreteMallocBlock

但是,發(fā)現(xiàn)在一種情況下,ARC不會自動處理,需要我們對Block對象進(jìn)行手動轉(zhuǎn)換。

1.ARC下的Blocks陷阱

先看代碼:

// main.m

typedef void(^VoidBlock)(void);

/** 返回包含Block對象的集合的函數(shù) */
NSArray *getBlocksArray () {
    int myVal = 2;
    // 內(nèi)部的Block對象均為__NSConcreteStackBlock對象
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"block1~%d", myVal);},
            ^{NSLog(@"block2~%d", myVal);},
            nil
            ];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 獲取該數(shù)組
        NSArray *blocksArray = getBlocksArray();
        // 取出Block對象
        VoidBlock voidBlock = blocksArray[0];
        // 執(zhí)行
        voidBlock();
    }
    return 0;
}

執(zhí)行情況,我們可以直接得到個漂亮的“EXC_BAD_ACCESS”。

Block在集合中的坑.jpg

在圖中已經(jīng)看出,這種情況下,編譯器并沒有將捕獲有變量的Block拷貝至堆中。故在準(zhǔn)備執(zhí)行時,Block對象已經(jīng)被釋放(第一個被轉(zhuǎn)成__NSConcreteMallocBlock的原因是由于NSArray的init方法會自動保留對象,進(jìn)而發(fā)生了Block的copy操作)。當(dāng)執(zhí)行完畢后,由于數(shù)組對象的釋放,在對其內(nèi)部元素依次釋放時訪問了野指針,導(dǎo)致崩潰。

所以,在集合中使用Block對象時,為了保證其安全性,我們可以有兩種方式:

  1. 手動將Block復(fù)制到堆中:
NSArray *getBlocksArray () {
    int myVal = 2;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"block1~%d", myVal);} copy],
            [^{NSLog(@"block2~%d", myVal);} copy],
            nil
            ];
}

由于Block是OC對象,故對其發(fā)送copy消息可以直接將其轉(zhuǎn)換為__NSConcreteMallocBlock對象。

  1. 在初始化Block時,利用ARC的特性,對Block進(jìn)行顯式聲明,以獲取“__strong”修飾的Block,自動生成__NSConcreteMallocBlock對象
NSArray *getBlocksArray () {
    int myVal = 2;
    
    // 生成了強引用Block變量,自動分配到了堆內(nèi)存中
    VoidBlock block1 = ^{NSLog(@"block1~%d", myVal);};
    VoidBlock block2 = ^{NSLog(@"block2~%d", myVal);};

    return [[NSArray alloc] initWithObjects:
            block1,
            block2,
            nil
            ];
}

注意:例外情況

在系統(tǒng)帶有Block參數(shù)的API中(如GCD或是Animation相關(guān)等等),無需手動對Block進(jìn)行復(fù)制(其內(nèi)部實現(xiàn)已經(jīng)包含了復(fù)制操作)。

2.Blocks的保留操作

2.1 Blocks的保留解析

我們知道,在生成__strong修飾的Block對象時,其實隱含的對生成的對象進(jìn)行了retain操作。此操作實際為:

VoidBlock blockObj = ...;
// 對Block進(jìn)行保留操作
objc_retainBlock(blockObj);
...

在NSObject.mm中,我們找到了此方法的實現(xiàn):

// NSObject.mm

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

因此,對Block進(jìn)行retain其實也就是進(jìn)行了copy操作,進(jìn)而在堆上生成了Block。

2.2 Blocks的copy操作

現(xiàn)在,我們知道了對棧中的Block進(jìn)行復(fù)制或保留操作,會在堆內(nèi)存上生成對應(yīng)的Block對象。但對于其他兩者呢?

copy對應(yīng)Block 效果
_NSConcreteGlobalBlock 無作用
_NSConcreteMallocBlock 引用計數(shù) + 1

對于堆內(nèi)存中的Block對象,其實是遵循了引用計數(shù)的內(nèi)存管理方式。因此,在使用Block對象時,也要注意引用循環(huán)問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容