本文的內容主要是基於Clang編譯器的官方文檔所寫。
Clang
先說些題外話,什麼是Clang?Clang是C++編寫的編譯器。我們知道,我們平常代碼所寫的任何程序,最終都需要通過編譯器轉換成與語言無關的機器二進制代碼。而Clang,則是支持/C++/Objective-C/Objective-C++的編譯器。那我們在做OC開發時,可能也會聽說LLVM編譯器,那麼Clang和LLVM之間是什麼關係呢?
它們的關係如下圖所示:
Clang是編譯器的前端,它會分析具體的編程語言,然後用於生成與機器無關的中間代碼。而LLVM是編譯器的後端,與具體編程語言無關,而是會去分析統一的中間代碼,生成符合對應機器的目標程序。
這樣拆分前端後端的好處在於,前後端可以獨立的替換,便於編譯器的優化。
關於Clang,我們瞭解這些就足夠了。
Block的本質
回到Block上來。我們在使用Block語法時,總會感覺到有些奇怪:
^{
NSLog(@"Hello");
};
這麼一個^{}
是什麼鬼?似乎在別的語言中也沒有見過這麼個關鍵字定義。其實,^{}
對於Clang編譯器來說,僅僅是一個語言標記,它會告訴Clang,這裏我需要定義一個Block類型的結構體。
而Clang發現這個語言標記時,會將^{}
這麼一個奇怪的定義,轉換爲C語言中的結構體
。經過Clang轉換後的Block,其形式是這樣的:
struct Block_literal_1 {
// 第一部分. Block基本信息以及 invoke函數指針
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
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結構體定義分成了三個部分:
- Block基本信息以及 invoke函數指針
- Block descriptor指針
- Block所截取的外部變量
在這裏我們得出結論:Block的本質是一個C語言的struct
。
Block對應的結構體
上面探討了Block的本質是一個struct,接下來我們就來詳細看一下這個 Block struct的定義。
Block基本信息以及Block descriptor
struct Block_literal_1 {
// 第一部分. Block基本信息以及 invoke函數指針
void *isa; // initialized to __NSGlobalBlock__&__NSMallocBlock__&__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 struct的第一部分。
當我們聲明一個Block時,對應的Block struct會被如下初始化:
-
系統會聲明並初始化一個Block descriptor結構體。初始化Block descriptor步驟如下
a. Block descriptor 的size部分會被設置爲Block結構體的大小
b. copy_helper 和 dispose_helper函數指針會被設置爲對應的函數指針(如果需要這兩個helper 函數的話) -
系統初始化Block 結構體。 初始化Block 結構體的步驟如下:
a. isa 部分會被設置爲__NSGlobalBlock__
/__NSMallocBlock__
/__NSStackBlock__
所對應的地址。
b. flags 會被置爲對應的flag數值。比如,如果Block struct需要copy,dispose helper函數時,響應的flag會被置位。同時,flags還有標誌Block ABI 版本的功能。
c. 設置invoke函數指針指向對應的函數。該函數的第一個參數是Block struct本身的指針
,而其餘的參數則是Block執行時外部要傳入的參數(如果有的話)
舉個例子,對於下面的Block:
^ { printf("hello world\n"); }
Clang會創建如下內容:
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將會如下被初始化:
struct __block_literal_1 _block_literal = {
&__NSGlobalBlock__,
(1<<29), <uninitialized>,
__block_invoke_1,
&__block_descriptor_1
};
這是Clang文檔給出的官方例子,但是我們這裏不要去究竟flags究竟是設置的什麼,因爲根據本人的測試,其flags的值並不是1<<29。
這裏有個問題,就是什麼時候isa會被設爲__NSGlobalBlock__
/__NSMallocBlock__
/__NSStackBlock__
呢?
- 當Block中沒有引用外部變量,或引用了全局變量,const 標量或static變量時,Block的isa會被設置爲
__NSGlobalBlock__
。 這時的Block生命週期是伴隨程序始終的。 __NSStackBlock__
表示這個block, 是在棧上面分配的,出了棧就會消亡。使用了外部棧變量,就會是__NSStackBlock__
類型。__NSMallocBlock__
表示Block複製到堆上面了,可以存儲下來,以後使用。當Block引用了外部的OC對象,Block對象或用__block修飾的變量時,Block會被設置爲__NSMallocBlock__
類型。這裏有一點要注意,在ARC的情況下。只要將block賦值給變量,就自動幫你複製了。也就是說,如果將一個棧上的block賦值給另一個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]));
輸出爲:
而對於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]));
輸出爲:
Block的外部變量截取
理解Block的關鍵,在於理解Block是如何處理外部變量的。
我們先來想一想,Block中會截取那些類型的外部變量:
- 全局/靜態變量
- 自動存儲類型
- Block類型
- NSObject類型
- __block修飾的變量
截取全局/靜態類型變量
對於全局/靜態變量,Block會直接引用這類變量,不會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內,static int a的地址是一樣的,Block並沒有做特殊的處理。
截取自動存儲類型變量
所謂自動存儲類型,指的是auto類型
。其實在函數中的局部變量,不加特殊聲明,都是auto變量,但是關鍵字auto
可以被省略。這些變量在函數被調用時分配存儲方式,函數調用結束後這些存儲空間就被釋放了。
我們可以理解爲棧上的變量(Block類型、__block、NSObject類型除外)。
Variables of auto storage class are imported as const copies.
也就是說,auto類型會在Block中用const copy一份。也就是說Block內,外是完全不同的兩個變量。
例如:
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內部,表面上同樣的b變量,其地址是不一樣的。究其原因,就是因爲在Block內部,系統會默默的const copy一份b。
這時候,Block的數據結構是這樣的:
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; // 這裏會有一份const copy
};
struct __block_literal_2 __block_literal_2 = {
&_NSConcreteStackBlock,
(1<<29), <uninitialized>,
__block_invoke_2,
&__block_descriptor_2,
x
};
一般的,對於標量類型(int, float, bool等基本類型),struct,unions和函數指針類型,都會採用const copy的方式,將Block外部的變量拷貝到Block內部
。
這裏需要注意一點,在iOS系統中,當我們把一個stack 上的Block賦值給一個Block變量時:
void (^vv)(void) = ^{ printf("x is %d\n", x); }
會默認調用Block的copy方法,即,上面實際上是如下代碼:
void (^vv)(void) = [^{ printf("x is %d\n", x); } copy];
這樣得到的vv,是一個在堆上的Block變量。這時候再輸出vv中x的地址,會得到一個堆上的地址。
因此,我們在做實驗的時候,不要輸出對拷貝後的Block中變量地址,而應該直接輸出Block中的地址:
^{
NSLog(@"Inside Block, static int a address is %p", &a);
}();
上面代碼中並沒有賦值,因此會輸出棧上的a的const copy地址。
截取Block類型變量
對於截取Block類型的變量,在Block內部,同樣會保留其const copy。
如下代碼:
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();
輸出爲:
可以看到在Block外部和內部,existingBlock分別是不同的地址。
但是,這裏爲了copy 外部Block到Block內部,還需要兩個輔助函數:copy_helper
和 dispose_helper
。這兩個函數用來將stack上的Block pointer拷貝到堆上。
這裏需要注意的是,我們所聲明的Block變量existingBlock,是一個指向Block類型的指針,而不是Block本身。正如同NSObject *obj = [NSObject new]一樣,obj是一個指向NSObject的指針,而不是NSObject類型變量
。
下面是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;
};
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,
};
這時候Block的數據結構是:
struct __block_literal_4 _block_literal = {
&_NSConcreteStackBlock,
(1<<25)|(1<<29), <uninitialized>
__block_invoke_4,
& __block_descriptor_4
existingBlock,
};
截取NSObject類型變量
在Clang中,NSObject類型變量被當做__attribute__((NSObject))
類型。和Block類型一樣,Block截取NSObject對象時,同樣會做一份const copy。
比如:
@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];
}();
輸出爲:
可以看到,當Block對NSObject做const copy時,僅是做了淺拷貝
,並沒有複製指針所指向的內容,僅僅是const copy了指針。因此,這裏的self指針地址是改變了,而self指針所指向的地址都是同一個。
就像上面Block類型變量的例子,是同一個道理。
而對於NSObject類型,同樣需要兩個copy helper函數:
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修飾的變量
鑑於我們上面所說的const copy,因此對於在Block中對於其截取變量的任何改變,都是不被允許的。如果我們要修改Block內部的值,編譯器就會提示如下錯誤:
如果需要在Block內部修改所截取的外部變量,需要在外部變量上加上__block
修飾符。我們將上面代碼改成下面的形式,則會順利編譯通過:
__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(@"Now b = %d", b);
輸出爲:
這裏會發現一個有意思的現象,雖然在Block外部和內部,b的地址並不一樣,也就是在Block外部和內部,其實有兩個不同的b
。但是當我們在Block內部修改b的值時(b++
),然後在Block外部再輸出b的值時,發現外部的b也同樣被修改了!
之所以會這樣,與Clang對於__block類型變量的處理有關。
當變量被標記爲__block類型時,Clang會對變量進行改寫成一個如下格式的struct:
struct _block_byref_foo {
void *isa; // 設置爲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;
會被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
結構體。
這裏有個關鍵的屬性變量,forwarding
,forwarding
指向一個__block結構體。
當Block在棧上時,forwarding
會指向Block自身的棧地址。而當Block在堆上生成一份copy時,這時候棧上的forwarding
會指向堆上的那一份拷貝。
也就是說,只要通過forwarding
來操作__block結構體捕獲的外部變量,實質上是操作的同一個變量。
我們用圖片可以更清楚的弄懂其中的原理:
這也就是爲什麼,即使Block外和Block內部i
分別是兩個變量,而i
的值卻可以被改變的原因。因爲在__block結構體
中,通過forwarding指針指向了外部的i的地址,當在Block內部改變i
的值時,只需要通過forwarding指針直接修改地址中的值即可。
當我們將__block的變量導入Block中時,Clang會作如下改寫:
例如,
int __block i = 2;
functioncall(^{ i = 10; });
會被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->forwarding->captured_i = 10;
}
void __block_copy_5(struct __block_literal_5 *dst, struct __block_literal_5 *src) {
//_Block_byref_assign_copy(&dst->captured_i, src->captured_i);
_Block_object_assign(&dst->captured_i, src->captured_i, BLOCK_FIELD_IS_BYREF | BLOCK_BYREF_CALLER);
}
void __block_dispose_5(struct __block_literal_5 *src) {
//_Block_byref_release(src->captured_i);
_Block_object_dispose(src->captured_i, 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 };
上面的數據結構會做如下初始化
struct _block_byref_i i = {( .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,
};
小測試
題目一. 下面代碼會輸出什麼?
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;
}
這裏考察對於auto類型變量,Block的截取方式。因爲auto變量會在Block中做一份const copy,因此在Block內外,實質上應該存在兩個i
。
這裏的輸出爲:
題目二. 下面的代碼會 正常輸出/編譯錯誤/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;
}
因爲對於NSObject類型,在Block中會當做NSObject *const obj處理,此時是一個指針常量。對於指針常量,是不能夠更改其指針所指向的位置的,因此,這裏會出現編譯錯誤。
題目三. 下面的代碼會 正常輸出/編譯錯誤/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;
}
因爲str變量用了__block
修飾,因此__block NSString *str
實質上一個__block struct 類型變量:
struct _block_byref_str {
void *isa;
struct _block_byref_str *forwarding;
int flags;
int size;
NSString *captureStr;
}
當創建__block 類型變量時,在Block結構體中,會存儲__block結構體指針:
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結構體指針
}
當調用invoke方法時,會是這樣的:
void invoke(struct __block_literal * _cself) {
_block_byref_str *str_holder = _cself->str_holder;
str_holder->forwarding->captureStr = @"World";
}
由於通過forwarding指針,確保了Block外部和內部的str都是一個指針,因此,當Block內部的str指向新的地址時(str = @"World"
),在Block外部的str也指向了新的地址。(因爲它們是同一個東西)。
這個過程用圖表示爲:
-
__block str = @“World”;
-
當在Block中操作str=@"World"時,相應的__block結構體會拷貝到heap上,同時,stack上的__block結構體的forwarding指針也會指向heap上的那份copy:
-
因此,在Block外面再次輸出str的內容時,由於這時候stack上__block結構體的forwarding指針已經指向了heap上的__block結構體,因此也會輸出heap上的captured_str指針所指向的內容:
@“World”
。
爲了驗證我們的猜測,我們可以用如下代碼:
在進入Block前,Block中,進入Block後分別設置斷點,並打印aR
指針的地址&aR
,會得到如下結果:
可以看到,在Block中和進入Block後,aR
的地址是一樣的,而在進入Block之前,則是另一個地址。這是因爲在stack上的__block結構變量,將其forwarding指針指向了heap地址所導致的。
題目四. 下面的代碼會 正常輸出/編譯錯誤/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;
}
這個題目和題目三類似,只不過對於str的賦值由__block NSString *str = @"Hello"
變成了__block NSString *str = aStr
。
上面這段代碼會正常輸出,其結果爲:
至於str爲什麼會由@"Hello"變成@“World”,其原因見題目三。
這裏aStr是沒有任何變化的,這是因爲在將str在Block中賦值爲@"World"時,僅僅是將str指向了新的地址,而沒有更改原地址的內容。而aStr一直指向舊的地址,也就是值爲@"World"的地址。
題目五. 下面的代碼會 正常輸出/編譯錯誤/runtime crash
NSMutableString *str = [NSMutableString stringWithString:@"Hello"];
blockType blk = ^{
[str appendString:@" World"];
};
blk();
NSLog(@"Now str is %@", str);
答案是會正常輸出。因爲對於NSObject類型來說,Block會copy一份指針常量來保存NSObject的地址。所謂指針常量,是指指針指向的地址是不可用更改的。而這裏在Block中,並沒有更改指針指向的地址,而僅僅是改變了指針指向地址中的值,這個操作是允許的。
其輸出結果爲:
同樣的,類似還有下面代碼,也是可以正常運行,並輸出名字Tim:
MyRetaion *aR = [MyRetaion new];
aR.name = @"Jack";
blockType blk = ^{
aR.name = @"Tim";
};
blk();
NSLog(@"Now name is %@", aR.name);
總結
在本篇文章中,我們根據Clang的官方文檔,分析總結了Clang爲了支持Block,其背後所使用的數據結構。同時,我們重點分析了Block對於不同類型的外部變量的截取方式。按照Block不同的處理方式,Block截取的變量類型可以分爲:
- auto類型
- Block類型
- NSObject類型
- __block類型
不同的類型,Block都有不同的截取處理方式。
通過深入瞭解Block的機制,相信對大家編程中正確高效的使用Block,是很有幫助的。