雖然網(wǎng)上Block已經(jīng)被寫爛了,自己還是覺得棋电,無論如何自己抱著求真的態(tài)度自己看一遍源碼。之前介紹過Block的基本使用Block苇侵。這一片將深入介紹一下Block的具體實現(xiàn)离陶。
真沒想到自己一直認為自己做了這么久的iOS不應(yīng)該再去糾結(jié)Block這樣的知識點,回頭一看衅檀,知識還是得溫故而知新招刨。
測試代碼
分析思路:使用clang -rewrite-objc
將含有Block的.m
文件翻譯為.cpp
。對照著翻譯之后的源碼進行分析哀军。
現(xiàn)在定義如下幾個block沉眶。
//沒有捕獲變量
void blockFunc0()
{
void (^block)(void) = ^{
NSLog(@"num ");
};
block();
}
//普通局部變量
void blockFunc1()
{
int num = 100;
void (^block)(void) = ^{
NSLog(@"num equal %d", num);
};
num = 200;
block();
}
//普通__block局部變量
void blockFunc2()
{
__block int num = 100;
void (^block)(void) = ^{
NSLog(@"num equal %d", num);
};
num = 200;
block();
}
//全局變量
int num = 100;
void blockFunc3()
{
void (^block)(void) = ^{
NSLog(@"num equal %d", num);
};
num = 200;
block();
}
//靜態(tài)變量
void blockFunc4()
{
static int num = 100;
void (^block)(void) = ^{
NSLog(@"num equal %d", num);
};
num = 200;
block();
}
上面是準備的測試代碼
普通局部變量(blockFunc1)
blockFunc1和blockFunc0翻譯之后的差距就只是__blockFunc0_block_impl_0和__blockFunc1_block_impl_0結(jié)構(gòu)體中多了num這個字段,其余的都一樣杉适。所以這幾直接從blockFunc1講起谎倔。
執(zhí)行完clang -rewrite-objc
命令之后會得到不少的警告,最終翻譯出來的文件比較大猿推。因為翻譯之后方法的名字不會變片习,所以可以通過搜索相關(guān)的方法名就能快速找到翻譯之后方法對應(yīng)的位置。
blockFunc1翻譯為(注意對比翻譯前后的結(jié)果)
void blockFunc1()
{
int num = 100;
void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
num = 200;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
根據(jù)上面的結(jié)果可以提取出如下幾個重要的數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)
__block_impl
定義block的結(jié)構(gòu)體蹬叭,根據(jù)定義可以知道Block實際上就是對象藕咏,保存了一個ISA指針。那么如果是對象的話就很順利成長的把block的內(nèi)存秽五,生命周期管理同對象關(guān)聯(lián)起來孽查。目前block_impl的isa指針有_NSConcreteStackBlock、_NSConcreteGlobalBlock坦喘、_NSConcreteMallocBlock
struct __block_impl {
void *isa;//表明Block實際上是個對象
int Flags;
int Reserved;
void *FuncPtr;
};
字段對應(yīng)的含義
字段名 | 含義 |
---|---|
isa | isa 指向?qū)嵗龑ο竺ぴ伲砻?block 也是一個 Objective-C 對象。block 有三種類型:_NSConcreteStackBlock瓣铣、_NSConcreteGlobalBlock答朋、_NSConcreteMallocBlock,isa 對應(yīng)有三種值棠笑。因為block是對像梦碗,則就有對象的內(nèi)存管理及生命周期管理。恰恰三個對應(yīng)的值也就是這個作用。 |
Flags | 按位表示一些block的附加信息 |
Reserved | 保留變量 |
FuncPtr | 函數(shù)指針叉弦,指向具體的block實現(xiàn)的函數(shù)調(diào)用地址 |
__blockFunc1_block_desc_0
保存對block的一些描述信息丐一,比如保留字段大小藻糖,以及block結(jié)構(gòu)大小淹冰。并且這里定義__blockFunc1_block_desc_0_DATA
,并且還初始化了巨柒∮K可以看到初始化的情況下reserved為0,Block_size就是__blockFunc1_block_impl_0(包含了兩種結(jié)構(gòu)體的結(jié)構(gòu)體洋满,下面會介紹)大小晶乔。
static struct __blockFunc1_block_desc_0 {
size_t reserved;
size_t Block_size;
} __blockFunc1_block_desc_0_DATA = { 0, sizeof(struct __blockFunc1_block_impl_0)};
字段名 | 含義 |
---|---|
reserved | 保留字段的大小 |
Block_size | block結(jié)構(gòu)大小 |
__blockFunc1_block_impl_0
保存了block相關(guān)的信息,是前面兩種結(jié)構(gòu)體的結(jié)合體牺勾,包含了捕獲的外部變量正罢。比如這里的num,初始化的時候會初始化block_imp內(nèi)部變量驻民,如函數(shù)指針翻具,block_imp的對象類型。
struct __blockFunc1_block_impl_0 {
struct __block_impl impl;
struct __blockFunc1_block_desc_0* Desc;
int num;//定義的變量
//初始化傳入?yún)?shù)有函數(shù)指針回还,block的秒速
__blockFunc1_block_impl_0(void *fp, struct __blockFunc1_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
字段對應(yīng)的含義
字段名 | 含義 |
---|---|
impl | block_imp結(jié)構(gòu)體信息 |
Desc | 對block的附加描述 |
*** |
對外部捕獲的變量 |
方法翻譯
Block內(nèi)容
Block里面的具體內(nèi)容被翻譯為了__blockFunc1_block_func_0
函數(shù)裆泳,注意int num = __cself->num; // bound by copy
表明了num的值是值接拷貝過去的。這一點將會隨著捕獲外部變量的作用域不同而不同柠硕,后面會總結(jié)工禾。
static void __blockFunc1_block_func_0(struct __blockFunc1_block_impl_0 *__cself) {
int num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8f_84gy29_n1dvdwnqfkytdv4nw0000gn_T_BlockTest_581040_mi_1, num);
}
Block調(diào)用
這就是OC中的blockFunc1最終被翻譯的結(jié)果。
void blockFunc1()
{
int num = 100;
void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
num = 200;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
注意void在C++中的一些不同蝗柔,比如void*表示“任意類型的指針”或表示“該指針與一地址值相關(guān)闻葵,但是不清楚在此地址上的對象的類型”。
癣丧,可以簡單的理解為泛型的表示笙隙。 最終block被翻譯為了函數(shù)指針。
因為在__blockFunc1_block_impl_0
創(chuàng)建的時候設(shè)置了最后一個參數(shù)flags=0
的默認值坎缭,所以上面在創(chuàng)建__blockFunc1_block_impl_0
沒有傳入?yún)?shù)flags竟痰。
這里還需要注意的就是使用了C++中的結(jié)構(gòu)體強轉(zhuǎn)類型。對應(yīng)的代碼的就是
__blockFunc1_block_func_0
轉(zhuǎn)為__block_impl
類型掏呼。因為C++中坏快,只要高地址的數(shù)據(jù)類型相同(也就是首地址相同)就可以實現(xiàn)強轉(zhuǎn)。因為在__blockFunc1_block_func_0
結(jié)構(gòu)體中憎夷。第一個數(shù)據(jù)類型就是__block_impl
所以可以實現(xiàn)強轉(zhuǎn)莽鸿。
__block局部變量(blockFunc2)
上面通過了第一個例子分析了整個過程。后面加與不加block其實實現(xiàn)內(nèi)容都一樣。下面列舉幾個不同點祥得。
如果使用__block
修飾變量兔沃,則在生成為__blockFunc2_block_impl_0
的時候?qū)ν獠孔兞康男揎椃灰粯印>唧w來講對應(yīng)到下面的__Block_byref_num_0 *num; // by ref
级及。同時在初始化__blockFunc2_block_impl_0
的時候num會初始化為_num->__forwarding
乒疏。
struct __blockFunc2_block_impl_0 {
struct __block_impl impl;
struct __blockFunc2_block_desc_0* Desc;
__Block_byref_num_0 *num; // by ref
__blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
數(shù)據(jù)結(jié)構(gòu)
_block_imp的結(jié)構(gòu)同上面一樣。
__Block_byref_num_0
和上面不同饮焦,但是多了一個__Block_byref_num_0怕吴。他的定義如下。
struct __Block_byref_num_0 {
void *__isa;
__Block_byref_num_0 *__forwarding;
int __flags;
int __size;
int num;
};
其中各個字段含義如下:
字段名 | 含義 |
---|---|
__isa | 對象指針 |
__forwarding | 指向自己的指針 |
__flags | 標志位 |
__size | 結(jié)構(gòu)體大小 |
num | 外部變量 |
__blockFunc2_block_impl_0
相比之前多了一個__Block_byref_num_0字段县踢。該字段在初始化的時候就已經(jīng)賦值了转绷。
struct __blockFunc2_block_impl_0 {
struct __block_impl impl;
struct __blockFunc2_block_desc_0* Desc;
__Block_byref_num_0 *num; // by ref
__blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__blockFunc2_block_desc_0
static struct __blockFunc2_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __blockFunc2_block_impl_0*, struct __blockFunc2_block_impl_0*);
void (*dispose)(struct __blockFunc2_block_impl_0*);
} __blockFunc2_block_desc_0_DATA = { 0, sizeof(struct __blockFunc2_block_impl_0), __blockFunc2_block_copy_0, __blockFunc2_block_dispose_0};
相對于blockFunc1的翻譯結(jié)果多了一個copy的方法以及一個dispose方法,并且在初始化的時候就確定了這兩個方法硼啤。
后面會知道這兩個方法起的作用就是內(nèi)存管理的作用议经。
__blockFunc2_block_copy_0
static void __blockFunc2_block_copy_0(struct __blockFunc2_block_impl_0*dst, struct __blockFunc2_block_impl_0*src) {_Block_object_assign((void*)&dst->num, (void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
找到了_Block_object_assign的函數(shù)聲明為void _Block_object_assign(void *, const void *, const int);
。當block field是指針類型的時候就會發(fā)生拷貝谴返。可以很明確的看到進行拷貝的其實是__blockFunc2_block_impl_0結(jié)構(gòu)體中的__Block_byref_num_0煞肾。
__blockFunc2_block_dispose_0
static void __blockFunc2_block_dispose_0(struct __blockFunc2_block_impl_0*src) {_Block_object_dispose((void*)src->num, 8/*BLOCK_FIELD_IS_BYREF*/);}
同樣最終釋放的也是__blockFunc2_block_impl_0
中的__Block_byref_num_0
。所以__Block_byref_num_0
這個對象非常重要亏镰。
方法翻譯
Block內(nèi)容
追憶這里直接傳遞的是__Block_byref_num_0結(jié)構(gòu)體指針扯旷。在這個結(jié)構(gòu)體指針里面保存了之前的外部變量。使用到外部變量的時候直接從結(jié)構(gòu)體里面獲取索抓。
static void __blockFunc2_block_func_0(struct __blockFunc2_block_impl_0 *__cself) {
__Block_byref_num_0 *num = __cself->num; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8f_84gy29_n1dvdwnqfkytdv4nw0000gn_T_BlockTest_581040_mi_2, (num->__forwarding->num));
}
block調(diào)用
void blockFunc2()
{
__attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 100};
void (*block)(void) = ((void (*)())&__blockFunc2_block_impl_0((void *)__blockFunc2_block_func_0, &__blockFunc2_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
(num.__forwarding->num) = 200;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
首先創(chuàng)建了一個__Block_byref_num_0結(jié)構(gòu)體钧忽。里面記錄了自己的指針以及結(jié)構(gòu)體大小和外部變量的值100。在給外部變量賦值的時候使用了(num.__forwarding->num) = 200;
逼肯,也通過指針的方式復制耸黑。這樣就達到改變外部變量值得目的。
情況分析
根據(jù)上面的內(nèi)容篮幢,當在使用__block修飾變量之后涩堤,之前直接出現(xiàn)外部變量(這里的num)的地方都被__Block_byref_num_0這個結(jié)構(gòu)體所替換換队询。并且在此基礎(chǔ)上還多了copy以及dispose方法。最終也是通過__Block_byref_num_0實現(xiàn)的內(nèi)存管理。
copy和dispose的作用如下:
當blockFunc2從棧上被copy到堆上時蕴坪,會調(diào)用__blockFunc2_block_copy_0將blockFunc2類型的成員變量num(具體來講應(yīng)該是__Block_byref_num_0)從棧上復制到堆上蹬挺,而這個時候__forwarding指針就會指向堆上的結(jié)構(gòu)體缭乘;
當blockFunc2被釋放時身坐,相應(yīng)地會調(diào)用__blockFunc2_block_dispose_0來釋放blockFunc2類型的成員變量num(具體來講應(yīng)該是__Block_byref_num_0)。同樣__forwarding指針指向堆上的結(jié)構(gòu)體也就被釋放蛋叼。
為什么要這么做呢焊傅?其實也是為了保證block能夠訪問有效的正確內(nèi)存區(qū)域剂陡。
因為blockFunc2函數(shù)中的局部變量num和函數(shù)__blockFunc2_block_impl_0不在同一個作用域中,調(diào)用過程中只是進行了值傳遞狐胎。當然鸭栖,在上面代碼中,我們可以通過指針來實現(xiàn)局部變量的修改握巢。不過這是由于在調(diào)用__blockFunc2_block_impl_0時晕鹊,blockFunc2函數(shù)棧還沒展開完成,變量num還在棧中镜粤。但是在很多情況下捏题,block是作為參數(shù)傳遞以供后續(xù)回調(diào)執(zhí)行的玻褪。通常在這些情況下肉渴,block被執(zhí)行時,定義時所在的函數(shù)棧已經(jīng)被展開带射,局部變量已經(jīng)不在棧中了已經(jīng)被銷毀了同规,再用指針訪問就會報常見的壞內(nèi)存訪問。
因為block可以作為屬性窟社,并且也經(jīng)常作為參數(shù)傳遞券勺,而Block最終展開的是一個函數(shù),展開的函數(shù)里面的變量作用域和被block被調(diào)用的函數(shù)作用域是不同的灿里。通常在情況下关炼,block被執(zhí)行時,定義時所在的函數(shù)棧已經(jīng)被展開匣吊,局部變量已經(jīng)不在棧中了已經(jīng)被銷毀了儒拂,再用指針訪問就會報常見的壞內(nèi)存訪問。因此block有copy方法將其在堆上色鸳,所以在block被copy的同時社痛,將局部變量間接也copy放在堆上就能夠保證局部變量可以被block正常訪問到。具體來講可以看看下面這張圖命雀,最終是通過__forwarding指針來實現(xiàn)這個目的:
全局變量(blockFunc3)
這種情況下轉(zhuǎn)換的結(jié)果block和不捕獲任何變量的block結(jié)果是一樣的蒜哀。
struct __blockFunc0_block_impl_0 {
struct __block_impl impl;
struct __blockFunc0_block_desc_0* Desc;
__blockFunc0_block_impl_0(void *fp, struct __blockFunc0_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
int num = 100;//注意,這里已經(jīng)聲明了一個num為全局變量
struct __blockFunc3_block_impl_0 {
struct __block_impl impl;
struct __blockFunc3_block_desc_0* Desc;
__blockFunc3_block_impl_0(void *fp, struct __blockFunc3_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
因為是全局變量吏砂,所以任何地方都可以修改撵儿。跟block沒什么關(guān)系(因為block最終也是轉(zhuǎn)換為函數(shù)來調(diào)用)。
static局部變量(blockFunc4)
這種情況下重點看一下如下幾點:
__blockFunc4_block_impl_0中保持的是int *num;
也即是指針狐血。
struct __blockFunc4_block_impl_0 {
struct __block_impl impl;
struct __blockFunc4_block_desc_0* Desc;
int *num;
__blockFunc4_block_impl_0(void *fp, struct __blockFunc4_block_desc_0 *desc, int *_num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
block調(diào)用被轉(zhuǎn)換為:
void blockFunc4()
{
static int num = 100;
void (*block)(void) = ((void (*)())&__blockFunc4_block_impl_0((void *)__blockFunc4_block_func_0, &__blockFunc4_block_desc_0_DATA, &num));
num = 200;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
注意這里聲明了static int num = 100;
所以傳遞的是指針淀歇,和blockFunc1中最大的不同就是int num 變成了int *num
由整型變成了整型指針類型,注意:這個不再是傳值氛雪,而是傳地址房匆,這說明對應(yīng) static 修飾的自動變量值在被 block 截獲之后仍可以與外部自動變量保持同步,因為它們的地址是同一個。
Block內(nèi)存管理
Block一共有三種類型浴鸿,三面介紹的幾種block全是_NSConcreteGlobalBlock井氢。
_NSConcreteGlobalBlock
經(jīng)過測試只有當 block 字面量寫在全局作用域時,即為 global block岳链。而且僅此一種花竞,網(wǎng)上有人當 block 字面量不獲取任何外部變量時也是global block,但是經(jīng)過自己測試還是_NSConcreteStackBlock類型掸哑。
globalblock如下形式:
_NSConcreteStackBlock
這種類型block是最多的一種约急,處于內(nèi)存的棧區(qū),如果其變量作用域結(jié)束苗分,這個 block 就被廢棄厌蔽,block 上的 __block 變量也同樣會被廢棄。
block 提供了 copy 的功能摔癣,將 block 和 __block 變量從椗拷貝到堆,就是 _NSConcreteMallocBlock择浊。
_NSConcreteMallocBlock
當 block 從棿鞑罚拷貝到堆后,當棧上變量作用域結(jié)束時琢岩,仍然可以繼續(xù)使用 block
堆上的 block 類型為 _NSConcreteMallocBlock投剥,所以會將 _NSConcreteMallocBlock 寫入 isa。對應(yīng)到代碼上就是impl.isa = &_NSConcreteMallocBlock
担孔。
ARC 下的 block
在開啟 ARC 時江锨,大部分情況下編譯器通常會將創(chuàng)建在棧上的 block 自動拷貝到堆上。當
block 作為函數(shù)的參數(shù)傳遞時攒磨,編譯器不會自動調(diào)用 copy 方法
如下這幾種情況都不用手動拷貝
- 當 block 作為函數(shù)返回值返回時泳桦,編譯器自動將 block 作為 _Block_copy 函數(shù),效果等同于 block 直接調(diào)用 copy 方法娩缰;
- 當 block 被賦值給 __strong id 類型的對象或 block 的成員變量時灸撰,編譯器自動將 block 作為 _Block_copy 函數(shù),效果等同于 block 直接調(diào)用 copy 方法拼坎;
- 當 block 作為參數(shù)被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時浮毯。這些方法會在內(nèi)部對傳遞進來的 block 調(diào)用 copy 或 _Block_copy 拷貝;
比如自動拷貝的情況:
/************ ARC下編譯器自動拷貝block ************/
typedef int (^blk_t)(int);
blk_t func(int rate)
{
return ^(int count){return rate * count;};
}
如果沒有自動拷貝,因為外部傳入的參數(shù)放到的是棧上泰鸡,如果后面去調(diào)用這個返回的block肯定會發(fā)生異常债蓝,但是在ARC下面不會出問題,于是翻譯一下盛龄∈渭#可以查到如下代碼
blk_t func(int rate)
{
blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}
由于 block 字面量是創(chuàng)建在棧內(nèi)存芳誓,通過 objc_retainBlock() 函數(shù)拷貝到堆內(nèi)存,讓 tmp 重新指向堆上的 block啊鸭,然后將 tmp 所指的堆上的 block 作為一個 Objective-C 對象放入 autoreleasepool 里面锹淌,從而保證了返回后的 block 仍然可以正確執(zhí)行。
需要手動執(zhí)行copy的block:
/************ ARC下編譯器手動拷貝block ************/
id getBlockArray()
{
int val = 10;
return [[NSArray alloc] initWithObjects:
^{NSLog(@"blk0:%d", val);},
^{NSLog(@"blk1:%d", val);}, nil];
}
這里block最為了函數(shù)參數(shù)赠制,編譯器不會自動拷貝赂摆。所以在調(diào)用這個方法的時候會出現(xiàn)異常。
總結(jié)
Block原理總算是講完了钟些。其難點就是對于外部變量的捕獲場景比較多烟号。通過分析源碼,比較麻煩的就是加了__block的情況政恍。通過__forwarding指針達到棧和堆上面的切換汪拥。這里沒有列舉對象類型的例子原因是對象類型其實就是一個指針。簡單來講就是把上面的num換成指針類型而已抚垃,其余的規(guī)則都是一樣的喷楣。
__block的作用可以簡單總結(jié)為趟大,處理兩個函數(shù)作用域訪問訪問變量的時候鹤树,防止壞內(nèi)存訪問。
后面講了Block的幾種類型以及相關(guān)內(nèi)存管理逊朽,同堆棧一樣罕伯。最后將了ARC環(huán)境下Block的注意事項。