一、Block本質(zhì)
Block是“帶有自動(dòng)變量值的匿名函數(shù)”。
所謂的匿名函數(shù)就是不帶有名稱的函數(shù)
typedef int (^blk_t)(int)
blk_t = ^(int count){
return count+1;
}
但它究竟是什么呢?
轉(zhuǎn)碼
通過(guò)-rewrite-objc選項(xiàng)將含有Block語(yǔ)法的源代碼變換為C++代碼
變換前:
#include<stdio.h>
int main() {
void (^blk)(void) = ^{printf("Block");};
blk();
return 0;
}
終端:clang -rewrite-objc 源代碼文件名
變換后:(變換后有568行,精簡(jiǎn)后如下)
struct __block_impl {
void *isa; //isa指針
int Flags; //標(biāo)志位
int Reserved;
void *FuncPtr; //函數(shù)指針
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; //block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc; //block描述信息
//構(gòu)造函數(shù)(類似于OC的init方法),返回結(jié)構(gòu)體對(duì)象
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; //isa指針
impl.Flags = flags; //標(biāo)志位
impl.FuncPtr = fp;//函數(shù)指針
Desc = desc; //block描述信息
}
};
/*
^{printf("Block");};變換后的樣子
Block匿名函數(shù)實(shí)際上被作為簡(jiǎn)單的C函數(shù)來(lái)處理
函數(shù)名的命名規(guī)則:根據(jù)Block語(yǔ)法所在的函數(shù)名(此處為mian)和該Block語(yǔ)法在該函數(shù)出現(xiàn)的順序值(此處為0)來(lái)命名的
__cself相當(dāng)于OC中的self
*/
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block");
}
int main() {
/*
void (^blk)(void) = ^{printf("Block");};轉(zhuǎn)換后的代碼
構(gòu)造函數(shù)構(gòu)造后,__main_block_impl_0結(jié)構(gòu)體結(jié)果如下
isa = &_NSConcreteStackBlock;
Flags = 0;
FuncPtr = __main_block_func_0; //函數(shù)指針,指向__main_block_func_0函數(shù)
Desc = &__main_block_desc_0_DATA;
*/
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
/*
blk();轉(zhuǎn)換后的代碼如下
*/
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
分析
1、分析void (^blk)(void) = ^{printf("Block");}
上面C++代碼
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
去掉轉(zhuǎn)換后的部分如下
//創(chuàng)建一個(gè)結(jié)構(gòu)體實(shí)例
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA));
//將結(jié)構(gòu)體實(shí)例的指針賦給blk
struct __main_block_impl_0 * blk = &tmp;
通過(guò)簡(jiǎn)化后的代碼可知,源代碼將__main_block_impl_0結(jié)構(gòu)體類型的自動(dòng)變量,即棧上生成的__main_block_impl_0結(jié)構(gòu)體實(shí)例的指針,賦值給__main_block_impl_0結(jié)構(gòu)體指針類型的變量blk。
而最初的Block源代碼是void (^blk)(void) = ^{printf("Block");};
因此,將Block語(yǔ)法生成的Block賦給Block類型變量blk。它等同于將__main_block_impl_0結(jié)構(gòu)體實(shí)例的指針賦給變量blk。
堆:動(dòng)態(tài)分配內(nèi)存,需要程序員自己申請(qǐng),程序員自己管理
棧:自動(dòng)分配內(nèi)存,自動(dòng)銷毀,先入后出,棧上的內(nèi)容存在自動(dòng)銷毀的情況
2、分析blk()
blk();轉(zhuǎn)換后的代碼如下:
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
去掉轉(zhuǎn)換后的部分如下:
(*blk->impl.FuncPtr)(blk);
可見(jiàn),這就是簡(jiǎn)單地使用函數(shù)指針調(diào)用函數(shù)。__main_block_func_0的函數(shù)指針被賦值到成員變量FuncPtr中。另外也說(shuō)明了,__main_block_func_0函數(shù)的參數(shù)__cself指向Block值。在調(diào)用該函數(shù)的源代碼中可以看出Block正是作為參數(shù)進(jìn)行傳遞。
__main_block_impl_0結(jié)構(gòu)體相當(dāng)于基于objc_object結(jié)構(gòu)體的Objective-C類對(duì)象的結(jié)構(gòu)體。
_NSConcreteStackBlock相當(dāng)于class_t結(jié)構(gòu)體實(shí)例。在將Block作為Objective-C對(duì)象處理時(shí),關(guān)于該類的信息放置于_NSConcreteStackBlock中。
因此,Block本質(zhì)上也是一個(gè)OC對(duì)象(最終繼承NSObject),它內(nèi)部也有個(gè)isa指針。
二、Block捕獲變量值
Block是帶有自變量的匿名函數(shù),其中的"帶有自變量值"是什么意思呢?"帶有自變量值"在block中表現(xiàn)為"捕獲自變量值"。
為了保證Block內(nèi)部能夠正常訪問(wèn)外部的變量,Block有個(gè)變量捕獲機(jī)制:
局部變量(auto):捕獲到Block內(nèi),值傳遞;
局部變量(static):捕獲到Block內(nèi),指針傳遞;
全局變量:不捕獲到Block內(nèi),直接訪問(wèn);
Q:下列代碼輸出值分別為多少?
int val = 10;
const char * fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt,val);
};
val = 2;
fmt = "Change the value of val,val = %d\n";
blk();
輸出結(jié)果為:val = 10
原因:在執(zhí)行Block語(yǔ)法時(shí),會(huì)捕獲自動(dòng)變量值,即Block語(yǔ)法表達(dá)式所使用到的自動(dòng)變量值被保存到Block的結(jié)構(gòu)體實(shí)例中。
源碼證明:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int val = __cself->val; // bound by copy
printf(fmt,val);
}
int main() {
int val = 10;
const char * fmt = "val = %d\n";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
val = 2;
fmt = "Change the value of val,val = %d\n";
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
Q:下列代碼輸出值分別為多少?
auto int age = 10;
static int num = 25;
void (^Block)(void) = ^{
printf("age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();
輸出結(jié)果為:age:10,num:11
原因:auto變量Block訪問(wèn)方式是值傳遞,static變量Block訪問(wèn)方式是指針傳遞
源碼證明:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_num, int flags=0) : age(_age), num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *num = __cself->num; // bound by copy
printf("age:%d,num:%d",age,(*num));
}
int main() {
auto int age = 10;
static int num = 25;
void (*Block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &num));
age = 20;
num = 11;
((void (*)(__block_impl *))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block);
return 0;
}
上述代碼可知static修飾的變量,是根據(jù)指針訪問(wèn)的
Q:為什么block對(duì)auto和static變量捕獲有差異?
auto自動(dòng)變量可能會(huì)銷毀的,內(nèi)存可能會(huì)消失,不采用指針訪問(wèn);static變量一直保存在內(nèi)存中,指針訪問(wèn)即可
三、__block說(shuō)明符
__block說(shuō)明符類似于static、auto和register說(shuō)明符,它們用于指定將變量值設(shè)置到哪個(gè)存儲(chǔ)域中(參見(jiàn)下文)。如auto表示作為自動(dòng)變量存儲(chǔ)在棧中,static表示作為靜態(tài)變量存儲(chǔ)在數(shù)據(jù)區(qū)中。
自變量被Block截獲后,Block保存的是當(dāng)前的瞬間值,保存后就不能修改該值。若想在Block語(yǔ)法中修改捕獲到的自變量的值,則需要在該值變量前加上__block說(shuō)明符,如果不加該說(shuō)明符,則運(yùn)行會(huì)報(bào)錯(cuò)
__block int val = 0;
void (^blk)(void) = ^{
val = 1; //修改捕獲到的自變量的值
};
blk();
printf("val = %d\n",val);
系統(tǒng)對(duì)__block int val = 0;做了什么?
編譯器會(huì)將__block變量包裝成一個(gè)對(duì)象,具體的C++代碼如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding; //val的地址
int __flags;
int __size;
int val; //val的值
};
從C++代碼可知,結(jié)構(gòu)體持有相當(dāng)于原自動(dòng)變量的成員變量。通過(guò)成員變量__forwarding訪問(wèn)成員變量val。(成員變量val是該實(shí)例自身持有的變量,它相當(dāng)于原自動(dòng)變量本身),如下圖:

因此,加了__block修飾的變量,Block截獲后是通過(guò)指針去操作該變量,因此可以修改變量的值。
棧上__block的__forwarding指向本身
棧上__block復(fù)制到堆上后,棧上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身

捕獲OC對(duì)象(不用__block修飾),調(diào)用變更對(duì)象的方法是可以的,而向捕獲的變量賦值則會(huì)產(chǎn)生編譯錯(cuò)誤
id arr = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[arr addObject:obj]; //這樣是可以的
arr = [[NSMutableArray alloc] init]; //這樣是不行的(編譯報(bào)錯(cuò))
}
現(xiàn)在的Block中,捕獲自變量的方法并沒(méi)有實(shí)現(xiàn)對(duì)C語(yǔ)言數(shù)組的截獲,因此在訪問(wèn)C語(yǔ)言數(shù)組時(shí)會(huì)產(chǎn)生編譯錯(cuò)誤,可以通過(guò)使用指針解決該問(wèn)題
const char text[] = "hello";
void (^blk)(void) = ^{
printf("%c\n",text[2]); //這樣使用時(shí)不行的
};
const char *text = "hello";
void (^blk)(void) = ^{
printf("%c\n",text[2]); //改成指針可以
};
__block總結(jié)
- __block可以用于解決block內(nèi)部無(wú)法修改auto變量值的問(wèn)題
- __block不能修飾全局變量、靜態(tài)變量(static)
- 當(dāng)__block變量在棧上時(shí),不會(huì)對(duì)指向的對(duì)象產(chǎn)生強(qiáng)引用
- 編譯器會(huì)將__block變量包裝成一個(gè)對(duì)象
- __block修改變量:
age->forwarding->age - __Block_byref_val_0結(jié)構(gòu)體內(nèi)部地址和外部變量val是同一地址
四、Block類型
Block的類型取決于isa指針,可以通過(guò)調(diào)用class方法查看具體類型,最終都繼承自NSBlock。
- __NSGlobalBlock __ ( _NSConcreteGlobalBlock )
- __NSStackBlock __ ( _NSConcreteStackBlock )
- __NSMallocBlock __ ( _NSConcreteMallocBlock )
代碼示例
void (^block1)(void) = ^{
NSLog(@"block1");
};
NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
NSLog(@"%@",[[[[[block1 class] superclass] superclass] superclass] superclass]);
輸出結(jié)果:
NSGlobalBlock
__NSGlobalBlock
NSBlock
NSObject
null
上述代碼輸出了block1的類型,也證實(shí)了block是對(duì)象,最終繼承NSObject
代碼展示block的三種類型:
/*
全局block
沒(méi)有訪問(wèn)auto變量的block是__NSGlobalBlock__,放在數(shù)據(jù)段
因?yàn)樵谑褂萌肿兞康牡胤讲荒苁褂米詣?dòng)變量,所有不存在對(duì)自動(dòng)變量進(jìn)行截獲
由于此Block結(jié)構(gòu)體實(shí)例的內(nèi)容不依賴于執(zhí)行時(shí)的狀態(tài),所以整個(gè)程序中只需一個(gè)實(shí)例
因此,將Block結(jié)構(gòu)體實(shí)例設(shè)置在與全局變量相同的數(shù)據(jù)區(qū)域中即可
*/
void (^blk1)(void) = ^{ NSLog(@"blk1"); };
NSLog(@"%@",[blk1 class]);
/*
堆block
將Block賦值給__strong指針時(shí),ARC環(huán)境下,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的Block復(fù)制到堆上
如果void (^blk2)(void) = ^{ 寫成 void __weak (^blk2)(void) = ^{
則是棧block(編譯器沒(méi)有將其復(fù)制到堆上)
*/
int age = 1;
void (^blk2)(void) = ^{
NSLog(@"blk2:%d",age);
};
NSLog(@"%@",[blk2 class]);
/*
棧block
訪問(wèn)了變量,并且沒(méi)有做copy操作
*/
NSLog(@"%@",[^{
NSLog(@"blk3:%d",age);
} class]);
輸出結(jié)果:
NSGlobalBlockNSMallocBlock
NSStackBlock
__NSGlobalBlock __ 在數(shù)據(jù)區(qū)
__NSMallocBlock __ 在堆區(qū)
__NSStackBlock __ 在棧區(qū)
堆:動(dòng)態(tài)分配內(nèi)存,需要程序員自己申請(qǐng),程序員自己管理
-
棧:自動(dòng)分配內(nèi)存,自動(dòng)銷毀,先入后出,棧上的內(nèi)容存在自動(dòng)銷毀的情況
Block存儲(chǔ)域.png
如何判斷Block是哪種類型 沒(méi)有訪問(wèn)auto變量的block是__NSGlobalBlock __ ,放在數(shù)據(jù)段
訪問(wèn)了auto變量的block是__NSStackBlock __
[__NSStackBlock __ copy]操作就變成了__NSMallocBlock __
在ARC環(huán)境下,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的Block復(fù)制到堆上的情況有如下幾種
- Block作為函數(shù)返回值返回時(shí),編譯器會(huì)自動(dòng)生成復(fù)制到堆上的代碼;
- 將Block賦值給__strong指針時(shí);
- Cocoa框架的方法,并且方法名中包含有usingBlock等時(shí);
- Block作為GCD API的方法參數(shù)時(shí);
如在使用NSArray類的enumerateObjectsUsingBlock實(shí)例方法以及dispatch_async函數(shù)時(shí),不用手動(dòng)復(fù)制。相反
在NSArray類的initWithObjects實(shí)例方法上傳遞Block時(shí)需要手動(dòng)復(fù)制。
備注:將Block從棧上復(fù)制到堆上是相當(dāng)消耗CPU的。
對(duì)每種類型Block調(diào)用copy操作后是什么結(jié)果?

不管Block配置在何處,用copy方法復(fù)制都不會(huì)引起任何問(wèn)題,在不確定時(shí)調(diào)用copy方法即可。
__block變量存儲(chǔ)域

若一個(gè)Block中使用
__block變量,則當(dāng)該Block從棧復(fù)制到堆時(shí),由于其使用的所有__block變量也必定配置在棧上。所以這些__block變量也會(huì)一并從棧復(fù)制到堆,并且被該Block所持有。
當(dāng)多個(gè)Block使用同一個(gè)
__block變量時(shí),因?yàn)锽lock最先是配置在棧上的,所以__block變量也都配置在棧上。當(dāng)其中一個(gè)Block被復(fù)制到堆上時(shí),__block變量也會(huì)一并從棧復(fù)制到堆,并被Block所持有。當(dāng)其他的Block從棧復(fù)制到堆時(shí),被復(fù)制的Block持有__block變量,并增加__block變量的引用計(jì)數(shù)。如下圖:
如果配置在堆上的Block被廢棄,那么它所使用的
__block變量也就被釋放。
MRC下Block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);
ARC下Block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);
Block的屬性修飾詞為什么是copy
Block需要通過(guò)copy才會(huì)被復(fù)制到堆上,只有在堆上,程序員才能對(duì)它做內(nèi)存管理、控制Block生命周期等操作。
五、Block循環(huán)引用
__strong強(qiáng)引用
__strong修飾符是id類型和對(duì)象類型,默認(rèn)的所有權(quán)修飾符,可以不寫
__weak弱引用
__weak弱引用,不持有對(duì)象,在超出其變量作用域時(shí)(如函數(shù)花括號(hào)之外),對(duì)象立即被釋放
__weak可以避免循環(huán)強(qiáng)引用
__weak在持有某對(duì)象的弱引用時(shí),若該對(duì)象被廢棄,則此弱引用將自動(dòng)失效,且被置為nil
__weak修飾符只能在iOS5以上使用
__unsafe_unretained
在iOS5以下用__unsafe_unretained修飾符
盡管ARC式的內(nèi)存管理是編譯器的工作,但符有__unsafe_unretained修飾符的變量不屬于編譯器的內(nèi)存管理對(duì)象。
賦值給__unsafe_unretained修飾的變量的對(duì)象,需確保對(duì)象不為空,否則會(huì)產(chǎn)生懸垂指針,導(dǎo)致運(yùn)行奔潰。
如下:
id __unsafe_unretained obj1 = nil
{
id __strong obj0 = [[NSObject alloc] init]; //obj0持有對(duì)象
obj1 = obj0; //雖然obj0變量賦給obj1,但obj1變量既不持有對(duì)象的強(qiáng)引用,也不持有對(duì)象的弱引用
NSLog(@"A:%@",obj1);
}
//obj0超出其作用域,強(qiáng)引用失效,所以自動(dòng)釋放自己持有的對(duì)象,因?yàn)閇[NSObject alloc] init]對(duì)象沒(méi)有持有者,所以廢棄該對(duì)象
NSLog(@"B:%@",obj1);
//obj1變量表示的對(duì)象已被廢棄(懸垂指針),因此訪問(wèn)出錯(cuò)
@autoreleasepool自動(dòng)釋放池
@autoreleasepool {
id __strong obj = [NSMutableArray array];
} //到這個(gè)花括號(hào)釋放池代碼塊結(jié)束,隨著@autoreleasepool塊的結(jié)束,注冊(cè)到autoreleasepool中的所有對(duì)象被自動(dòng)釋放
解決循環(huán)引用:
__weak:不會(huì)產(chǎn)生強(qiáng)引用,指向的對(duì)象銷毀時(shí),會(huì)自動(dòng)讓指針置為nil
MyObject * object = [[MyObject alloc] init];
object.age = 10;
__weak typeof(object) weakObject = object;
object.blk = ^{
NSLog(@"age is %d", weakObject.age);
};
object.blk();
__block:必須把引用對(duì)象置為nil,并且要調(diào)用該block
__block MyObject * object = [[MyObject alloc] init];
object.age = 10;
object.blk = ^{
NSLog(@"age is %d", object.age);
object = nil;
};
object.blk();
參考資料
Objective-C高級(jí)編程
