Block做爲Objective-C中閉包的實如今iOS開發中佔有很是重要的地位,尤爲是做爲回調(callback)使用。這篇文章主要記錄Block的實現,關於Block的語法能夠參考這裏:How Do I Declare A Block in Objective-Chtml
Block被稱爲帶有自動變量(局部變量)的匿名函數,Block語法去和C語言的函數很是類似。實際上Block的底層就是做爲C語言源代碼來處理的,支持Block的編譯器會將含有Block語法的源代碼轉換爲C語言編譯器能處理的源代碼,看成C語言源碼來編譯。編程
經過LLVM編譯器clang能夠將含有Block的語法轉換爲C++源碼:bash
clang -rewrite-objc fileName
複製代碼
好比一段很是簡單的含有Block的代碼:數據結構
#include <stdio.h>
int main() {
void (^blk)(void) = ^{
printf("Hello Block!\n");
};
blk();
return 0;
}
複製代碼
使用clang將其轉換爲C++源碼後,其核心內容以下:多線程
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// block的數據結構定義
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;
}
};
// block中的方法
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello Block!\n");
}
// 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() {
// 調用__main_block_impl_0的構造函數
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// blk()調用
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
複製代碼
這段源碼主要包含了3個struct和兩個函數,實際上就是C語言源碼:閉包
很容易看出mian()函數就是最初代碼中的mian函數,__main_block_func_0函數就是最初代碼中的Block語法:app
^{
printf("Hello Block!\n");
};
複製代碼
由此得出:框架
接下來重點看看__main_block_impl_0結構體函數
__main_block_func_0函數的參數__cself類型聲明爲struct __main_block_impl_0。__main_block_impl_0就是該Block的數據結構定義,其中包含了成員變量爲impl和Desc指針,impl的__block_impl結構體聲明中包含了某些標誌、從此版本升級所需的區域以及函數指針:ui
struct __block_impl {
void *isa;
int Flags; // 某些標誌
int Reserved; // 從此版本升級所需的區域
void *FuncPtr; // 函數指針
};
複製代碼
Desc指針的中包含了Block的大小:
static struct __main_block_desc_0 {
size_t reserved; // 從此版本升級所需的區域
size_t Block_size; // Block的大小
};
複製代碼
在__main_block_impl_0的構造函數中調用了impl和Desc的成員變量,這個構造函數在mian函數中被調用,爲了便於閱讀,將其中的轉換去掉:
// 調用__main_block_impl_0的構造函數
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// 去掉轉換以後
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_func_0 *blk = &tmp;
複製代碼
構造函數中使用的實參爲函數指針__main_block_func_0和靜態全局變量初始化的__main_block_desc_0結構體實例指針__main_block_desc_0_DATA:
static struct __main_block_desc_0 __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
複製代碼
經過這些調用能夠總結出下面兩條:
將__main_block_impl_0結構體展開:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
};
複製代碼
在該程序中構造函數的初始化數據以下:
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
複製代碼
能夠看出FuncPtr = __main_block_func_0就是簡單的使用函數指針FuncPtr調用函數__main_block_func_0打印Hello Block!語句,這就是最初的源碼中對於block調用的實現:
blk();
複製代碼
其對應的源碼去掉轉換以後就很清晰:
// blk()調用
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 去掉轉換以後:
(*blk->impl.FuncPtr)(blk);
複製代碼
到此,對於Block的建立和使用就能夠這樣理解:
在__main_block_impl_0的構造函數中有一個_NSConcreteStackBlock:
impl.isa = &_NSConcreteStackBlock;
複製代碼
這個isa指針很容易想到Objective-C中的isa指針。在Objective-C類和對象中,每一個對象都有一個isa指針,Objective中的類最終轉換爲struct,類中的成員變量會被聲明爲結構體成員,各種的結構體是基於objc_class結構體的class_t結構體。
typedef struct Objc_object {
Class isa;
} *id;
typedef struct obje_class *Class;
struct objc_class {
Class isa;
};
struct class_t {
struct class_t *isa;
strcut class_t *superclass;
Cache cache;
IMP *vtable;
uintptr_t data_NEVER_USE;
};
複製代碼
實際上Objective-C中由類生成對象就是像結構體這樣生成該類生成的對象的結構體實例。生成的各個對象(即由該生成的對象的各個結構體實例),經過成員變量isa保持該類的結構體實例指針。
好比一個具備成員變量valueA和valueB的TestObject類:
@interface TestObject : NSObject {
int valueA;
int valueB;
}
@end
複製代碼
其類的對象的結構體以下:
struct TestObject{
Class isa;
int valueA;
int valueB;
};
複製代碼
**在Objective-C中,每一個類(好比NSObject、NSMutableArray)均生成並保持各個類的class_t結構體實例。**該實例持有聲明的成員變量、方法名稱、方法的實現(即函數指針)、屬性以及父類的指針,並被Objective-C運行時庫所使用。
再看__main_block_impl_0結構體就至關於基於Objc_object的結構體的Objective-C類對象的結構體,其中的成員變量isa初始化爲isa = &_NSConcreteStackBlock;
,_NSConcreteStackBlock就至關於calss_t結構體實例,在將Block做爲Objective-C對象處理時,關於該類的信息放置於_NSConcreteStackBlock中。
實際上Block的實質Block就是Objective-C對象。
Block做爲傳統回調函數的替代方法的其中一個緣由是:block容許訪問局部變量,能捕獲所使用的變量的值,即保存該自動變量的瞬間值,好比下面這段代碼,Block中保存了局部變量mul的瞬間值7,因此後面對於mul的更改不影響Block中保存的mul值:
int mul = 7;
int (^blk)(int) = ^(int num) {
return mul * num;
};
// change mul
mul = 10;
int res = blk(3);
NSLog(@"res:%d", res); // res:21 not 30
複製代碼
經過clang來看看Block捕獲自動變量以後Block的結構有什麼變化:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int mul; // Block語法表達式中使用的自動變量被看成成員變量追加到了__main_block_impl_0結構體中
// 初始化結構體實例時,根據傳遞構造函數的參數對由自動變變量追加的成員變量進行初始化
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _mul, int flags=0) : mul(_mul) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int mul = __cself->mul; // bound by copy
printf("mul is:%d\n", mul);
}
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 mul = 7;
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, mul));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
複製代碼
分析__main_block_impl_0和其構造方法能夠發現:Block中使用的自動變量mul被看成成員變量追加到了__main_block_impl_0結構體中,並根據傳遞構造函數的參數對該成員變量進行初始化。__main_block_impl_0中結構體實例的初始化以下:
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
mul = 7; // 追加的成員變量
複製代碼
因而可知,**在__main_block_impl_0結構體實例(即Block)中,自動變量值被捕獲。**能夠將Block捕獲自動變量總結爲以下:Block在執行語法時,Block中所使用的自動變量值被保存到Block的結構體實例(即Block自身)中。即向結構體__main_block_impl_0中追加成員變量。
雖然Block能捕獲自動變量值,可是卻不能對其進行修改,好比下面代碼就會報錯:
int main() {
int val = 10;
void(^blk)(void) = ^ {
val = 1; // error Variable is not assignable (missing __block type specifier)
};
blk();
printf("val:%d\n", val);
return 0;
}
複製代碼
需對val變量使用__block說明符:
int main() {
__block int val = 10;
void(^blk)(void) = ^ {
val = 1;
};
blk();
printf("val:%d\n", val); // val:1
return 0;
}
複製代碼
將其用clang轉換以後:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
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};
int main() {
// __block類型的變量竟然變成告終構體 __block int val = 10;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
(void*)0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
10
};
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
printf("val:%d\n", (val.__forwarding->val));
return 0;
}
複製代碼
增長了__block變量以後源碼急劇增多,最明顯的是增長了一個結構體和4個函數:
首先比較一下使用__block和沒有使用__block的__main_block_func_0函數對變化
// 沒有使用__block
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int mul = __cself->mul; // bound by copy
printf("mul is:%d\n", mul);
}
// 使用__block
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
複製代碼
能夠看出在沒有使用__block時,Block僅僅是捕獲自動變量的值,即int mul = __cself->mul;
。
再看剛纔的源碼,使用__block變量的val竟然變成告終構體實例。
// __block int val = 10; 轉換以後的源碼:
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
複製代碼
__block變量也同Block同樣變成了__Block_byref_val_0結構體類型的自動變量(棧上生成的__Block_byref_val_0結構體實例),該變量初始化爲10,且這個值也出如今結構體實例的初始化中,表示該結構體持有至關於原有自動變量的成員變量(下面__Block_byref_val_0結構體中的成員變量val就是至關於原自動變量的成員變量):
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val; // 至關於原自動變量的成員變量
};
複製代碼
回過頭去看Block給val變量賦值的代碼:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
複製代碼
得出:__Block_byref_val_0結構體實例的成員變量__forwarding持有指向實例自身的指針。經過成員變量__forwarding訪問成員變量val。
這裏沒有將__block變量的__Block_byref_val_0結構體直接寫在Block的__main_block_impl_0結構體中是爲了能在多個Block中使用同一個__block變量。 好比在兩個Block中使用同一個__block變量:
__block int val = 10;
void (^blk1)(void) = ^ {
val = 1;
};
void (^blk2)(void) = ^ {
val = 2;
};
複製代碼
轉換以後:
__Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof( __Block_byref_val_0), 10};
blk1 = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 570425344);
blk2 = &__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, &val, 570425344);
複製代碼
雖然到這裏已經大體知道爲何Block能捕獲自動變量了,可是這裏還遺留幾個問題:
綜上可知:
結構體類型的自動變量即棧上說生成的該結構體的實例。
既然Block是Objective-C對象,那麼它具體是哪一種對象?在Block中的isa指針指向的就是該Block的Class,目前所見都是_NSConcreteStackBlock類型,而在block的runtime中實際定義了6中類型的Block,其中咱們主要接觸到的是這三種:
它們對應在程序中的內存分配:
那麼Block在什麼狀況下時在堆上的?何時時棧上的?何時又是全局的?
_NSConcreteGlobalBlock很好理解,將Block看成全局變量使用的時候,生成的Block就是_NSConcreteGlobalBlock類對象。好比:
#include <stdio.h>
void (^blk)(void) = ^{
printf("Gloabl Block\n");
};
int main() {
return 0;
}
複製代碼
用clang轉換以後爲該Block用結構體__block_impl的成員變量初始化爲_NSConcreteGlobalBlock,即Block用結構體實例設置在程序內存的數據區:
isa = &_NSConcreteGlobalBlock;
複製代碼
將全局Block存放在數據區的原爲:**使用全局變量的地方不能使用自動變量,因此不存在對自動變量的捕獲。所以Block用結構體實例的內容不依賴於執行時的狀態,因此整個程序中只須要一個實例。**只有在捕獲自動變量時,Block用結構體實例捕獲的值纔會根據執行時的狀態變化。所以總結Block爲_NSConcreteGlobalBlock類對象的狀況以下:
除了上述兩中狀況下Block配置在程序的數據區中之外,Block語法生成的Block爲_NSConcreteStackBlock類對象,且設置在棧上。 配置在棧上的Block,若是其所屬的變量做用域結束,該Block就被自動廢棄。
那麼配置在堆上的_NSConcreteMallocBlock類在什麼時候使用?
配置在全局變量上的Block,從變量做用域外也能夠經過指針訪問。可是設置在棧上的Block,若是其所屬的做用域結束,該Block就被廢棄;而且__block變量的也是配置在棧上的,若是其所屬的變量做用域結束,則該__block變量也會被廢棄。那麼這時須要將Block和__block變量複製到堆上,才能讓其不受變量域做用結束的影響。
Block提供了將Block和__block變量從棧上覆制到堆上的方法。複製到堆上的Block將_NSConcreteMallocBlock類對象寫入Block用結構體實例的成員變量isa:
isa = &_NSConcreteMallocBlock;
複製代碼
對於堆上的__block的訪問,就是經過__forwarding實現的:**__block變量用結構體成員變量__forwarding實現不管__block變量配置在棧仍是在堆上都能正確的訪問__block變量。**當__block變量配置在堆上時,只要棧上的結構體成員變量__forwarding指向堆上的結構體實例,那麼不論是從棧上仍是從堆上的__block變量都能正確訪問。
而且在ARC時期,大多數狀況下編譯器知道在合適自動將Block從棧上覆制到堆上,好比將Block做爲返回值時。而當向方法或函數的參數中傳遞Block時,編譯器不能判斷,須要手動調用copy方法將棧上的Block複製到堆上,可是apple提供的一些方法已經在內部恰當的地方複製了傳遞過來的參數,這種狀況就不須要再手動複製:
而且,無論Block配置在何存,用copy方法複製都不會出現問題。可是將Block從棧上覆制到堆上時至關消耗CPU的。對於已經在堆上的Block調用copy方法,會增長其引用計數。
而且對使用__block變量的Block從棧複製到堆上時,__block變量也會收到影響:若是在1個Block中使用__block變量,當該Block從棧複製到堆時,這些__block變量也所有被從棧複製到堆上。而且此時Block持有__block變量。若是有個Block使用__block變量,在任何一個Block從棧複製到堆時,__block變量都會一併複製到堆上並被該Block持有;當剩下的Block從棧複製到堆時,被複制的Block持有__block變量,並增長其引用計數。若是配置在堆上的Block被廢棄,它說使用的__block變量也就被釋放。這種思考方式同Objective-C內存管理方式相同。即使用__block變量的Block持有該__block變量,當Block被廢棄時,它所持有的__block變量也被廢棄。
在這裏Block捕獲的是__block類型的變量val,若是捕獲的是Objective-C對象會有什麼區別?
int main() {
id arr = [[NSMutableArray alloc] init];
void (^blk)(id) = [^(id obj) {
[arr addObject:obj];
NSLog(@"arr count: %ld", [arr count]);
} copy];
blk(@"Objective-C");
blk(@"Switf");
blk(@"C++");
return 0;
}
複製代碼
值得注意的是:Block捕獲的是objective-C對象,而且調用變動該對象的方法addObject:,因此這裏不會產生編譯錯誤。這是由於block捕獲的變量值是一個NSMutableArray類的對象,用C語言描述就是捕獲NSMutableArray類對象用的結構體實例指針。addObject方法是使用block截獲的自動變量arr的值,因此不會有任何問題,可是若是在Block內部去給捕獲的arr對象賦值就會出錯:
int main() {
id arr = [[NSMutableArray alloc] init];
void (^blk)(id) = [^(id obj) {
arr = [NSMutableArray arrayWithObjects:obj, nil]; // error Variable is not assignable (missing __block type specifier)
NSLog(@"arr count: %ld", [arr count]);
} copy];
blk(@"Objective-C");
return 0;
}
複製代碼
以前的代碼轉換以後的部分源碼爲:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->arr, (void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
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 int val = 10;
轉換以後的源碼中的部份內容:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
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*);
}
複製代碼
OBjective-C對象和__block變量對比,發如今Block用的結構體部分基本相同,不一樣之處在於:Objective-C對象用BLOCK_FIELD_IS_OBJECT標識,__block變量是用BLOCK_FIELD_IS_BYREF標識。即通過BLOCK_FIELD_IS_OBJECT和BLOCK_FIELD_IS_BYREF參數區分copy函數和dispose函數的對象類型是對象仍是__block變量。
該源碼中在__main_block_desc_0 結構體中增長了成員變量copy和dispose,以及做爲指針賦值給該成員變量的__main_block_copy_0函數和__main_block_dispose_0函數,這兩個函數的做用:
__main_block_copy_0函數中所使用的_Block_object_assign函數將對象類型對象複製給Block用結構體的成員變量arr並持有該對象,調用_Block_object_assign函數至關於retain函數,將對象賦值在對象類型的結構體成員變量中。
__main_block_dispose_0函數中使用_Block_object_dispose函數釋放賦值在Block用結構體成員變量arr中的對象。調用_Block_object_dispose函數至關於調用release函數,釋放賦值在對象類型結構體中的對象。
這兩個函數在Block從棧複製到堆和已經堆上的Block被廢棄時調用:
當Block從棧複製到堆上時,Block會持有捕獲的對象,這樣就容易產生循環引用。好比在self中引用了Block,Block優捕獲了self,就會引發循環引用,編譯器一般能檢測出這種循環引用:
@interface TestObject : NSObject
@property(nonatomic, copy) void (^blk)(void);
@end
@implementation TestObject
- (instancetype)init {
self = [super init];
if (self) {
self.blk = ^{
NSLog(@"%@", self); // warning:Capturing 'self' strongly in this block is likely to lead to a retain cycle
};
}
return self;
}
複製代碼
一樣,若是捕獲到的是當前對象的成員變量對象,一樣也會形成對self的引用,好比下面的代碼,Block使用了self對象的的成員變量name,實際上就是捕獲了self,對於編譯器來講name只不過期對象用結構體的成員變量:
@interface TestObject : NSObject
@property(nonatomic, copy) void (^blk)(void);
@property(nonatomic, copy) NSString *name;
@end
@implementation TestObject
- (instancetype)init {
self = [super init];
if (self) {
self.blk = ^{
NSLog(@"%@", self.name);
};
}
return self;
}
@end
複製代碼
解決循環引用的方法有兩種:
使用__weak來聲明self
- (instancetype)init {
self = [super init];
if (self) {
__weak typeof(self) weakSelf = self;
self.blk = ^{
NSLog(@"%@", weakSelf.name);
};
}
return self;
}
複製代碼
使用臨時變量來避免引用self
- (instancetype)init {
self = [super init];
if (self) {
id tmp = self.name;
self.blk = ^{
NSLog(@"%@", tmp);
};
}
return self;
}
複製代碼
使用__weak修飾符修飾對象以後,在Block中對對象就是弱引用: