本文的內(nèi)容主要是基于Clang編譯器的官方文檔所寫。
在開始探索Block的本質(zhì)之前,大家先試著分析一下,下面的代碼會(huì)輸出什么:
void main() {
__block int a = 13;
int b = 13;
NSMutableString *str = [[NSMutableString alloc] initWithString:@"Hello"];
void(^blockTest)(void) = ^{
NSLog(@"a = %d, b = %d, str = %@", a, b, str);
};
a++;
b++;
[str appendString:@"World"];
blockTest();
}
如果你對(duì)輸出結(jié)果不是那么有把握的話,那么相信通過(guò)今天的這篇文章,你會(huì)有一個(gè)明確的答案(答案在文章最后)。
Clang
先說(shuō)些題外話,什么是Clang?Clang是C++編寫的編譯器。我們知道,我們平常代碼所寫的任何程序,最終都需要通過(guò)編譯器轉(zhuǎn)換成與語(yǔ)言無(wú)關(guān)的機(jī)器二進(jìn)制代碼。而Clang,則是支持/C++/Objective-C/Objective-C++的編譯器。那我們?cè)谧鯫C開發(fā)時(shí),可能也會(huì)聽說(shuō)LLVM編譯器,那么Clang和LLVM之間是什么關(guān)系呢?
它們的關(guān)系如下圖所示:
Clang是編譯器的前端,它會(huì)分析具體的編程語(yǔ)言,然后用于生成與機(jī)器無(wú)關(guān)的中間代碼。而LLVM是編譯器的后端,與具體編程語(yǔ)言無(wú)關(guān),而是會(huì)去分析統(tǒng)一的中間代碼,生成符合對(duì)應(yīng)機(jī)器的目標(biāo)程序。
這樣拆分前端后端的好處在于,前后端可以獨(dú)立的替換,便于編譯器的優(yōu)化。
關(guān)于Clang,我們了解這些就足夠了。
Block的本質(zhì)
回到Block上來(lái)。我們?cè)谑褂肂lock語(yǔ)法時(shí),總會(huì)感覺到有些奇怪:
^{
NSLog(@"Hello");
};
這么一個(gè)^{}是什么鬼?似乎在別的語(yǔ)言中也沒(méi)有見過(guò)這么個(gè)關(guān)鍵字定義。其實(shí),^{}對(duì)于Clang編譯器來(lái)說(shuō),僅僅是一個(gè)語(yǔ)言標(biāo)記,它會(huì)告訴Clang,這里我需要定義一個(gè)Block類型的結(jié)構(gòu)體。 而Clang發(fā)現(xiàn)這個(gè)語(yǔ)言標(biāo)記時(shí),會(huì)將^{}這么一個(gè)奇怪的定義,轉(zhuǎn)換為C語(yǔ)言中的結(jié)構(gòu)體。經(jīng)過(guò)Clang轉(zhuǎn)換后的Block,其形式是這樣的:
struct Block_literal_1 {
// 第一部分. Block基本信息以及 invoke函數(shù)指針
void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__
int flags;
int reserved;
void (*invoke)(void *, ...);
// 第二部分. Block descriptor指針
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
const char *signature; // IFF (1<<30)
} *descriptor;
// 第三部分. Block所截取的外部變量(如果有的話)
// imported variables
};
筆者將Block結(jié)構(gòu)體定義分成了三個(gè)部分:
- Block基本信息以及 invoke函數(shù)指針
- Block descriptor指針
- Block所截取的外部變量
在這里我們得出結(jié)論:Block的本質(zhì)是一個(gè)C語(yǔ)言的struct。
Block對(duì)應(yīng)的結(jié)構(gòu)體
上面探討了Block的本質(zhì)是一個(gè)struct,接下來(lái)我們就來(lái)詳細(xì)看一下這個(gè) Block struct的定義。
Block基本信息以及Block descriptor
struct Block_literal_1 {
// 第一部分. Block基本信息以及 invoke函數(shù)指針
void *isa; // initialized to &__NSGlobalBlock__ or &__NSMallocBlock__ or &__NSStackBlock__
int flags;
int reserved;
void (*invoke)(void *, ...);
// 第二部分. Block descriptor指針
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src); // IFF (1<<25)
void (*dispose_helper)(void *src); // IFF (1<<25)
// required ABI.2010.3.16
const char *signature; // IFF (1<<30)
} *descriptor;
...
};
我們先來(lái)看Block struct的第一部分和第二部分。至于Block的第三部分,外部變量的截取,我們會(huì)在下面單獨(dú)的章節(jié)進(jìn)行討論。
當(dāng)我們聲明一個(gè)Block時(shí),對(duì)應(yīng)的Block struct會(huì)被如下初始化:
系統(tǒng)會(huì)聲明并初始化一個(gè)Block descriptor結(jié)構(gòu)體。初始化Block descriptor步驟如下
a. Block descriptor 的size部分會(huì)被設(shè)置為Block結(jié)構(gòu)體的大小
b. copy_helper 和 dispose_helper函數(shù)指針會(huì)被設(shè)置為對(duì)應(yīng)的函數(shù)指針(如果需要這兩個(gè)helper 函數(shù)的話)系統(tǒng)初始化Block 結(jié)構(gòu)體。 初始化Block 結(jié)構(gòu)體的步驟如下:
a. isa 部分會(huì)被設(shè)置為__NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__所對(duì)應(yīng)的地址。注意這里是地址,而不是NSMallocBlock這些變量。
b. flags 會(huì)被置為對(duì)應(yīng)的flag數(shù)值。比如,如果Block struct需要copy,dispose helper函數(shù)時(shí),響應(yīng)的flag會(huì)被置位。同時(shí),flags還有標(biāo)志Block ABI 版本的功能。
c. 設(shè)置invoke函數(shù)指針指向?qū)?yīng)的函數(shù)。該函數(shù)的第一個(gè)參數(shù)是Block struct本身的指針,而其余的參數(shù)則是Block執(zhí)行時(shí)外部要傳入的參數(shù)(如果有的話)
舉個(gè)例子,對(duì)于下面的Block:
^ { printf("hello world\n"); }
Clang會(huì)創(chuàng)建如下內(nèi)容:
struct __block_literal_1 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_1 *);
struct __block_descriptor_1 *descriptor;
};
void __block_invoke_1(struct __block_literal_1 *_block) {
printf("hello world\n");
}
static struct __block_descriptor_1 {
unsigned long int reserved;
unsigned long int Block_size;
} __block_descriptor_1 = { 0, sizeof(struct __block_literal_1) };
那么Block struct將會(huì)如下被初始化:
struct __block_literal_1 _block_literal = {
&__NSGlobalBlock__,
(1<<29), <uninitialized>,
__block_invoke_1,
&__block_descriptor_1
};
這是Clang文檔給出的官方例子,但是我們這里不要去糾結(jié)flags究竟是設(shè)置的什么,因?yàn)楦鶕?jù)本人的測(cè)試,其flags的值并不是1<<29。
這里有個(gè)問(wèn)題,就是什么時(shí)候isa會(huì)被設(shè)為&__NSGlobalBlock__/&__NSMallocBlock__/&__NSStackBlock__ 呢?
- 當(dāng)Block中沒(méi)有引用外部變量,或引用了全局變量,const 標(biāo)量或static變量時(shí),Block的isa會(huì)被設(shè)置為
&__NSGlobalBlock__。 這時(shí)的Block生命周期是伴隨程序始終的。 -
&__NSStackBlock__表示這個(gè)block, 是在棧上面分配的,出了棧就會(huì)消亡。使用了外部棧變量,就會(huì)是__NSStackBlock__類型。 -
&__NSMallocBlock__表示Block復(fù)制到堆上面了,可以存儲(chǔ)下來(lái),以后使用。當(dāng)Block引用了外部的OC對(duì)象,Block對(duì)象或用__block修飾的變量時(shí),Block會(huì)被設(shè)置為&__NSMallocBlock__類型。這里有一點(diǎn)要注意,在ARC的情況下。只要將block賦值給變量,就自動(dòng)幫你復(fù)制了。也就是說(shuō),如果將一個(gè)棧上的block賦值給另一個(gè)block變量,則被賦值的block變量類型是 &__NSMallocBlock__ 類型。
如下面代碼:
int a = 13;
NSLog(@"block type is %@", NSStringFromClass([^{NSLog(@"%d", a);} class]));
blockType1 blk2 = ^{
NSLog(@"%d", a);
};
NSLog(@"block type is %@", NSStringFromClass([blk2 class]));
輸出為:

而對(duì)于const類型的引用,
const int a = 13; // 這里是const引用
NSLog(@"block type is %@", NSStringFromClass([^{NSLog(@"%d", a);} class]));
blockType1 blk2 = ^{
NSLog(@"%d", a);
};
NSLog(@"block type is %@", NSStringFromClass([blk2 class]));
輸出為:

這是因?yàn)閷?duì)于Global,不必需要再在堆上開辟一塊內(nèi)存。
Block的外部變量截取
理解Block的關(guān)鍵,在于理解Block是如何處理外部變量的。
我們先來(lái)想一想,Block中會(huì)截取那些類型的外部變量:
- 全局/靜態(tài)變量
- 自動(dòng)(auto)存儲(chǔ)類型
- Block類型
- NSObject類型
- __block修飾的變量
截取全局/靜態(tài)類型變量
對(duì)于全局/靜態(tài)變量,Block會(huì)直接引用這類變量,不會(huì)copy。 例如,
static int a = 13;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"Outside Block, static int a address is %p", &a);
^{
NSLog(@"Inside Block, static int a address is %p", &a);
}();
}
輸出為:

在Block 外和Block內(nèi),static int a的地址是一樣的,Block并沒(méi)有做特殊的處理。
截取自動(dòng)存儲(chǔ)類型變量
所謂自動(dòng)存儲(chǔ)類型,指的是auto類型。我們可以理解為棧上的變量(Block類型、__block、NSObject類型除外),其內(nèi)存會(huì)有系統(tǒng)自動(dòng)釋放。
對(duì)于auto類型的變量截取,Clang文檔有如下描述:
Variables of auto storage class are imported as const copies.
也就是說(shuō),auto類型會(huì)在Block中用const copy一份。也就是說(shuō)Block內(nèi)、外是完全不同的兩個(gè)變量。
我們做個(gè)測(cè)試:
int b = 12;
NSLog(@"Outside Block, address of int b is %p", &b);
^{
NSLog(@"Inside Block, address of int b is %p", &b);
}();
輸出為:

可以看到,在Block外和Block內(nèi)部,表面上同樣的b變量,其地址是不一樣的。究其原因,就是因?yàn)樵贐lock內(nèi)部,系統(tǒng)會(huì)默默的const copy一份b。
這時(shí)候,Block的數(shù)據(jù)結(jié)構(gòu)是這樣的:
int x = 10;
void (^vv)(void) = ^{ printf("x is %d\n", x); }
x = 11;
vv();
struct __block_literal_2 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_2 *);
struct __block_descriptor_2 *descriptor;
const int x; // 這里會(huì)有一份const copy
};
struct __block_literal_2 __block_literal_2 = {
&_NSConcreteStackBlock,
(1<<29), <uninitialized>,
__block_invoke_2,
&__block_descriptor_2,
x
};
一般的,對(duì)于標(biāo)量類型(int, float, bool等基本類型),struct,unions和函數(shù)指針類型,都會(huì)采用const copy的方式,將Block外部的變量拷貝到Block內(nèi)部。
這里需要注意一點(diǎn),在iOS系統(tǒng)中,當(dāng)我們把一個(gè)stack 上的Block賦值給一個(gè)Block變量時(shí):
void (^vv)(void) = ^{ printf("x is %d\n", x); }
會(huì)默認(rèn)調(diào)用Block的copy方法,即,上面實(shí)際上是如下代碼:
void (^vv)(void) = [^{ printf("x is %d\n", x); } copy];
這樣得到的vv,是一個(gè)在堆上的Block變量。這時(shí)候再輸出vv中x的地址,會(huì)得到一個(gè)堆上的地址。
因此,我們?cè)谧鰧?shí)驗(yàn)的時(shí)候,不要輸出對(duì)拷貝后的Block中變量地址,而應(yīng)該直接輸出Block中的地址:
^{
NSLog(@"Inside Block, static int a address is %p", &a);
}();
上面代碼中并沒(méi)有賦值,因此會(huì)輸出棧上的a的const copy地址。
截取Block類型變量
對(duì)于截取Block類型的變量,在Block內(nèi)部,會(huì)保留const copy其Block指針。
如下代碼:
int a4 = 13;
void (^existingBlock)(void) = ^{NSLog(@"Hello %d", a4);};
NSLog(@"Outside Block, address of block pointer address is %p, block address is %p", &existingBlock, existingBlock);
^{
NSLog(@"Inside Block, address of block pointer address is %p, block address is %p", &existingBlock, existingBlock);}();
blockType1 blk = existingBlock;
blk();
輸出為:

這里可以看到,對(duì)于Block變量,existingBlock(注意,這個(gè)existingBlock變量是一個(gè)Block指針,而不是Block本身)被const copy了一份到Block中。而對(duì)于Block指針?biāo)赶虻腂lock實(shí)體,并沒(méi)有發(fā)生改變。
也就說(shuō),在Block內(nèi)部和外部,會(huì)有兩個(gè)Block指針,指向了同一個(gè)Block結(jié)構(gòu)體。
這里再次強(qiáng)調(diào)一下,我們所聲明的Block變量existingBlock,是一個(gè)指向Block類型的指針,而不是Block實(shí)體。正如同NSObject *obj = [NSObject new]一樣,obj是一個(gè)指向NSObject的指針,而不是NSObject實(shí)體。
下面是Clang文檔的例子:
void (^existingBlock)(void) = ...;
void (^vv)(void) = ^{ existingBlock(); }
vv();
struct __block_literal_3 {
...; // existing block
};
struct __block_literal_4 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_4 *);
struct __block_literal_3 *const existingBlock; // 這里可以看到,在Block內(nèi)部,是保持了外部的Block指針
};
void __block_invoke_4(struct __block_literal_2 *_block) {
__block->existingBlock->invoke(__block->existingBlock);
}
void __block_copy_4(struct __block_literal_4 *dst, struct __block_literal_4 *src) {
//_Block_copy_assign(&dst->existingBlock, src->existingBlock, 0);
_Block_object_assign(&dst->existingBlock, src->existingBlock, BLOCK_FIELD_IS_BLOCK);
}
void __block_dispose_4(struct __block_literal_4 *src) {
// was _Block_destroy
_Block_object_dispose(src->existingBlock, BLOCK_FIELD_IS_BLOCK);
}
static struct __block_descriptor_4 {
unsigned long int reserved;
unsigned long int Block_size;
void (*copy_helper)(struct __block_literal_4 *dst, struct __block_literal_4 *src);
void (*dispose_helper)(struct __block_literal_4 *);
} __block_descriptor_4 = {
0,
sizeof(struct __block_literal_4),
__block_copy_4,
__block_dispose_4,
};
這時(shí)候Block的數(shù)據(jù)結(jié)構(gòu)是:
struct __block_literal_4 _block_literal = {
&_NSConcreteStackBlock,
(1<<25)|(1<<29), <uninitialized>
__block_invoke_4,
& __block_descriptor_4 // 這里可以看到,在Block內(nèi)部,是保持了外部的Block指針
existingBlock,
};
截取NSObject類型變量
在Clang中,NSObject類型變量被當(dāng)做__attribute__((NSObject))類型。Block截取NSObject對(duì)象時(shí),同樣會(huì)做一份const copy NSObject *。
比如:
@interface MyObject : NSObject
- (void)sayMyObjectAddress
@end
@implementation MyObject
- (void)sayMyObjectAddress {
NSLog(@"Instance pointer address is %p, Instance address is %p", &self, self);
}
@end
MyObject *obj = [MyObject new];
[obj sayMyObjectAddress];
^{
[obj sayMyObjectAddress];
}();
輸出為:

可以看到,當(dāng)Block對(duì)NSObject做const copy時(shí),僅是做了淺拷貝,并沒(méi)有復(fù)制指針?biāo)赶虻膬?nèi)容,僅僅是const copy了指針。因此,這里的self指針地址是改變了,而self指針?biāo)赶虻牡刂范际峭粋€(gè)。
就像上面Block類型變量的例子,是同一個(gè)道理。
而對(duì)于NSObject類型,同樣需要兩個(gè)copy helper函數(shù):
void __block_copy_foo(struct __block_literal_5 *dst, struct __block_literal_5 *src) {
_Block_object_assign(&dst->objectPointer, src-> objectPointer, BLOCK_FIELD_IS_OBJECT);
}
void __block_dispose_foo(struct __block_literal_5 *src) {
_Block_object_dispose(src->objectPointer, BLOCK_FIELD_IS_OBJECT);
}
截取__block修飾的變量
鑒于我們上面所說(shuō)的都是const copy,因此對(duì)于在Block中對(duì)于其截取變量的任何改變,都是不被允許的。如果我們要修改Block內(nèi)部的值,編譯器就會(huì)提示如下錯(cuò)誤:

那如何在Block中修改截取變量的值呢?我們自然會(huì)想到對(duì)外部變量加上__block修飾符。我們將上面代碼改成下面的形式,則會(huì)順利編譯通過(guò):
__block int b = 13;
NSLog(@"Outside Block, address of __block int b is %p, b = %d", &b, b);
blockType1 blk = ^{
b++;
NSLog(@"Inside Block, address of __block int b is %p, b = %d", &b, b);
};
blk();
NSLog(@"After Block, address of __block int b is %p, b = %d", &b, b);
輸出為:

這里會(huì)發(fā)現(xiàn)一個(gè)有意思的現(xiàn)象,雖然在進(jìn)入Block前后,b的地址并不一樣!** 也就是在進(jìn)入Block前后,其實(shí)會(huì)有兩個(gè)不同的b **。
之所以會(huì)這樣,與Clang對(duì)于__block類型變量的處理有關(guān)。
當(dāng)變量被標(biāo)記為__block類型時(shí),Clang會(huì)對(duì)變量b進(jìn)行改寫成一個(gè)如下格式的struct:
struct _block_byref_foo {
void *isa; // 設(shè)置為NULL
struct Block_byref *forwarding; // Block外部變量的地址
int flags; //refcount;
int size; // size of _block_byref_foo
typeof(marked_variable) marked_variable; // copy of Block 外部變量
};
比如:
int __block i = 10;
i = 11;
會(huì)被Clang改寫做:
struct _block_byref_i {
void *isa;
struct _block_byref_i *forwarding;
int flags; //refcount;
int size;
int captured_i;
} i = { NULL, &i, 0, sizeof(struct _block_byref_i), 10 };
i.forwarding->captured_i = 11;
可以看到,int __block i 被改寫為了struct _block_byref_i 結(jié)構(gòu)體。這里需要明確一點(diǎn):
添加了__block關(guān)鍵字后的int b,實(shí)質(zhì)類型并不是int類型,而是一個(gè)struct _block的結(jié)構(gòu)體類型了。
這里有個(gè)關(guān)鍵的屬性變量,forwarding,forwarding指向一個(gè)__block結(jié)構(gòu)體。
當(dāng)__block在棧上時(shí),forwarding會(huì)指向__block自身。而當(dāng)__block在堆上生成一份copy時(shí),這時(shí)候棧上的forwarding會(huì)指向堆上的那一份拷貝。而在堆上的那個(gè)__block的__forwarding 指針,則指向自己的首地址。
也就是說(shuō),只要通過(guò)forwarding來(lái)操作__block結(jié)構(gòu)體捕獲的外部變量,實(shí)質(zhì)上是操作的同一個(gè)變量。
我們用圖片可以更清楚的弄懂其中的原理:
這也就是為什么,即使Block外和Block內(nèi)部b分別是兩個(gè)變量,而b的值卻可以被改變的原因。因?yàn)樵跅I系?code>__block結(jié)構(gòu)體中,通過(guò)forwarding指針指向了堆上的Block的地址。那么當(dāng)在Block內(nèi)部修改b的值,也就是改變堆上的int b的值的時(shí)候,在Block外部再訪問(wèn)b的值的時(shí)候,其實(shí)在棧上的__block int b通過(guò)__forwarding 指針,訪問(wèn)到了堆上的__block int b,這讓我們感覺在棧上的變量也被修改了。
這也就是為什么,在測(cè)試代碼中,在執(zhí)行完Block后,再輸出b的地址,發(fā)現(xiàn)是和Block內(nèi)部的地址一致,而不是進(jìn)入Block之前的地址的原因。(以為進(jìn)入Block后,再次訪問(wèn)b,實(shí)際上會(huì)指向堆上的那個(gè)b,而不是之前棧上的那個(gè)b)
當(dāng)我們將__block的變量導(dǎo)入Block中時(shí),Clang會(huì)作如下改寫:
例如,
int __block i = 2;
functioncall(^{ i = 10; });
會(huì)被Clang做如下改寫:
struct _block_byref_i {
void *isa; // set to NULL
struct _block_byref_voidBlock *forwarding;
int flags; //refcount;
int size;
void (*byref_keep)(struct _block_byref_i *dst, struct _block_byref_i *src);
void (*byref_dispose)(struct _block_byref_i *);
int captured_i;
};
struct __block_literal_5 {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal_5 *);
struct __block_descriptor_5 *descriptor;
struct _block_byref_i *i_holder;
};
void __block_invoke_5(struct __block_literal_5 *_block) {
_block_byref_i * i_holder = _block->i_holder;
i_holder->forwarding->captured_i = 10;
}
void __block_copy_5(struct __block_literal_5 *dst, struct __block_literal_5 *src) {
_Block_object_assign(&dst->i_holder, src->i_holder, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
}
void __block_dispose_5(struct __block_literal_5 *src) {
_Block_object_dispose(src->i_holder, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
}
static struct __block_descriptor_5 {
unsigned long int reserved;
unsigned long int Block_size;
void (*copy_helper)(struct __block_literal_5 *dst, struct __block_literal_5 *src);
void (*dispose_helper)(struct __block_literal_5 *);
} __block_descriptor_5 = { 0, sizeof(struct __block_literal_5) __block_copy_5, __block_dispose_5 };
上面的數(shù)據(jù)結(jié)構(gòu)會(huì)做如下初始化
struct _block_byref_i i_holder = {( .isa=NULL, .forwarding=&i, .flags=0, .size=sizeof(struct _block_byref_i), .captured_i=2 )};
struct __block_literal_5 _block_literal = {
&_NSConcreteStackBlock,
(1<<25)|(1<<29), <uninitialized>,
__block_invoke_5,
&__block_descriptor_5,
& i_holder,
};
是否只有__block類型才能夠在Block中被修改?
這里插入一個(gè)小測(cè)試,對(duì)于靜態(tài)變量a,是否可以在Block中作出改變呢?
static int a = 13;
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"Outside Block, static int a address is %p", &a);
^{
NSLog(@"Inside Block, static int a address is %p", &a);
a++;
}();
NSLog(@"Now a is %d", a);
}
答案是可以的,在Block之后,a的值變?yōu)?4。這是因?yàn)閷?duì)于全局/靜態(tài)變量而言,Block會(huì)直接引用變量,而不會(huì)做const copy。
所以,我們這一節(jié)討論的,是除去全局、靜態(tài)變量外,被Block const copy的其他的類型變量。
小測(cè)試
題目一. 下面代碼會(huì)輸出什么?
typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int i = 13;
blockType blk = ^{
NSLog(@"In block i = %d", i);
};
i += 2;
blk();
NSLog(@"Now i = %d", i);
}
return 0;
}
這里考察對(duì)于auto類型變量,Block的截取方式。因?yàn)閍uto變量會(huì)在Block中做一份const copy,因此在Block內(nèi)外,實(shí)質(zhì)上應(yīng)該存在兩個(gè)i。
這里的輸出為:

題目二. 下面的代碼會(huì) 正常輸出/編譯錯(cuò)誤/runtime crash
typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *str = @"Hello";
blockType blk = ^{
str = @"World";
};
blk();
NSLog(@"Now str is %@", str);
}
return 0;
}
因?yàn)閷?duì)于NSObject類型,在Block中會(huì)當(dāng)做NSObject *const obj處理,此時(shí)是一個(gè)指針常量。對(duì)于指針常量,是不能夠更改其指針?biāo)赶虻奈恢玫?,因此,這里會(huì)出現(xiàn)編譯錯(cuò)誤。
題目三. 下面的代碼會(huì) 正常輸出/編譯錯(cuò)誤/runtime crash
typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block NSString *str = @"Hello";
blockType blk = ^{
str = @"World";
};
blk();
NSLog(@"Now str is %@", str);
}
return 0;
}
因?yàn)閟tr變量用了__block修飾,因此__block NSString *str 實(shí)質(zhì)上一個(gè)__block struct 類型變量:
struct _block_byref_str {
void *isa;
struct _block_byref_str *forwarding;
int flags;
int size;
NSString *captureStr;
}
當(dāng)創(chuàng)建__block 類型變量時(shí),在Block結(jié)構(gòu)體中,會(huì)存儲(chǔ)__block結(jié)構(gòu)體指針:
struct __block_literal {
void *isa;
int flags;
int reserved;
void (*invoke)(struct __block_literal * _cself);
struct __block_descriptor *descriptor;
struct _block_byref_str *str_holder; // __block結(jié)構(gòu)體指針
}
當(dāng)調(diào)用invoke方法時(shí),會(huì)是這樣的:
void invoke(struct __block_literal * _cself) {
_block_byref_str *str_holder = _cself->str_holder;
str_holder->forwarding->captureStr = @"World";
}
由于通過(guò)forwarding指針,確保了Block外部和內(nèi)部的str都是一個(gè)指針,因此,當(dāng)Block內(nèi)部的str指向新的地址時(shí)(str = @"World"),在Block外部的str也指向了新的地址。(因?yàn)樗鼈兪峭粋€(gè)東西)。
這個(gè)過(guò)程用圖表示為:
-
__block str = @"World";
image
-
當(dāng)在Block中操作str=@"World"時(shí),相應(yīng)的__block結(jié)構(gòu)體會(huì)拷貝到heap上,同時(shí),stack上的__block結(jié)構(gòu)體的forwarding指針也會(huì)指向heap上的那份copy:
image 因此,在Block外面再次輸出str的內(nèi)容時(shí),由于這時(shí)候stack上__block結(jié)構(gòu)體的forwarding指針已經(jīng)指向了heap上的__block結(jié)構(gòu)體,因此也會(huì)輸出heap上的captured_str指針?biāo)赶虻膬?nèi)容:
@“World”。
為了驗(yàn)證我們的猜測(cè),我們可以用如下代碼:

在進(jìn)入Block前,Block中,進(jìn)入Block后分別設(shè)置斷點(diǎn),并打印aR指針的地址&aR,會(huì)得到如下結(jié)果:

可以看到,在Block中和進(jìn)入Block后,aR的地址是一樣的,而在進(jìn)入Block之前,則是另一個(gè)地址。這是因?yàn)樵趕tack上的__block結(jié)構(gòu)變量,將其forwarding指針指向了heap地址所導(dǎo)致的。
題目四. 下面的代碼會(huì) 正常輸出/編譯錯(cuò)誤/runtime crash
typedef void(^blockType)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *aStr = @"Hello";
__block NSString *str = aStr;
blockType blk = ^{
str = @"World";
};
blk();
NSLog(@"Now a aStr is %@", aStr);
NSLog(@"Now str is %@", str);
}
return 0;
}
這個(gè)題目和題目三類似,只不過(guò)對(duì)于str的賦值由__block NSString *str = @"Hello"變成了__block NSString *str = aStr。
上面這段代碼會(huì)正常輸出,其結(jié)果為:

至于str為什么會(huì)由@"Hello"變成@“World”,其原因見題目三。
這里aStr是沒(méi)有任何變化的,這是因?yàn)樵趯tr在Block中賦值為@"World"時(shí),僅僅是將str指向了新的地址,而沒(méi)有更改原地址的內(nèi)容。而aStr一直指向舊的地址,也就是值為@"World"的地址。
題目五. 下面的代碼會(huì) 正常輸出/編譯錯(cuò)誤/runtime crash
NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
blockType blk = ^{
[str appendString:@" World"];
};
blk();
NSLog(@"Now str is %@", str);
答案是會(huì)正常輸出。因?yàn)閷?duì)于NSObject類型來(lái)說(shuō),Block會(huì)copy一份指針常量來(lái)保存NSObject的地址。所謂指針常量,是指指針指向的地址是不可用更改的。而這里在Block中,并沒(méi)有更改指針指向的地址,而僅僅是改變了指針指向地址中的值,這個(gè)操作是允許的。
其輸出結(jié)果為:

同樣的,類似還有下面代碼,也是可以正常運(yùn)行,并輸出名字Tim:
MyRetaion *aR = [MyRetaion new];
aR.name = @"Jack";
blockType blk = ^{
aR.name = @"Tim";
};
blk();
NSLog(@"Now name is %@", aR.name);
總結(jié)
在本篇文章中,我們根據(jù)Clang的官方文檔,分析總結(jié)了Clang為了支持Block,其背后所使用的數(shù)據(jù)結(jié)構(gòu)。同時(shí),我們重點(diǎn)分析了Block對(duì)于不同類型的外部變量的截取方式。按照Block不同的處理方式,Block截取的變量類型可以分為:
- 全局/靜態(tài)類型
- auto類型
- Block類型
- NSObject類型
- __block類型
不同的類型,Block都有不同的截取處理方式。
通過(guò)深入了解Block的機(jī)制,相信對(duì)大家編程中正確高效的使用Block,是很有幫助的。
現(xiàn)在來(lái)回答我們文章最開始的部分,代碼的輸出結(jié)果為:
a = 14, b = 13, str = HelloWorld
至于原因,相信大家都會(huì)知道了:)