Block 的實質
Block 是“帶有自動變量值的匿名函數”,我們可以通過 Block 的實現來加深理解。首先通過 clang 工具可以將含有 Block 的源代碼轉換為一般C語言代碼。
將下面代碼保存為 test.m:
int main()
{
void (^blk)(void) = ^{int i=0;};
blk();
return 0;
}
在終端執行命令clang -rewrite-objc test.m
,生成 test.cpp C 代碼文件,我們來看下 block 語句^{int i=0;};
轉換后的代碼:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int i=0; }
__main_block_impl_0 是一個結構體,如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__main_block_impl_0
有兩個成員變量:impl,Desc,還有一個構造函數__main_block_impl_0(void*, struct, int)
。
我們先看impl
,__block_impl
類型, block 結構體:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
第二個成員變量 Desc,是__main_block_desc_0
類型指針:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
reserved 為預留字段,Block_size 是該 block 的大小
下面我們看看該結構體的構造函數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
以上就是初始化 __main_block_impl_0
結構體成員的源代碼。fp 是 __main_block_func_0
函數指針,賦給 impl.FuncPtr,通過 impl.FuncPtr 即可調用 block 函數。__block_impl
結構體的 isa
成員使用_NSConcreteStackBlock
初始化,這里的_NSConcreteStackBlock
是什么?首先要理解 Objective-C 類和對象的實質。其實,所謂 Block 就是 Objective-C 對象。在 /usr/include/objc/runtime.h 中 isa 的聲明:
typedef struct objc_object {
Class isa;
} *id;
isa 保持該類的結構體實例指針。_NSConcreteStackBlock 是 Block 結構體存儲類型。
Block 存儲區
- _NSConcreteStackBlock:存儲在棧上,超出作用域被銷毀。
- _NSConcreteGlobalBlock:存儲在程序的數據區域中。當 block 定義在全區或者 block 沒有截獲外部變量時,block 存在數據區。
- _NSConcreteMallocBlock:存儲在由 malloc 函數分配的堆中。使用此種方式,可在 block 作用域外訪問 block,引用計數為 0 時被銷毀。
截獲自動變量
int main()
{
int val = 3;
void (^blk)(void) = ^{int i = val;};
blk();
return 0;
}
那么 __main_block_impl_0
結構體變成了
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val;
}
多了一個成員變量 val,val 被截獲了。所謂的截獲自動變量值,就是 block 所使用的外部變量被保存到 Block 的結構體實例中。
__block
block 不能修改外部變量,但如果在聲明變量的時候加上關鍵字 __block 就可在 block 內部修改外部變量。這是如何實現的?看代碼:
int main()
{
__block int val = 3;
void (^blk)(void) = ^{val = 1;};
blk();
return 0;
}
轉換后的代碼多了一個結構體聲明
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
val 被轉換成一個結構體,其中__forwarding 是一個指向自身的指針,當 block 被拷貝時,該指針指向堆上的結構體實例。而__main_block_impl_0
val 變成對這個結構體的引用:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val;
}
因此可以在 block 內部修改外部的變量。
同時,改變的還有__main_block_desc_0
,多了兩個成員變量 copy 和 dispose,在拷貝 block 的時候使用到。
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
變量和 Block 也被廢棄,復制到堆上的 __block
變量和 Block 在變量作用域結束時不受影響。
__block
用結構體成員變量__forwarding
可以實現無論__block
變量配置在棧上還是堆上時都能夠正確地訪問__block
變量。
Block 循環引用
如果在 Block 中使用附有 __strong 修飾符的對象,那么當 Block 從棧復制到堆時,該對象為 Block 所持有,容易引起循環引用。如下代碼
typedef void (^blk_t)(void);
@interface MyObject : NSObject {
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{NSLog(@"obj_ = %@", obj_);};
return self;
}
@end
編譯器會產生警告:Capturing 'self' strongly in this block is likely to lead to a retain cycle。但很多時候編譯器無法判斷是否產生循環引用,我們在使用 block 的時候要特別小心,避免循環引用可以使用 __weak 修飾符。
id __weak obj = obj_;
blk_ = ^{NSLog(@"obj_ = %@", obj);};