Block的本質(zhì)

當(dāng)需要執(zhí)行異步操作泡垃,或同步多個(gè)操作時(shí),塊(Block)會(huì)非常有用。這一篇文章將介紹 Block 的本質(zhì)夹姥。如果你對(duì) block 還不了解,推薦先查看Block的用法辙诞。

1. Block的本質(zhì)

Block 是封裝了函數(shù)調(diào)用及函數(shù)調(diào)用環(huán)境的 Objective-C 對(duì)象辙售,內(nèi)部也有一個(gè) isa 指針。即 Block 本質(zhì)上也是一個(gè) Objective-C 對(duì)象倘要。

下面寫一個(gè)簡(jiǎn)單的 block:

        int age = 10;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d", age);
        };
        myblock();

使用 clang 命令將上述代碼轉(zhuǎn)化為 C++圾亏,方便查看 block 內(nèi)部結(jié)構(gòu):

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

轉(zhuǎn)化后如下:

        int age = 10;
        // 定義block變量
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
        // 調(diào)用block
        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);

1.1 聲明 block

BlockDeclare.png
1.1.1 __main_block_impl_0

通過(guò)轉(zhuǎn)化的 C++ 代碼可以看到,block定義中調(diào)用了__main_block_impl_0函數(shù)封拧,并將其地址賦值給myblock志鹃。進(jìn)一步查看__main_block_impl_0函數(shù):

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  
  // 該構(gòu)造函數(shù)最終返回__main_block_impl_0。會(huì)將傳入的_age賦值給成員age泽西。
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0結(jié)構(gòu)體內(nèi)的構(gòu)造函數(shù)對(duì)變量進(jìn)行賦值曹铃,最終返回__main_block_impl_0結(jié)構(gòu)體,也就是最終返回給myblock變量的是__main_block_impl_0結(jié)構(gòu)體捧杉。

1.1.2 __main_block_func_0

__main_block_impl_0函數(shù)的第一個(gè)參數(shù)是__main_block_func_0陕见,其定義如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_63c9df_mi_0, age);
}

__main_block_func_0函數(shù)內(nèi)存儲(chǔ)著 block 內(nèi)代碼秘血。其函數(shù)內(nèi)部先取出局部變量 age,后面調(diào)用NSLog评甜。

也就是將 block 內(nèi)的代碼封裝到__main_block_func_0函數(shù)灰粮,將__main_block_func_0函數(shù)地址傳遞給__main_block_impl_0

1.1.3 __main_block_desc_0

__main_block_impl_0函數(shù)的第二個(gè)參數(shù)是__main_block_desc_0忍坷,其定義如下:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

__main_block_desc_0中存儲(chǔ)著兩個(gè)成員粘舟,reservedBlock_size。并且為reserved賦值0佩研,為Block_size賦值sizeof(struct __main_block_impl_0)柑肴,即block的大小。

最終旬薯,將__main_block_desc_0結(jié)構(gòu)體傳給__main_block_impl_0中晰骑,賦值給desc。

1.1.4 age

__main_block_impl_0函數(shù)的第三個(gè)參數(shù)是age绊序,即定義的局部變量硕舆。

如果在 block 中使用了局部變量,block 聲明的時(shí)候會(huì)將 age 作為參數(shù)傳入政模,即 block 會(huì)捕獲(capture)age岗宣。如果 block 中沒有使用 age,則只會(huì)給__main_block_impl_0函數(shù)傳入__main_block_func_0__main_block_desc_0_DATA參數(shù)淋样。

由于 block 在聲明時(shí)捕獲了局部變量耗式,在聲明后、調(diào)用前修改局部變量值趁猴,不會(huì)影響 block 內(nèi)捕獲到的局部變量值刊咳。如下所示:

        int age = 10;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d", age);
        };
        age = 11;
        myblock();

執(zhí)行后,控制臺(tái)打印如下:

age: 10

block 在定義之后已將局部變量age值存儲(chǔ)在__main_block_impl_0結(jié)構(gòu)體儡司,調(diào)用時(shí)直接從結(jié)構(gòu)體中取出娱挨。聲明后修改局部變量的值不會(huì)影響__main_block_impl_0結(jié)構(gòu)體捕獲的值。

1.1.5 __block_impl

__main_block_impl_0結(jié)構(gòu)體第一個(gè)成員是__block_impl結(jié)構(gòu)體捕犬,__block_impl結(jié)構(gòu)體如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

isa指針指向類對(duì)象跷坝,FuncPtr指針存儲(chǔ)著__main_block_func_0函數(shù)地址,即 block 內(nèi)代碼地址碉碉。

__block_impl結(jié)構(gòu)體第一個(gè)成員就是 isa 指針柴钻。Objective-C對(duì)象本質(zhì)上也是結(jié)構(gòu)體,第一個(gè)成員也是 isa 指針垢粮。因此贴届,block 本質(zhì)上也是一個(gè) OC 對(duì)象。__main_block_impl_0函數(shù)的構(gòu)造函數(shù)將傳入 block 的值存儲(chǔ)到__main_block_impl_0結(jié)構(gòu)體中,最終將__main_block_impl_0結(jié)構(gòu)體地址賦值給myblock毫蚓。

分析__main_block_impl_0構(gòu)造函數(shù)占键,特點(diǎn)如下:

  • __main_block_func_0封裝了函數(shù)地址,其中先取出局部變量元潘,再調(diào)用 block 內(nèi)代碼畔乙。
  • __main_block_desc_0_DATA封裝 block 大小。
  • age是 block 捕獲的局部變量翩概。
  • __main_block_impl_0結(jié)構(gòu)體中的__block_impl結(jié)構(gòu)體包含了isa指針啸澡、FuncPtr。

1.2 調(diào)用 block

        // 調(diào)用block
        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);

將上述代碼中的強(qiáng)制轉(zhuǎn)換移除后氮帐,變?yōu)橄旅娴拇a:

        (myblock->FuncPtr)(myblock);

調(diào)用myblock就是通過(guò)myblock找到FuncPtr指針,然后進(jìn)行調(diào)用洛姑。

myblock是指向__main_block_impl_0結(jié)構(gòu)體的指針上沐,內(nèi)部并沒有FuncPtr指針,為什么這里可以直接訪問(wèn)楞艾?這是因?yàn)?code>__main_block_impl_0結(jié)構(gòu)體第一個(gè)成員是__block_impl参咙,而__block_impl也是一個(gè)結(jié)構(gòu)體,即__main_block_impl_0可以改為以下內(nèi)容:

struct __main_block_impl_0 {
  // 使用__block_impl直接替換
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
  
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

另一方面硫眯,__main_block_impl_0結(jié)構(gòu)體第一個(gè)成員是__block_impl蕴侧,__main_block_impl_0結(jié)構(gòu)體地址就是__block_impl的地址,這樣也可以查找到FuncPtr指針两入。

block 底層的數(shù)據(jù)結(jié)構(gòu)也可以使用下面圖片表示:

BlockLayout.png

2. 變量捕獲

除了包含可執(zhí)行代碼净宵,塊還具有捕獲塊以外值的能力。如果在一個(gè)方法內(nèi)聲明了一個(gè)塊裹纳,該塊可以獲取方法內(nèi)任何變量择葡,也就是可以捕獲局部變量。

BlockCapture.png

2.1 局部變量

2.1.1 auto變量

局部變量默認(rèn)是 automatic variable 類型剃氧,簡(jiǎn)寫為auto敏储,一般省略不寫。當(dāng)程序進(jìn)入朋鞍、離開局部變量作用域時(shí)已添,會(huì)自動(dòng)分配、釋放內(nèi)存滥酥。

auto會(huì)自動(dòng)捕獲到 block 內(nèi)更舞,__main_block_impl_0結(jié)構(gòu)體內(nèi)增加了存儲(chǔ)局部變量的成員。block 內(nèi)訪問(wèn)auto變量的方式是值傳遞恨狈,即直接將auto變量傳遞給__main_block_impl_0函數(shù)疏哗。

2.1.2 static變量

static變量會(huì)一直存儲(chǔ)在內(nèi)存中。block 會(huì)捕獲static修飾的局部變量,訪問(wèn)時(shí)使用指針訪問(wèn)返奉。

下面分別添加使用auto贝搁、static修飾的局部變量:

        auto int age = 10;
        static int weight = 125;
        void(^myblock)(void) = ^{
            NSLog(@"age: %d, weight: %d", age, weight);
        };
        myblock();

生成C++后如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 捕獲了age、weight芽偏。
  int age;
  int *weight;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *weight = __cself->weight; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_e2f202_mi_0, age, (*weight));
        }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        auto int age = 10;
        static int weight = 125;
        // age直接傳遞值雷逆,weight傳遞指針。
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));

        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
    }
    return 0;
}

可以看到污尉,__main_block_impl_0捕獲了age膀哲、weight,并且給__main_block_impl_0函數(shù)傳遞age時(shí)直接傳遞值被碗,傳遞weight時(shí)傳遞的是指針某宪。

2.2 全局變量

block 是否會(huì)捕獲全局變量?以及如何使用锐朴?

添加以下全局變量:

int height = 170;
static int number = 11;

生成C++代碼如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *weight;
  // 并沒有捕獲全局變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_weight, int flags=0) : age(_age), weight(_weight) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *weight = __cself->weight; // bound by copy

            // 直接使用height兴喂、number
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_964e22_mi_0, age, (*weight), height, number);
        }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        auto int age = 10;
        static int weight = 125;
        void(*myblock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &weight));

        ((void (*)(__block_impl *))((__block_impl *)myblock)->FuncPtr)((__block_impl *)myblock);
    }
    return 0;
}

可以看到__main_block_func_0并沒有添加任何全局變量,而是直接使用焚志。這是因?yàn)槿肿兞繒?huì)一直存放在內(nèi)存中衣迷,全局都可以使用。

3. Block 的類型

既然 block 也是 OC 對(duì)象酱酬,那么 block 是什么類型呢壶谒?

聲明一個(gè) block,并打印其父類膳沽,如下所示:

        void(^myblock)(void) = ^{
            NSLog(@"github.com/pro648");
        };
        NSLog(@"%@", [myblock class]);
        NSLog(@"%@", [[myblock class] superclass]);
        NSLog(@"%@", [[[myblock class] superclass] superclass]);
        NSLog(@"%@", [[[[myblock class] superclass] superclass] superclass]);

輸出如下:

__NSGlobalBlock__
NSBlock
NSObject
(null)

即 block 的繼承關(guān)系是:__NSGlobalBlock__NSBlockNSObject汗菜。進(jìn)一步證實(shí)了 block 本質(zhì)上也是一個(gè) OC 對(duì)象。

定義三個(gè)不同的 block挑社,分別打印其類型:

        // 沒有調(diào)用外部變量的block
        void(^myblock1)(void) = ^{
            NSLog(@"github.com/pro648");
        };
        
        // 訪問(wèn)auto變量
        int age = 10;
        void(^myblock2)(void) = ^{
            NSLog(@"age: %d", age);
        };
        
        // 直接調(diào)用block的 class
        NSLog(@"%@ %@ %@", [myblock1 class], [myblock2 class], [^{
            NSLog(@"%d", age);
        } class]);

打印如下:

__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

將上述代碼轉(zhuǎn)換為C++呵俏,可以看到三個(gè) block 類型都是_NSConcreteStackBlock類型。這可能是 runtime 運(yùn)行時(shí)進(jìn)行了某種轉(zhuǎn)換滔灶,使用 clang 生成的C++代碼僅供參考普碎,不能保證和運(yùn)行時(shí)完全一致。

三種類型的block在內(nèi)存中的位置如下:

BlockMemory.png

__NSGlobalBlock__录平、__NSStackBlock__麻车、__NSMallocBlock__三種類型的block是按照以下規(guī)則產(chǎn)生的:

block 類型 環(huán)境
__NSGlobalBlock__ 沒有訪問(wèn)auto變量
__NSStackBlock__ 訪問(wèn)了auto變量
__NSMallocBlock__ __NSStackBlock__調(diào)用了copy方法

3.1 __NSGlobalBlock__

當(dāng) block 內(nèi)沒有訪問(wèn)auto變量時(shí),block 為__NSGlobalBlock__類型斗这,__NSGlobalBlock__存在數(shù)據(jù)段中槽片,程序結(jié)束才會(huì)回收內(nèi)存感论。但因?yàn)槠渑c普通函數(shù)沒有區(qū)別拥褂,很少使用__NSGlobalBlock__類型的 block镇匀。

3.2 __NSStackBlock__

在 block 內(nèi)訪問(wèn)了auto變量為__NSStackBlock__類型。

__NSStackBlock__類型的 block 存放在棧中。棧的內(nèi)存由系統(tǒng)自動(dòng)分配和釋放彼水,超出變量作用域后自動(dòng)釋放崔拥。由于棧中代碼超出作用域之后,內(nèi)存就會(huì)被銷毀凤覆,而有可能內(nèi)存銷毀之后才去調(diào)用它链瓦,此時(shí)就會(huì)出現(xiàn)問(wèn)題。

ARC 自動(dòng)管理內(nèi)存時(shí)會(huì)幫助我們做很多事情盯桦,為了方便理解其本質(zhì)慈俯,先關(guān)閉 ARC 使用 MRC 管理內(nèi)存。進(jìn)入TARGETS > Build Settings > Objective-C Automatic Reference Counting拥峦,修改其值為 NO贴膘。

關(guān)閉 ARC 后,使用以下代碼驗(yàn)證問(wèn)題:

void (^myblock)(void);

void test() {
    // __NSStackBlock__
    int age = 10;
    myblock = ^{
        NSLog(@"age: %d", age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        myblock();
    }
    return 0;
}

執(zhí)行后控制臺(tái)輸出如下:

age: -272632840

這是因?yàn)閙yblock是在棧中的略号,即__NSStackBlock__類型的步鉴。當(dāng)test函數(shù)執(zhí)行完畢后,棧內(nèi)存中 block 已經(jīng)被系統(tǒng)回收璃哟。

3.3 __NSMallocBlock__

為了避免函數(shù)執(zhí)行完畢棧內(nèi)存立即被回收,可以將__NSStackBlock__block copy 到堆中喊递。以下是修改后的代碼:

void (^myblock)(void);

void test() {
    // __NSMallocBlock__
    int age = 10;
    // 將 block 從棧中復(fù)制到堆中随闪。
    myblock = [^{
        NSLog(@"age: %d", age);
    } copy];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        myblock();
    }
    return 0;
}

執(zhí)行后控制臺(tái)輸出如下:

age: 10

block 調(diào)用 copy 后,類型改變?nèi)缦滤荆?/p>

block類型 內(nèi)存區(qū)域 調(diào)用copy的效果
__NSGlobalBlock__ 數(shù)據(jù)段 什么都不做骚勘,類型不變铐伴。
__NSStackBlock__ 從棧復(fù)制到堆,類型變?yōu)?code>__NSMallocBlock__
__NSMallocBlock__ 引用計(jì)數(shù)加一俏讹,類型不變当宴。

使用 MRC 管理內(nèi)存時(shí),經(jīng)常需要使用 copy 保存 block泽疆,將棧上的 block 復(fù)制到堆上户矢,超出作用域時(shí) block 不會(huì)被釋放,后續(xù)需調(diào)用 release 銷毀 block殉疼。ARC 環(huán)境下梯浪,系統(tǒng)會(huì)自動(dòng)調(diào)用 copy 操作,使 block 不被銷毀瓢娜;不再使用時(shí)挂洛,自動(dòng)調(diào)用 release 引用計(jì)數(shù)減一。

4. ARC 在某些情況下會(huì)對(duì) block 自動(dòng)進(jìn)行一次 copy 操作眠砾,將其從棧區(qū)移動(dòng)到堆區(qū)

出現(xiàn)以下情況時(shí)虏劲,ARC 會(huì)自動(dòng)對(duì) block 執(zhí)行一次 copy 操作,將其從棧區(qū)移動(dòng)到堆區(qū):

  1. 當(dāng) block 作為函數(shù)返回值時(shí)。
  2. 當(dāng) block 被強(qiáng)指針引用時(shí)柒巫。
  3. 當(dāng) Cocoa API 方法名包含usingBlock励堡,且 block 作為參數(shù)時(shí),或 block 作為 GCD API 方法參數(shù)吻育。

4.1 當(dāng) block 作為函數(shù)返回值時(shí)

typedef void (^MyBlock)(void);

MyBlock test() {
    int age = 10;
    // myblock 作為函數(shù)返回值念秧,ARC 會(huì)自動(dòng)進(jìn)行copy。
    MyBlock myblock = ^{
        NSLog(@"age: %d", age);
    };
    return myblock;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myblock = test();
        NSLog(@"%@", [myblock class]);
    }
    return 0;
}

在 ARC 環(huán)境下布疼,參數(shù)返回值為 block 類型時(shí)摊趾,系統(tǒng)會(huì)對(duì) ARC 自動(dòng)執(zhí)行一次 copy 操作,使其變?yōu)?code>__NSMallocBlock__類型游两。在 MRC 環(huán)境下砾层,超出作用域后 block 會(huì)被銷毀,此時(shí)再調(diào)用會(huì)引起閃退贱案。

4.2 當(dāng) block 被強(qiáng)指針引用時(shí)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        MyBlock myblock = ^{
            NSLog(@"age: %d", age);
        };
        NSLog(@"%@",[myblock class]);
    }
    return 0;
}

由于 block 訪問(wèn)了auto變量肛炮,其是__NSStackBlock__類型。在 MRC 環(huán)境中宝踪,不會(huì)自動(dòng)進(jìn)行 copy 操作侨糟,輸出是__NSStackBlock__;在 ARC 環(huán)境中瘩燥,有強(qiáng)指針引用時(shí)會(huì)自動(dòng)執(zhí)行 copy 操作秕重,將 block 從棧中移動(dòng)到堆中。

修改上述代碼如下厉膀,即取消強(qiáng)指針對(duì) block 的引用:

        int age = 10;
        // 取消強(qiáng)指針的引用
        NSLog(@"%@",[^{
            NSLog(@"age: %d", age);
        } class]);

可以看到輸出為:

__NSStackBlock__

手動(dòng)調(diào)用 copy溶耘,如下所示:

        int age = 10;
        NSLog(@"%@", [[^{
            NSLog(@"age: %d", age);
        } copy] class]);

輸出為:

__NSMallocBlock__

這也進(jìn)一步證明了 ARC 環(huán)境下,有強(qiáng)指針引用 block 時(shí)會(huì)自動(dòng)調(diào)用 copy 方法服鹅。

4.3 當(dāng) Cocoa API 方法名包含usingBlock凳兵,且 block 作為參數(shù)時(shí),或 block 作為 GCD API 的方法參數(shù)

當(dāng) Cocoa API 方法名包含usingBlock企软,且 block 作為參數(shù)時(shí)庐扫,或 block 作為 GCD API 的方法參數(shù)。ARC 會(huì)根據(jù)情況自動(dòng)將棧上的 block copy到堆上仗哨。

        // Cocoa API
        NSArray *arr = @[@1];
        [arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // 這個(gè) block 在堆上
        }];

        // GCD API
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // 這個(gè) block 在堆上
        });

block 作為屬性時(shí)與其它屬性類似聚蝶,但 MRC 環(huán)境下,只能使用copy修飾藻治。因?yàn)榈饷悖琤lock 訪問(wèn)auto變量時(shí),block 是__NSStackBlock__類型桩卵,超出作用域 block 會(huì)被自動(dòng)銷毀验靡。如果想要在外部繼續(xù)訪問(wèn)倍宾、調(diào)用 block,就需要將 block 從棧中復(fù)制到堆中胜嗓,因此需用copy修飾高职。

在 ARC 環(huán)境下,系統(tǒng)會(huì)在需要時(shí)自動(dòng)進(jìn)行 copy 操作辞州。此時(shí)屬性可以使用strong怔锌,但copy更能表明用意。

5. Block 內(nèi)引用對(duì)象

之前 block 內(nèi)只引用過(guò)基本數(shù)據(jù)類型变过,這一部分介紹 block 內(nèi)引用對(duì)象類型埃元。如下所示:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            ^{
                NSLog(@"person.age = %d", person.age);
            }();
        }
        NSLog(@"--------");
    }
    return 0;
}

執(zhí)行后控制臺(tái)輸出如下:

person.age = 10
-[Person dealloc]
--------

可以看到在打印虛線前person已經(jīng)釋放。此時(shí)媚狰,block 是棧類型 block岛杀,即__NSStackBlock__。棧區(qū) block 即便引用了對(duì)象崭孤,也會(huì)在超出作用域時(shí)一起釋放类嗤。

更新上述代碼如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myblock;
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            myblock = ^{
                NSLog(@"person.age = %d", person.age);
            };
            myblock();
        }
        NSLog(@"--------");
    }
    return 0;
}

執(zhí)行后輸出如下:

person.age = 10
--------
-[Person dealloc]

可以看到執(zhí)行到虛線位置時(shí),person對(duì)象并沒有釋放辨宠。這是因?yàn)?block 內(nèi)部對(duì)person對(duì)象進(jìn)行了強(qiáng)引用遗锣,block 又被 myblock 強(qiáng)指針引用,即 block 是堆類型嗤形。堆類型的 block 會(huì)對(duì)外部對(duì)象強(qiáng)引用精偿。

使用以下命令生成 C++ 代碼,查看其底層實(shí)現(xiàn):

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m

查看 C++ 代碼派殷,block 定義如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_desc_0定義如下:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

與 block 內(nèi)引用基本數(shù)據(jù)類型相比,__main_block_desc_0內(nèi)增加了copydispose兩個(gè)參數(shù)墓阀,用于管理對(duì)象內(nèi)存毡惜。

copy操作調(diào)用的是__main_block_copy_0,如下所示:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->person,
                         (void*)src->person,
                         3/*BLOCK_FIELD_IS_OBJECT*/);
}

最終調(diào)用_Block_object_assign函數(shù)斯撮,_Block_object_assign會(huì)對(duì)引用的對(duì)象person進(jìn)行引用計(jì)數(shù)操作经伙。如果引用的對(duì)象是__strong修飾(默認(rèn)是__strong,即忽略時(shí)就是__strong)勿锅,則引用計(jì)數(shù)加一帕膜;如果使用的__weak修飾,則引用計(jì)數(shù)不變溢十。

當(dāng) block 執(zhí)行完畢垮刹,會(huì)調(diào)用dispose方法,dispose底層會(huì)調(diào)用以下方法:

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->person,
                          3/*BLOCK_FIELD_IS_OBJECT*/);
}

__main_block_dispose_0內(nèi)部會(huì)調(diào)用_Block_object_dispose方法张弛。如果之前 copy 時(shí)使用了強(qiáng)引用荒典,此時(shí)引用計(jì)數(shù)減一酪劫;如果之前使用了弱引用,直接取消對(duì)原來(lái)對(duì)象的弱引用寺董。

6. Block 內(nèi)修改外部變量

如果外部變量是auto類型覆糟,block 通過(guò)值傳遞的方式捕獲變量。由于是值傳遞的方式進(jìn)行的遮咖,其不能修改外部變量滩字。如果需要外部變量,可以通過(guò)以下兩種方式:

  • 使用 static 修飾外部變量御吞。
  • 使用__block修飾外部變量麦箍。

6.1 使用 static 修飾外部變量

使用 static 修飾的變量會(huì)一直存在內(nèi)存中,程序結(jié)束前不會(huì)被釋放魄藕。block 捕獲時(shí)通過(guò)引用方式進(jìn)行内列,即傳遞地址。因此背率,使用 static 修飾的外部變量可以直接修改值话瞧。

6.2 使用__block修飾外部變量

使用 static 修飾的變量會(huì)一直存放在內(nèi)存中,直到程序結(jié)束寝姿,這不利于性能優(yōu)化交排。

使用__block修飾外部變量,也可以達(dá)到在 block 內(nèi)修改成員變量的目的饵筑,那__block底層是如何實(shí)現(xiàn)的呢埃篓?

__block不能修飾全局變量、靜態(tài)變量根资。

下面代碼使用__block修飾局部變量:

        __block int age = 10;
        MyBlock myblock = ^{
            age = 20;
            NSLog(@"age: %d", age);
        };
        myblock();

使用以下命令將其轉(zhuǎn)換為 C++:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 main.m

轉(zhuǎn)換后的 block 定義如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // age 被封裝成了對(duì)象架专。
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到使用__block修飾的外部變量被封裝成了__Block_byref_age_0對(duì)象類型,__Block_byref_age_0聲明如下:

struct __Block_byref_age_0 {
    // 也有isa指針玄帕,即也是對(duì)象類型部脚。
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 // 值
 int age;
};

__Block_byref_age_0結(jié)構(gòu)體也有isa指針,即也是對(duì)象類型裤纹。

使用__block修飾的age被轉(zhuǎn)換為:

        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {
            (void*)0,(__Block_byref_age_0 *)&age,
            0,
            sizeof(__Block_byref_age_0),
            10
        };

__main_block_func_0函數(shù)被轉(zhuǎn)換為:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    
    // 使用age的forwarding指向age委刘。
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_05_pj1lwvjs50j3gx6vjtvxcvf80000gn_T_main_d88942_mi_0, (age->__forwarding->age));
}

使用age__forwarding取出變量地址,這樣即使 block 從棧移動(dòng)到了堆上鹰椒,也可以正確修改變量值锡移。

7. 對(duì)象類型的auto變量、__block變量

在 block 內(nèi)訪問(wèn)了使用auto漆际、__block修飾的對(duì)象類型的變量:

  • 如果 block 在棧上淆珊,將不會(huì)對(duì)變量產(chǎn)生強(qiáng)引用。

  • 如果 block 被拷貝到堆上

    • 會(huì)調(diào)用 block 內(nèi)部的copy函數(shù)奸汇。

    • copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù)套蒂。

    • _Block_object_assign函數(shù)會(huì)根據(jù)變量修飾符__strong钞支、__weak__unsafe_unretained做出相應(yīng)操作操刀,類似于 retain(形成強(qiáng)引用烁挟、弱引用)。

      使用__block修飾的變量只有在 ARC 環(huán)境中會(huì)根據(jù)__strong骨坑、__weak撼嗓、__unsafe_unretained修飾符進(jìn)行強(qiáng)引用,在 MRC 環(huán)境中不會(huì)進(jìn)行強(qiáng)引用欢唾。

  • 如果 block 從堆上移除:

    • 會(huì)調(diào)用 block 內(nèi)部的 dispose 函數(shù)且警。
    • dispose 函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù)。
    • _Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的變量礁遣,類似于 release斑芜。

8. Block 的循環(huán)引用

使用 block 容易產(chǎn)生循環(huán)引用。如果類中定義了一個(gè) block祟霍,在 block 內(nèi)又訪問(wèn)了類的屬性杏头,就會(huì)導(dǎo)致循環(huán)引用。

Person類中聲明了屬性age和 myblock沸呐,main.m文件中為 block 賦值醇王,如下所示:

typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) MyBlock myblock;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.myblock = ^{
            NSLog(@"age: %d", 20);
        };
    }
    NSLog(@"-------");
    return 0;
}

執(zhí)行后輸出如下:

-[Person dealloc]
-------

可以看到person先釋放,后打印虛線崭添。

更新myblock賦值語(yǔ)句如下:

        person.myblock = ^{
            NSLog(@"age: %d", person.age);
        };

再次執(zhí)行后寓娩,控制臺(tái)只輸出了虛線,person類沒有被釋放呼渣。

因?yàn)?code>person強(qiáng)引用了myblock棘伴,此時(shí)myblock在堆上;myblock內(nèi)訪問(wèn)了person對(duì)象屁置,堆上的 block 會(huì)對(duì)對(duì)象進(jìn)行強(qiáng)引用焊夸。此時(shí)person強(qiáng)引用myblockmyblock強(qiáng)引用person缰犁,形成了循環(huán)引用淳地。

不止訪問(wèn)person會(huì)產(chǎn)生循環(huán)引用怖糊,在person類里的 block 內(nèi)訪問(wèn)成員變量也會(huì)產(chǎn)生循環(huán)引用帅容,因?yàn)樵L問(wèn)成員變量本質(zhì)上是在調(diào)用self->instance,即仍然訪問(wèn)了self伍伤。

此外并徘,OC 方法轉(zhuǎn)換為 C 語(yǔ)言方法后,默認(rèn)帶有兩個(gè)參數(shù)扰魂。第一個(gè)是 id 類型的self麦乞,第二個(gè)參數(shù)是SEL類型的_cmd蕴茴,因此,平常訪問(wèn)的self也是局部變量姐直。

void test(id self, SEL _cmd) {
    
}

ARC 環(huán)境下有以下三種解決循環(huán)引用的方案:

  • 使用__weak修飾變量倦淀。
  • 使用__unsafe_unretained修飾變量。
  • 使用__block修飾變量声畏,同時(shí)在 block 內(nèi)將變量設(shè)置為nil撞叽,最后確保調(diào)用 block。

下面詳細(xì)介紹解決循環(huán)引用的方案插龄。

8.1 使用__weak修飾變量

使用__weak修飾變量愿棋,更新如下:

        __weak typeof(person) weakPerson = person;
        person.myblock = ^{
            NSLog(@"age: %d", weakPerson.age);
        };

將其轉(zhuǎn)換為 C++ 代碼,__main_block_impl_0函數(shù)如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 對(duì)捕獲的person進(jìn)行弱引用均牢。
  Person *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

此時(shí)執(zhí)行后糠雨,超出person作用域,person就會(huì)釋放徘跪。

8.2 使用__unsafe_unretained修飾變量

使用__unsafe_unretained修飾變量也可以解決循環(huán)引用問(wèn)題甘邀。

__unsafe_unretained__weak區(qū)別在于:

  • __weak:不會(huì)產(chǎn)生強(qiáng)引用。指向的對(duì)象銷毀時(shí)真椿,會(huì)自動(dòng)讓指針置為nil鹃答。
  • __unsafe_unretained:不會(huì)產(chǎn)生強(qiáng)引用,但沒有__weak安全突硝。指向?qū)ο箐N毀時(shí)测摔,指針存儲(chǔ)地址不變,但內(nèi)存已經(jīng)被回收解恰,再次訪問(wèn)時(shí)產(chǎn)生野指針錯(cuò)誤锋八。

8.3 使用__block修飾變量,同時(shí)在 block 內(nèi)將變量設(shè)置為nil护盈,最后確保調(diào)用 block

使用__block也可以解決循環(huán)引用問(wèn)題:

        // 1.添加__block修飾符
        __block Person *person = [[Person alloc] init];
        person.age = 10;
        person.myblock = ^{
            NSLog(@"age: %d", person.age);
            // 2.置為nil
            person = nil;
        };
        // 3.調(diào)用block()
        person.myblock();

使用__block解決循環(huán)引用問(wèn)題時(shí)挟纱,上述三步缺一不可。其缺點(diǎn)就是必須調(diào)用 block腐宋,如果沒有調(diào)用 block紊服,就無(wú)法在 block 執(zhí)行完畢后將person置為nil,就無(wú)法解決循環(huán)引用問(wèn)題胸竞。

在 MRC 環(huán)境中欺嗤,有以下兩種方案解決循環(huán)引用問(wèn)題:

  • 使用__unsafe_unretained,MRC 不支持弱指針__weak卫枝。
  • 直接使用__block煎饼。在 MRC 環(huán)境中,__block結(jié)構(gòu)體不會(huì)對(duì)結(jié)構(gòu)體內(nèi)對(duì)象進(jìn)行強(qiáng)引用校赤,不會(huì)產(chǎn)生循環(huán)引用吆玖。

參考資料:

  1. Cocoa blocks as strong pointers vs copy
  2. A look inside blocks: Episode 3 (Block_copy)
  3. How blocks are implemented (and the consequences)
  4. Objective-C Blocks Ins And Outs

歡迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/Block的本質(zhì).md

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筒溃,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子沾乘,更是在濱河造成了極大的恐慌怜奖,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翅阵,死亡現(xiàn)場(chǎng)離奇詭異烦周,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)怎顾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門读慎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人槐雾,你說(shuō)我怎么就攤上這事夭委。” “怎么了募强?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵株灸,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我擎值,道長(zhǎng)慌烧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任鸠儿,我火速辦了婚禮屹蚊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘进每。我一直安慰自己汹粤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布田晚。 她就那樣靜靜地躺著嘱兼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贤徒。 梳的紋絲不亂的頭發(fā)上芹壕,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音接奈,去河邊找鬼踢涌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鲫趁,可吹牛的內(nèi)容都是我干的斯嚎。 我是一名探鬼主播利虫,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼挨厚,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼堡僻!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起疫剃,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤钉疫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后巢价,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體牲阁,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年壤躲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了城菊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡碉克,死狀恐怖凌唬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情漏麦,我是刑警寧澤客税,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站撕贞,受9級(jí)特大地震影響更耻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捏膨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一秧均、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧号涯,春花似錦熬北、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至久又,卻和暖如春巫延,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背地消。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工炉峰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人脉执。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓疼阔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子婆廊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容