Objective-C運行時原理(四):Block

關(guān)于block的語法,請使勁戳這里→fuckingblocksyntax.com

這篇文章只記錄一下block的實現(xiàn),和block使用的注意事項。

正文:

1.block的數(shù)據(jù)結(jié)構(gòu)

首先,關(guān)于block的數(shù)據(jù)結(jié)構(gòu)和runtime是開源的,可以在llvm項目看到,或者下載蘋果的libclosure庫的源碼來看。蘋果也提供了在線的代碼查看方式,其中包含了很多示例和文檔說明。

這兩個地方的定義是相同的:

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};
 
struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

在objc中,根據(jù)對象的定義,凡是首地址是*isa的結(jié)構(gòu)體指針,都可以認為是對象(id)。這樣在objc中,block實際上就算是對象。

為了查看編譯器具體的工作,這里可以用clang重寫一段代碼試試看:

void foo_(){
    int i = 2;
    NSNumber *num = @3;
 
    long (^myBlock)(void) = ^long() {
        return i * num.intValue;
    };
 
    long r = myBlock();
}

上面這是一個很簡單的block,捕獲了兩個變量:一個int,一個NSNumber。

用clang翻譯成C++后變出了一大坨代碼,看著別扭不貼上來了。為了方便理解,這里稍微簡化和調(diào)整一下:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};
 
struct __foo_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(struct __foo_block_impl_0*, struct __foo_block_impl_0*);
    void (*dispose)(struct __foo_block_impl_0*);
};
 
//myBlock的數(shù)據(jù)結(jié)構(gòu)定義
struct __foo_block_impl_0 {
    struct __block_impl impl;
    struct __foo_block_desc_0* Desc;
    int i;
    NSNumber *num;
};
 
//block數(shù)據(jù)的描述
static struct __foo_block_desc_0 __foo_block_desc_0_DATA = {
    0,
    sizeof(struct __foo_block_impl_0),
    __foo_block_copy_0,
    __foo_block_dispose_0
};
 
//block中的方法
static long __foo_block_func_0(struct __foo_block_impl_0 *__cself) {
    int i = __cself->i; // bound by copy
    NSNumber *num = __cself->num; // bound by copy
 
    return i * num.intValue;
}
 
void foo(){
    int i = 2;
    NSNumber *num = @3;
 
    struct __foo_block_impl_0 myBlockT;
    struct __foo_block_impl_0 *myBlock = &myBlockT;
    myBlock->impl.isa = &_NSConcreteStackBlock;
    myBlock->impl.Flags = 570425344;
    myBlock->impl.FuncPtr = __foo_block_func_0;
    myBlock->Desc = &__foo_block_desc_0_DATA;
    myBlock->i = i;
    myBlock->num = num;
 
    long r = myBlock->impl.FuncPtr(myBlock);
}

編譯器會根據(jù)block捕獲的變量,生成具體的結(jié)構(gòu)體定義。block內(nèi)部的代碼將會提取出來,成為一個單獨的C函數(shù)。創(chuàng)建block時,實際就是在方法中聲明一個struct,并且初始化該struct的成員。而執(zhí)行block時,就是調(diào)用那個單獨的C函數(shù),并把該struct指針傳遞過去。

block中包含了被引用的自由變量(由struct持有),也包含了控制成分的代碼塊(由函數(shù)指針持有),符合閉包(closure)的概念。

2.block的Copy

block中的isa指向的是該block的Class。在block runtime中,定義了6種類:

_NSConcreteStackBlock 棧上創(chuàng)建的block
_NSConcreteMallocBlock 堆上創(chuàng)建的block
_NSConcreteGlobalBlock 作為全局變量的block
_NSConcreteWeakBlockVariable
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock

其中我們能接觸到的主要是前3種,后三種用于GC不再討論..

上面代碼可以看到,當struct第一次被創(chuàng)建時,它是存在于該函數(shù)的棧幀上的,其Class是固定的_NSConcreteStackBlock。其捕獲的變量是會賦值到結(jié)構(gòu)體的成員上,所以當block初始化完成后,捕獲到的變量不能更改。

當函數(shù)返回時,函數(shù)的棧幀被銷毀,這個block的內(nèi)存也會被清除。所以在函數(shù)結(jié)束后仍然需要這個block時,就必須用Block_copy()方法將它拷貝到堆上。這個方法的核心動作很簡單:申請內(nèi)存,將棧數(shù)據(jù)復制過去,將Class改一下,最后向捕獲到的對象發(fā)送retain,增加block的引用計數(shù)。詳細代碼可以直接點這里查看。

struct Block_layout *result = malloc(aBlock->descriptor->size);
memmove(result, aBlock, aBlock->descriptor->size);
result->isa = _NSConcreteMallocBlock;
_Block_call_copy_helper(result, aBlock);
return result;

3.__block類型的變量

默認block捕獲到的變量,都是賦值給block的結(jié)構(gòu)體的,相當于const不可改。為了讓block能訪問并修改外部變量,需要加上__block修飾詞。

舉個例子:

void foo(){
    __block int i = 3;
    void(^myBlock)(void) = ^{
        i *= 2;
    };
    myBlock();
}

讓clang重寫一下:

struct Block_byref { //Block_private.h中的定義
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};
 
//__block count的實現(xiàn)
struct __Block_byref_count_0 {
    void *__isa;
    __Block_byref_count_0 *__forwarding;
    int __flags;
    int __size;
    int count;
};
 
void foo_(){
    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 1};
 
    void(*myBlock)(void) = (void (*)())&__foo__block_impl_0((void *)__foo__block_func_0, &__foo__block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344);
 
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}

嘩~一下子變出來一坨東西。就因為加了個__block,原本的int值的位置變成了一個struct(struct __Block_byref)。這個struct的首地址為同樣為*isa。

正是如此,這個值才能被block共享、并且不受棧幀生命周期的限制、在block被copy后,能夠隨著block復制到堆上。

4.使用注意事項

block對變量的捕獲規(guī)則:

  1. 靜態(tài)存儲區(qū)的變量:例如全局變量、方法中的static變量
    引用,可修改。

  2. block接受的參數(shù)
    傳值,可修改,和一般函數(shù)的參數(shù)相同。

  3. 棧變量 (被捕獲的上下文變量)
    const,不可修改。 當block被copy后,block會對 id類型的變量產(chǎn)生強引用。
    每次執(zhí)行block時,捕獲到的變量都是最初的值。

  4. 棧變量 (有__block前綴)
    引用,可以修改。如果時id類型則不會被block retain,必須手動處理其內(nèi)存管理。
    如果該類型是C類型變量,block被copy到heap后,該值也會被挪動到heap


注意1.內(nèi)存

Block_copy()和Block_release()必須一一匹配,否則會內(nèi)存泄漏或crash。

__block這個修飾詞會將原本的簡單類型轉(zhuǎn)化為較大的struct,這會給內(nèi)存、調(diào)用帶來額外的開銷,使用時需要注意。

注意2.ARC

在開啟ARC后,block的內(nèi)存會比較微妙。ARC會自動處理block的內(nèi)存,不用手動copy/release。
但是,和非ARC的情況有所不同:

void (^aBlock)(void);
aBlock = ^{ printf("ok"); };

block是對象,所以這個aBlock默認是有__strong修飾符的,即aBlock對該block有strong references。即aBlock在被賦值的那一刻,這個block會被copy。所以,ARC開啟后,所能接觸到的block基本都是在堆上的。。

void (^aBlock)(void) = nil; 
if (!aBlock) {
    aBlock = ^{ printf("hehe"); };
}

//block此時block已經(jīng)被釋放,該處留下了一個dangling pointer
aBlock();
上面這個例子,如果是非ARC時,block還在棧幀上,所以沒問題。但開啟ARC后,block會被先copy到堆上,然后再被釋放,這里就會crash了(經(jīng)測試現(xiàn)在不會crash)。所以這時就必須手動調(diào)用Block_copy了。蘋果建議盡量避免這種情況。

注意3.循環(huán)引用

當block被copy之后(如開啟了ARC、或把block放入dispatch queue),該block對它捕獲的對象產(chǎn)生strong references (非ARC下是retain),
所以有時需要避免block copy后產(chǎn)生的循環(huán)引用。

如果用self引用了block,block又捕獲了self,這樣就會有循環(huán)引用。
因此,需要用weak來聲明self

- (void)configureBlock {
    XYZBlockKeeper * __weak weakSelf = self;
    self.block = ^{
        [weakSelf doSomething]; //捕獲到的是弱引用
    }
}

如果捕獲到的是當前對象的成員變量對象,同樣也會造成對self的引用,同樣也要避免。

- (void)configureBlock {
    id tmpIvar = _ivar; //臨時變量,避免了self引用
    self.block = ^{
        [tmpIvar msg];
    }
}

為了避免循環(huán)引用,可以這樣理解block:block就是一個對象,它捕獲到的值就是這個對象的@property(strong)。這樣在遇到問題時,就能迅速確定是否有循環(huán)引用了。Xcode5已經(jīng)能自動發(fā)現(xiàn)這種問題了,不錯~

PS:Pro Multithreading and Memory Management for iOS and OS X 這是一本好書,強烈推薦。
PSS:后來才發(fā)現(xiàn)原來這是本日文原版書,并且有中文版翻譯。名字叫做"Objective-C高級編程:iOS與OS X多線程和內(nèi)存管理"。名字差那么多?。。“Α?。買到中文版才發(fā)現(xiàn)之前看過。。

本文轉(zhuǎn)自objc 中的 block

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

相關(guān)閱讀更多精彩內(nèi)容

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