這本書作者幾乎通篇都在用 C、C++ 語言分析講解 Block 的實現(xiàn),初次看真的很吃力。這里推薦一篇文章:《Objective-C 高級編程》干貨三部曲(二):Blocks篇??偨Y(jié)的非常精簡到位。這篇博客作者有三篇文章分別是:
- 《Objective-C 高級編程》干貨三部曲(一):引用計數(shù)篇
- 《Objective-C 高級編程》干貨三部曲(二):Blocks篇
- 《Objective-C 高級編程》干貨三部曲(三):GCD篇

C 語言相關(guān)
Objective-C 轉(zhuǎn) C++ 的方法
Block 是帶有自動變量的匿名函數(shù),實質(zhì)上就是對 C 語言的擴充,最終通過支持 C 語言的編輯器,將目標代碼轉(zhuǎn)化為 C 語言代碼被編譯,可通過下面命令行轉(zhuǎn)化:
clang -rewrite-objc 源代碼文件名
例如:
- 創(chuàng)建 OC 源文件 block.m 并編輯代碼。
- 打開終端,cd 到 block.m 所在的文件夾。
- 輸入
clang -rewrite-objc block.m,就會在當前文件夾生成 block.cpp 文件。
C 語言幾種變量特點
C 語言函數(shù)可能使用的變量:
- 自動變量(局部變量)
- 函數(shù)的參數(shù)
- 靜態(tài)變量(局部靜態(tài)變量)
- 靜態(tài)全局變量
- 全局變量
其中,在函數(shù)的多次調(diào)用之間能夠傳遞值的變量有:
- 靜態(tài)變量(局部靜態(tài)變量)
- 靜態(tài)全局變量
- 全局變量
Block 實質(zhì)
先說結(jié)論:Block 即為 Objective-C 的對象。
Block 的構(gòu)成
以下是 block 語法的 BN 范式:
Blokc_literal_expression ::= ^block_decl compound_statement_body
block_decl ::=
block_decl ::= parameter_list
block_decl ::= type_expression
如下所示:
^ 返回值類型 參數(shù)列表 表達式
表層分析 Block 實質(zhì):它是一個類型
Block 是一種類型,一旦使用了 Block 就相當于生成了可以賦值給 Block 類型變量的類型。舉個例子:
int (^blk)(int) = ^(int count) {
return count + 1;
};
- 等號左側(cè)代碼定義這個 Block 類型:它接受一個 int 參數(shù),返回一個 int 值。
- 等號右側(cè)代碼表示這個 Block 的值:它是左側(cè)定義的 Block 的一種實現(xiàn)。
如果我們項目中經(jīng)常使用某個類型的 Block,可以通過 typedef 來抽象出這種類型的 Block,如下:
// 抽象成一種類型
typedef int (^AddOneBlock)(int count);
AddOneBlock block = ^(int count) {
// 具體實現(xiàn)代碼
return count + 1;
};
這樣一來,Block 的賦值和傳遞就和普通類型一樣方便了。
深層分析 Block 的實質(zhì):它是 Objective-C 對象
Block 其實就是 Objective-C 對象。它的結(jié)構(gòu)體內(nèi)含有 ISA 指針。
下面將 Objective-C 的代碼轉(zhuǎn)化為 C++ 來看下實現(xiàn),這里我把方法命名為 test:
#include <stdio.h>
int test()
{
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
C++ 核心代碼,這里并不是全部代碼,抽取了關(guān)鍵部分來說:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// block 結(jié)構(gòu)體
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// Block 被調(diào)用的代碼,也是Block被轉(zhuǎn)化為 C 語言的代碼
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
printf("Block\n");
}
// 方法的描述
static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
// 自己定義的 test 方法
int test()
{
void (*blk)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
先來看下 OC 中源代碼 block 語法 ^{printf("Block\n")}; 變化成 C++ 后:
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
printf("Block\n");
}
該函數(shù)的參數(shù) __cself 相當于 C++ 實例方法中指向自身實例方法的 this,或是 OC 實例方法中指向?qū)ο笞陨淼淖兞?self,即參數(shù) __cself 為指向 Block 值的變量,也是 __test_block_impl_0 結(jié)構(gòu)體的指針。
結(jié)合這個方法,可知 __test_block_impl_0 即為這個 Block 的結(jié)構(gòu)體。也就是說 blk 這個 Block就是通過 __test_block_impl_0 構(gòu)造出來的。
下面看下這個方法:
// block 結(jié)構(gòu)體
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
// 構(gòu)造函數(shù)
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,該結(jié)構(gòu)體由三部分組成:
第一個成員變量是 impl,其 __block_impl 結(jié)構(gòu)體聲明為:
struct __block_impl {
void *isa; // 指針
int Flags; // 標志
int Reserved; // 今后版本升級所需要的區(qū)域
void *FuncPtr; // 函數(shù)指針
};
第二個成員變量是 Desc 指針,用來描述這個 block 的附加信息,__test_block_desc_0 結(jié)構(gòu)體如下:
// 方法的描述
static struct __test_block_desc_0 {
size_t reserved; // 今后版本升級所需要的區(qū)域
size_t Block_size; // block 大小
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
第三部分是 __test_block_impl_0 結(jié)構(gòu)體的構(gòu)造函數(shù):
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
在這個結(jié)構(gòu)體的構(gòu)造函數(shù)里,ISA 指針保持著所屬類的結(jié)構(gòu)體的實例指針。__test_block_impl_0 結(jié)構(gòu)體相當于 Objective-C 類對象的結(jié)構(gòu)體,這里 _NSConcreteStackBlock 相當于 block 結(jié)構(gòu)體的實例。所以說 Block 的實質(zhì)即為 Objective-C 的對象。
Objective-C 類和對象的實質(zhì)
id 這一變量用于存儲 Objective-C 對象。源碼中面對 id 類型的聲明為:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
id 為 objc_object 結(jié)構(gòu)體的指針類型。我們再看下 Class:
/// usr/include/objc/objc.h
typedef struct objc_class *Class;
/// usr/include/objc/runtime.h
struct objc_class {
Class isa;
};
各類的結(jié)構(gòu)體就是基于 objc_class 的結(jié)構(gòu)體的 class_t 結(jié)構(gòu)體。class_t 在 objc4 運行時庫的 runtime/objc-runtime-new.h 中聲明如下:
typedef struct class_t {
struct class_t *isa;
struct class_t *superclass;
Cache cache;
IMP *vtable;
class_rw_t *data;
} class_t;
這里要說一下,objc4-437 還有這段代碼,但是最新的 objc4-756.2 中已經(jīng)沒有了。查看一個 NSObject 對象的 C++ 源碼,依然可以找到:
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
借用書中的一張圖,表達如下:

Block 截獲自動變量和對象
Block 截獲自動變量(局部變量)
使用 Block 時,不僅可以使用其內(nèi)部參數(shù),還能使用 Block 外部的局部或者全局變量。
當 Block 使用外部的局部變量時,這些變量就會被 Block 保存,即使在 Block 外部修改這些值,存在于 Block 內(nèi)部的這些變量也不會被修改,如下:
int a = 10;
int b = 20;
void (^printNumbers)(void) = ^{
printf("a = %d\nb = %d\n", a, b);
};
printNumbers();
// a = 10 b = 20
a = 55;
b = 66;
printNumbers();
// a = 10 b = 20
如果想要在 Block 內(nèi)部修改外部局部變量值,會編譯錯誤,提示添加 __block。如果操作外部局部變量,則不會有這問題。如:
NSMutableArray *array = [[NSMutableArray alloc] init];
void (^printNumbers)(void) = ^{
[array addObject:@1];
NSLog(@"array1 = %@\n", array);
};
printNumbers();
NSLog(@"array2 = %@\n", array);
[array addObject:@2];
printNumbers();
// 輸出:
array1 = [1]
array2 = [1]
arrar1 = [1, 2, 1]
如果 Block 內(nèi)部執(zhí)行 array = [NSMutableArray array]; 則會編譯錯誤,提示加上 __block。
根據(jù)以上例子,可以暫時總結(jié)三點:
- Block 可以截獲局部變量。
- 修改 Block 外部的局部變量,Block 內(nèi)部被截獲的值不會改變。
- 直接修改 Block 內(nèi)部的值,編譯錯誤。但是可以操作局部變量。
下面通過 C++ 代碼分析下為什么會出現(xiàn)這種現(xiàn)象, C 代碼:
#include <stdio.h>
int main() {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^ {
printf(fmt, val);
};
val = 2;
fmt = "Value changed. Val = %d\n";
blk(); // 輸出: val = 10
return 0;
}
通過 Clang 將其轉(zhuǎn)化為 C++ 代碼,主要代碼如下:
// block 結(jié)構(gòu)體
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/// block 函數(shù)體
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy
printf(fmt, val);
}
/// block 描述
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 方法
int main() {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
val = 2;
fmt = "Value changed. Val = %d\n";
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
這里加一點個人看法,前面說過,Block 是帶有自動變量的匿名函數(shù),我個人認為可以從上面 C++ 代碼來做驗證,Block 被轉(zhuǎn)化為 static void __main_block_func_0,即是一個函數(shù)。
單獨看 block 的構(gòu)成結(jié)構(gòu)體 __main_block_impl_0:
- 可以看到,在 block 內(nèi)部使用的局部變量 val、fmt 作為成員變量被追加到 __main_block_impl_0 結(jié)構(gòu)體中。而 block 中沒有使用的局部變量不會追加,如 dmy。
- 初始化 block 結(jié)構(gòu)體實例時,即
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));,截獲了局部變量 val 和 fmt 來初始化 block 的結(jié)構(gòu)體實例。
再看一下 block 函數(shù)體代碼:
/// block 函數(shù)體
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy
printf(fmt, val);
}
可知, val 、fmt 都是從 cself 截獲的,而 cself 是指向 block 的對象。所以也能說,val 和 fmt 是屬于 block 的。而且從 Clang 生成的注釋 bound by copy ,表示對這兩個對象做了只讀值拷貝,而不是指針傳遞。所以即使改變了 block 外部局部變量的值,block 內(nèi)部拷貝的值,也不會發(fā)生改變。
不過可以使用 __block 修飾局部變量,來滿足 block 內(nèi)部修改的需求。后面會對 __block 做詳細分析。
改變存儲于特殊存儲區(qū)域的變量
在 Block 內(nèi)部,下列幾種值是可以直接訪問和修改的:
- 全局變量
- 靜態(tài)全局變量
- 靜態(tài)變量
下面通過源碼來與自動變量對比來分析下:
#include <stdio.h>
int global_val = 1; // 全局變量
static int static_global_val = 2; // 全局靜態(tài)變量
int main() {
// 局部靜態(tài)變量
static int static_val = 3;
void (^blk)(void) = ^ {
printf("global_val = %d\nglobal_val = %d\nstatic_val = %d\n", global_val, static_global_val, static_val);
global_val = 11;
static_global_val = 12;
static_val = 13;
};
global_val = 21;
static_global_val = 22;
static_val = 23;
blk();
printf("global_val2 = %d\nglobal_val2 = %d\nstatic_val2 = %d\n", global_val, static_global_val, static_val);
return 0;
}
調(diào)用該方法,打印如下:
global_val = 21
global_val = 22
static_val = 23
global_val2 = 11
global_val2 = 12
static_val2 = 13
可知:
- 這三種類型的值,可以在 Block 內(nèi)部修改,并且內(nèi)外修改相互影響。
轉(zhuǎn)化成 C++ 代碼:
/// 定義變量
int global_val = 1;
static int static_global_val = 2;
/// block 結(jié)構(gòu)體
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_vtypedefal(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/// block 函數(shù)體
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
printf("global_val = %d\nglobal_val = %d\nstatic_val = %d\n", global_val, static_global_val, (*static_val));
global_val = 11;
static_global_val = 12;
(*static_val) = 13;
}
// block 描述
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() {
static int static_val = 3;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
global_val = 21;
static_global_val = 22;
static_val = 23;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
printf("global_val2 = %d\nglobal_val2 = %d\nstatic_val2 = %d\n", global_val, static_global_val, static_val);
return 0;
}
通過 __main_block_impl_0 結(jié)構(gòu)體可以看到,全局變量、靜態(tài)全局變量并沒被截獲到 block 內(nèi)部,而局部靜態(tài)變量 static_val 也是通過指針訪問。所以他們在 block 內(nèi)外的改變都是相互影響的,用的是同一份值。
Block 截獲對象
下面看一下 Block 語法中使用 Array 對象的代碼,Array已經(jīng)超過其作用域:
void testBlock() {
typedef void(^blk_t)(id obj);
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
調(diào)用 testBlock() 方法,控制臺打?。?/p>
ViewController.m:31 array count = 1
ViewController.m:31 array count = 2
ViewController.m:31 array count = 3
array 在超出其作用域后,并沒有被徹底銷毀,看一下其 C++ 代碼實現(xiàn):
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
id array; // 截獲的對象
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在 OC 中, C 語言的結(jié)構(gòu)體不能含有 __strong 修飾符的變量,因為編譯器不知道應(yīng)該何時進行 C 語言結(jié)構(gòu)體的初始化和廢棄操作,不能很好地管理內(nèi)存。
但是 OC 的運行時庫能夠準確的把握 Block 從棧復制到堆以及堆上的 Block 函數(shù)被廢棄的時機。在實現(xiàn)上是通過 __testBlock_block_copy_0 和 __testBlock_block_dispose_0 兩個函數(shù)進行的:
static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {
_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
其中,_Block_object_assign 函數(shù)相當于 retain 實例方法的函數(shù),將對象賦值在對象類型的結(jié)構(gòu)體成員變量中。 _Block_object_dispose 函數(shù)相當于調(diào)用 release 實例方法的函數(shù),釋放賦值在對象類型的結(jié)構(gòu)體成員變量中的對象。
這兩個函數(shù)在源碼中并沒有被調(diào)用,而是在 Block 從棧復制到堆上時以及堆上的 block 被廢棄時會調(diào)用這些函數(shù),如下表:調(diào)用 copy 函數(shù) 和 dispose 函數(shù)的時機
| 函數(shù) | 調(diào)用時機 |
|---|---|
| copy 函數(shù) | 棧上的 Block 復制到堆時 |
| dispose 函數(shù) | 堆上的 Block 被廢棄時 |
什么時候棧上的 Block 會復制到堆上呢?
- 調(diào)用 Block 的 copy 實例方法時
- Block 作為函數(shù)的返回值時
- 將 Block 賦值給附有 __strong 的修飾符 id 類型的類,或 Block 類型成員變量時
- 在方法名中含有 usingBlock 的 Cocoa 框架方法或者 GCD 的 API 中傳遞 Block 時
上面四種情況,棧上的 Block 都會復制到堆上,但其實本質(zhì)都可以歸結(jié)為 block_copy 函數(shù)被調(diào)用時 Block 從棧復制到堆。
特殊說明:針對如下修改,書中說程序會強制結(jié)束:
void testBlock() {
typedef void(^blk_t)(id obj);
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
但是我測試時,調(diào)用 testBlock 函數(shù)依然輸出:
ViewController.m:34 array count = 1
ViewController.m:34 array count = 2
ViewController.m:34 array count = 3
不確定是 Apple 之后做了修改,還是我不應(yīng)該借用 OC 的機制這么寫。
另外, __testBlock_block_copy_0 函數(shù)是否存在,與是否調(diào)用了 copy 實例方法并沒有關(guān)系。
相對的,在釋放復制到堆上的 Block 后,誰都不持有 Block 而被廢棄時調(diào)用 dispose 函數(shù),相當于對象的 dealloc 方法。比如在使用了 __weak 關(guān)鍵字時:
void testBlock() {
typedef void(^blk_t)(id obj);
blk_t blk;
{
id array0 = [[NSMutableArray alloc] init];
id __weak array = array0;
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
輸出:
ViewController.m:34 array count = 0
ViewController.m:34 array count = 0
ViewController.m:34 array count = 0
這段代碼無法通過 Clang 轉(zhuǎn)化,會報錯:
cannot create __weak reference in file using manual reference counting
__block 的實現(xiàn)原理
__block 修飾局部變量
__block 說明符 更精確的表達方式應(yīng)該是 __block 存儲域類說明符(__block storage-class-specifier)。C 語言有如下存儲域類說明符:
- typedef
- extern
- static
- auto
- register
__block 說明符類似 static、auto和 register 說明符,用于指定將變量值放到哪個存儲域中。例如,auto 表示作為自動變量存儲在棧中,static 表示作為靜態(tài)變量存儲在數(shù)據(jù)區(qū)中。給自動變量加上 __block 說明符后,就會改變這個自動變量的存儲區(qū)域。
下面通過給局部變量添加 __block 關(guān)鍵字后效果來分析:
typedef void(^PrintNumbers)(void);
void lineBlock() {
__block int a = 10;
int b = 20;
PrintNumbers printBlock = ^() {
a -= 10;
printf("a1 = %d, b1 = %d\n", a, b);
};
printBlock();
a += 20;
b += 20;
printf("a2 = %d, b2 = %d\n", a, b);
printBlock();
}
輸出:
a1 = 0, b1 = 20
a2 = 20, b2 = 40
a1 = 10, b1 = 20
附有 __block 修飾符的變量稱之為 __block 變量,可以看到,__block 變量在 Block 內(nèi)部可以被修改,且修改的值被同步到 Block 作用域外。
下面用 Clang 工具看一下 C++ 代碼:
OC 代碼:
void lineBlock() {
__block int valNumber= 10;
void (^blk)(void) = ^{
valNumber = 1;
};
}
C++ 代碼:
struct __Block_byref_valNumber_0 {
void *__isa;
__Block_byref_valNumber_0 *__forwarding;
int __flags;
int __size;
int valNumber;
};
struct __lineBlock_block_impl_0 {
struct __block_impl impl;
struct __lineBlock_block_desc_0* Desc;
__Block_byref_valNumber_0 *valNumber; // by ref
__lineBlock_block_impl_0(void *fp, struct __lineBlock_block_desc_0 *desc, __Block_byref_valNumber_0 *_valNumber, int flags=0) : valNumber(_valNumber->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __lineBlock_block_func_0(struct __lineBlock_block_impl_0 *__cself) {
__Block_byref_valNumber_0 *valNumber = __cself->valNumber; // bound by ref
(valNumber->__forwarding->valNumber) = 1;
}
static void __lineBlock_block_copy_0(struct __lineBlock_block_impl_0*dst, struct __lineBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->valNumber, (void*)src->valNumber, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __lineBlock_block_dispose_0(struct __lineBlock_block_impl_0*src) {_Block_object_dispose((void*)src->valNumber, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __lineBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __lineBlock_block_impl_0*, struct __lineBlock_block_impl_0*);
void (*dispose)(struct __lineBlock_block_impl_0*);
} __lineBlock_block_desc_0_DATA = { 0, sizeof(struct __lineBlock_block_impl_0), __lineBlock_block_copy_0, __lineBlock_block_dispose_0};
void lineBlock() {
__attribute__((__blocks__(byref))) __Block_byref_valNumber_0 valNumber = {(void*)0,(__Block_byref_valNumber_0 *)&valNumber, 0, sizeof(__Block_byref_valNumber_0), 10};
void (*blk)(void) = ((void (*)())&__lineBlock_block_impl_0((void *)__lineBlock_block_func_0, &__lineBlock_block_desc_0_DATA, (__Block_byref_valNumber_0 *)&valNumber, 570425344));
}
可以看到:
- 被 __block 修飾的自動變量,被轉(zhuǎn)變?yōu)?__Block_byref_valNumber_0 結(jié)構(gòu)體存在,即棧上生成 __Block_byref_valNumber_0 結(jié)構(gòu)體實例。
- block 的構(gòu)造結(jié)構(gòu)體
__lineBlock_block_impl_0中可以看到,通過 __block 變量的結(jié)構(gòu)體指針訪問該變量。 - block 函數(shù)實現(xiàn)方法
__lineBlock_block_func_0中看到,block 內(nèi)部通過valNumber->__forwarding->valNumbe指針修改 __block 變量。
關(guān)于 __block 變量的結(jié)構(gòu)體實現(xiàn):
struct __Block_byref_valNumber_0 {
void *__isa;
__Block_byref_valNumber_0 *__forwarding;
int __flags;
int __size;
int valNumber;
};
- 結(jié)構(gòu)體的成員變量 valNumber 持有原本變量的字面值。
- __forwarding: 結(jié)構(gòu)體實例的成員變量 __forwarding 持有指向該實例自身的指針,可以通過成員變量 __forwarding 訪問成員變量 valNumber,即
valNumber->__forwarding->valNumbe。如下圖:

而 __Block_byref_valNumber_0 結(jié)構(gòu)體之所以不在 __lineBlock_block_impl_0 結(jié)構(gòu)體內(nèi)部定義,是為了在多個 block 中共用一個 __block 變量結(jié)構(gòu)體。
另外,當 Block 從棧復制到堆時,被 Block 持有的 __block 變量也會復制到堆上,注意,這里會增加 __block 變量的引用計數(shù)。同時,如果 Block 被廢棄,持有的 __block 變量也會被釋放。
不管 __block 變量配置在棧上還是堆上,都能夠正確的訪問該變量。通過下面這個圖可以更深刻的理解這句話:

__block 修飾對象
__block 說明符可以用來指定任何類型的自動變量,下面用來指定 id 類型的自動變量:
void testBlock() {
__block id obj = [[NSObject alloc] init];
void (^blk)(void) = ^{
obj = [NSObject new];
};
}
ARC 有效時,id 類型以及對象類型變量必定附加所有權(quán)修飾符,缺省為附有 __strong 修飾符的變量。改代碼通過 clang 轉(zhuǎn)換。 __block 變量的結(jié)構(gòu)體如下:
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
id obj;
};
被 __strong 修飾的 id 類型或?qū)ο箢愋?,生成?copy 和 dispose 兩個方法:
static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
- 如果 __block 對象變量從棧復制到堆,使用 _Block_object_assign 方法。
- 當堆上的 __block 對象變量被廢棄時,使用 _Block_object_dispose 方法。
另外,__weak 與 __block 同時使用,clang 時報錯 cannot create __weak reference because the current deployment target does not support weak references。
說明不支持 __weak。
Block 存儲域,即三種 Block
通過前面說明可知, Block 轉(zhuǎn)換為 Block 結(jié)構(gòu)體類型的自動變量, __block 變量轉(zhuǎn)換為 __block 變量的結(jié)構(gòu)體類型的自動變量。而所謂 結(jié)構(gòu)體類型的自動變量,即棧上生成的該結(jié)構(gòu)體的實例。
Block 與 __block 變量的實質(zhì):
| 名稱 | 實質(zhì) |
|---|---|
| Block | 棧上 Block 的結(jié)構(gòu)體實例 |
| __block變量 | 棧上 __block 變量的結(jié)構(gòu)體實例 |
而 Block 根據(jù)存儲域的不同,可以歸結(jié)為三類:
| Block 的類 | 存儲域 | 拷貝效果 |
|---|---|---|
| _NSConcreteStackBlock | 棧 | 從??截惖蕉?/td> |
| _NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域(.data) | 什么也不做 |
| _NSConcreteMallocBlock | 堆 | 引用計數(shù)增加 |
全局 Block: _NSConcreteGlobalBlock
因為全局 Block 的結(jié)構(gòu)體實例設(shè)置在程序的數(shù)據(jù)存儲區(qū),可以在程序任意位置通知指針來訪問,只有兩種情況下會產(chǎn)生全局 Block:
- 記述全局變量的地方有 Block 語法時。
- Block 語法的表達式中不截獲自動變量時。
關(guān)于情況一,如下:
#include <stdio.h>
void(^blk_t)(void) = ^{
printf("Global Block.\n");
};
void test() {
blk_t();
}
通過 Clang 分析代碼,獲取 Block 的結(jié)構(gòu)體實例如下,可知確實是全局 Block:
struct __blk_t_block_impl_0 {
struct __block_impl impl;
struct __blk_t_block_desc_0* Desc;
__blk_t_block_impl_0(void *fp, struct __blk_t_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
特別提示: 第二種情況我按照書中測試,即:
#include <stdio.h>
typedef int (^BlockNum) (int);
void test() {
for(int rate = 0; rate < 5; ++rate) {
BlockNum num = ^(int count) {
return count;
};
}
}
該 Block 的結(jié)構(gòu)體如下
typedef int (*BlockNum) (int);
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
依然被認為存儲在棧上,這里暫時不確定原因。
棧 Block:_NSConcreteStackBlock
在生成 Block 后,如果這個 Block 不是全局 Block,那么它就是棧 Block,即 _NSConcreteStackBlock 對象。
配置在全局變量上的 Block,從變量作用域外也可通過指針安全的使用。但設(shè)置在棧上的 Block,如果其所屬變量作用域結(jié)束,該 Block 就被廢棄。由于 __block 變量也配置在棧上,同樣,其所屬的變量作用域結(jié)束,則該 __block 變量也被廢棄。
Blocks 機制提供了將 Block 和 __block 變量從棧上復制到堆上的方法來解決這個問題。將配置在棧上的 Block 復制到堆上,這樣即使 Block 語法記述的變量作用域結(jié)束,堆上的 Block 還可以繼續(xù)存在。
堆 Block:_NSConcreteMallocBlock
將棧 Block 復制到堆后,block結(jié)構(gòu)體的 ISA 成員變量變成了 _NSConcreteMallocBlock。
在對 Block 使用 copy 實例方法時:
| Block 的類 | 副本源的配置存儲域 | 復制(copy)效果 |
|---|---|---|
| _NSConcreteStackBlock | 棧 | 從棧拷貝到堆 |
| _NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)域 | 什么也不做 |
| _NSConcreteMallocBlock | 堆 | 引用計數(shù)增加 |
不管 Block 配置之在何處,用 copy 方法都不會引起任何問題,在不確定時調(diào)用 copy 方法即可。
在 ARC 中不能顯示調(diào)用 release,多次調(diào)用 copy 方法依然沒有問題,如下:
typedef int (^BlockNum) (int);
void test(int rate) {
BlockNum num = ^(int count) { return count * rate;};
num = [[[[num copy] copy] copy] copy];
}
該源代碼可以解釋為:
{
BlockNum tmp = [num copy];
num = tmp;
}
{
BlockNum tmp = [num copy];
num = tmp;
}
{
BlockNum tmp = [num copy];
num = tmp;
}
{
BlockNum tmp = [num copy];
num = tmp;
}
內(nèi)存管理并不會存在問題。
大多數(shù)情況下,編譯器會自行判斷將 Block 從棧上復制到堆上:
- block 作為函數(shù)返回值的時候。
- 部分情況下向方法或函數(shù)中傳遞 Block 時。部分情況主要是下面兩種情況:
- Cocoa 框架的方法且方法名中含有 usingBlock 時。
- GCD 中的 API。
除了以上兩種情況,我們基本是都要手動 copy Block,才能將其從棧復制到堆。
備注,前文也提到過 Block 從棧復制到堆的情況,那里多了個將 Block 賦值給 __strong 類型的對象,這里卻沒有說,暫時沒有找到驗證的方式。
那么 __block 變量在 Block 執(zhí)行 copy 操作后會發(fā)生什么呢?
- 任何一個 Block 被復制到堆上時,使用的 __block 變量也會復制到堆上,并被該 Block 持有。
- 如果接著有其他 Block 復制到堆上,被復制的 Block 也持有 __block 變量,則會增加 __block 變量的引用計數(shù)。反過來如果堆上 Block 被廢棄,它所持有的 __block 變量也會被釋放。
Block 循環(huán)引用
如果在 Block 內(nèi)部使用附有 __strong 修飾符的對象類型自動變量,那么當 Block 從棧復制到堆時,該對象為 Block 所持有。這樣容易引起循環(huán)引用。如下:
typedef void(^blk_t)(void);
@interface Person : NSObject
{
blk_t blk_;
}
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"self = %@",self);
};
return self;
}
@end
Block blk_t持有self,而self也同時持有作為成員變量的blk_t,導致循環(huán)引用,MyObject 的對象實例會無法釋放,造成內(nèi)存泄漏。
另外,編譯器有時候能夠及時的查處循環(huán)引用,進行編譯警告 Capturing 'self' strongly in this block is likely to lead to a retain cycle 。
__weak 修飾符
為避免循環(huán)引用,可聲明附有 __weak 修飾符的變量,并將 self 賦值使用:
- (instancetype)init
{
self = [super init];
if (self) {
id __weak tmp = self;
blk_ = ^ {
NSLog(@"self = %@", tmp);
};
}
return self;
}
在低于 iOS4 時,使用 __unsafe_unretained 修飾符代替 __weak。
另外,以下源代碼中 Block 中沒有 self 但是同樣截獲了 self,引起循環(huán)引用:
typedef void(^blk_t) (void);
@interface MyObject : NSObject
{
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (instancetype)init
{
self = [super init];
if (self) {
blk_ = ^ {
NSLog(@"obj_ = %@", obj_);
};
}
return self;
}
@end
Block 語法內(nèi)部使用的 obj_ 實際上截獲了 self。對編譯器來說,obj_ 只是對象用結(jié)構(gòu)體的成員變量,即:
blk_ = ^ {
NSLog(@"obj_ = %@", self->obj_);
};
這里說個容易出錯的情況,如果某個屬性用 weak 關(guān)鍵字呢?
@interface Person()
@property (nonatomic, weak) NSArray *array;
@end
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"array = %@",_array);//循環(huán)引用警告
};
return self;
}
這里依然會有循環(huán)引用的警告,因為循環(huán)引用是 self 和 block 之間的事,和被這個 Block 持有的成員變量時strong、weak都沒有關(guān)系,即使是基本數(shù)據(jù)類型 assgin 一樣會有警告。
__block 修飾符
- (instancetype)init
{
self = [super init];
__block id temp = self;//temp持有self
//self持有blk_
blk_ = ^{
NSLog(@"self = %@",temp);//blk_持有temp
temp = nil;
};
return self;
}
- (void)execBlc
{
blk_();
}
如果不調(diào)用 execBlc 實例方法,即不給成員變量 blk_ 賦值,便會循環(huán)引用并造成內(nèi)存泄漏。
使用 __block 避免循環(huán)引用的優(yōu)點:
- 通過 __block 變量可控制對象的持有時間。
- 為避免循環(huán)引用必須執(zhí)行 Block,否則循環(huán)引用一直存在。
說一下,使用 __block 來避免循環(huán)引用,感覺并不方便。不過也可以更具實際情況去具體選擇。
參考文檔
后記
《Objective-C高級編程》三篇總結(jié)之一:引用計數(shù)篇