一、概述
閉包 = 一個函數(shù)『或指向函數(shù)的指針』 + 該函數(shù)執(zhí)行的外部的上下文變量 『也就是自由變量』;Block是Objective-C對閉包的實現(xiàn)。
其中,Block:
- 可以嵌套定義,定義Block方法和定義函數(shù)方法相似
- Block可以定義在方法內(nèi)部或外部
- 本質(zhì)是對象,使代碼高聚合
使用clang將OC代碼轉(zhuǎn)換為C++文件查看block的方法: - 在命令行輸入代碼
clang -rewrite-objc 需要編譯的oc文件.m - 這時查看當前的文件夾里多了一個相同的名稱的
.cpp文件,在命令行輸入open main.cpp查看文件
二、Block的定義與使用
1.無參數(shù)無返回值
void (^MyBlockOne)(void) = ^(void) {
};
MyBlockOne();//block調(diào)用
2.有參數(shù)無返回值
void (^MyBlockTwo)(int a) = ^(int a) {
};
MyBlockTwo(100);
3.有參數(shù)有返回值
int(^MyBlockThree)(int, int) = ^(int a, int b) {
return a + b;
};
MyBlockThree(12, 56);
4.無參數(shù),有返回值
int (^MyBlockFour)(void) = ^{
return 45;
};
MyBlockFour();
5.實際開發(fā)中常用typedef定義Block
例如,用typedef定義一個block:
typedef int (^MyBlock)(int, int);
這時,MyBlock就成為了一種Block類型,定義類的屬性時可以這樣:
@property (nonatomic, copy) MyBlock myBlockOne;
使用時:
self.myBlockOne = ^int (int, int) {
//TODO
}
三、Block與外界變量
截獲自動變量(局部變量)值
1.默認情況
對于block外的變量引用,block默認是將其復制到其數(shù)據(jù)結(jié)構(gòu)中來實現(xiàn)訪問的。也就是說block的自動變量截獲只針對block內(nèi)部使用的自動變量,不使用則不截獲,因為截獲的自動變量會存儲于block的結(jié)構(gòu)體內(nèi)部,會導致block體積變大,特別需要注意的是默認情況下block只能訪問不能修改局部變量的值。

int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
//輸出結(jié)果:
//age = 10;
2.__block修飾的外部變量
對于用__block修飾的外部變量引用,block是復制其引用地址來實現(xiàn)訪問的。block可以修改__block修飾的外部變量的值

__block int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
//輸出為:
//age = 18
為什么使用__block修飾的外部變量的值就可以被block修改呢?
我們使用clang將oc代碼轉(zhuǎn)換為c++文件:
clang -rewrite-objc 源代碼文件名
便可解開其真正面紗:
__block int val = 10;
//轉(zhuǎn)換成
__Block_byref_val_0 val = {
0,
&val,
0,
&sizeof(__Block_byref_val_0),
10
};
會發(fā)現(xiàn)一個局部變量加上__block修飾符后竟然跟block一樣變成了一個__Block_byref_val_0結(jié)構(gòu)體類型的自動變量實例。
此時我們在block內(nèi)部訪問val變量則需要通過一個叫__forwarding的成員變量來間接訪問val變量
四、Block的copy操作
1.Block的存儲域及copy操作
在開始研究Block的copy操作之前,先來思考一下:Block是存儲在棧上還是堆上呢?
我們先來看看一個由C/C++/OBJC編譯的程序占用內(nèi)存的結(jié)構(gòu):

其實,block有三種類型:
- 全局塊(_NSConcreteGlobalBlock)
- 棧塊(_NSConcreteStackBlock)
- 堆塊(_NSConcreteMallocBlock)
這三種block各自的存儲域如下圖:

- 全局塊存在于全局內(nèi)存中,相當于單利
- 棧塊存在于棧內(nèi)存中,超出其作用域馬上被銷毀
- 堆塊存在于堆內(nèi)存中,是一個帶引用計數(shù)的對象,需要自行管理其內(nèi)存
簡而言之,存儲在棧中的Block就是棧塊、存儲在堆中的就是堆塊、既不在棧中也不在堆中的塊就是全局塊。
遇到一個Block,我們怎么確定這個Block的存儲位置呢?
1.Block不訪問外界變量(包括棧中和堆中的變量),Block既不在棧又不在堆中,在代碼段中,ARC和MRC下都是如此,此時為全局塊。
2.Block訪問外界變量
MRC環(huán)境下:訪問外界變量的Block默認存儲在棧中。
ARC環(huán)境下:訪問外界變量的Block默認存儲在堆中,(實際是放在棧區(qū)的,然后ARC情況下自動又拷貝到堆區(qū)),自動釋放
ARC環(huán)境下,訪問外界變量的Block為什么要自動從棧區(qū)拷貝到堆區(qū)呢?
棧上的Block,如果其所屬的變量作用域結(jié)束,該Block就被廢棄,如同一般的自動變量,當然Block中的__block變量也同時被廢棄,如下圖:

為了解決棧塊在其變量作用域結(jié)束之后被廢棄(釋放)的問題,我們需要把Block復制到堆中,延長其生命周期。開啟ARC時,大多數(shù)情況下編譯器會恰當?shù)剡M行判斷是否有需要將Block從棧復制到堆,如果有,自動生成將Block從棧上復制到堆上的代碼。Block的復制操作執(zhí)行的是copy實例方法。Block只要調(diào)用了copy方法,棧塊就會變成堆塊。
如下圖:

例如下面一個返回值為Block類型的函數(shù):
typedef int (^blk_t) (int);
blk_t func(int rate) {
return ^(int count) {
return rate * count;
};
}
分析可知:上面的函數(shù)返回的Block是配置在棧上的,所以返回函數(shù)調(diào)用方式時,Block變量作用域就結(jié)束了,Block會被廢棄,但在ARC有效,這種情況編譯器會自動完成復制。
在非ARC環(huán)境下,則需要開發(fā)者調(diào)用copy方法手動復制,由于開發(fā)中幾乎都是ARC,所以手動復制內(nèi)容不再過多研究。
將Block從棧上復制到堆上相當消耗CPU,所以當Block設(shè)置在棧上也能夠使用時,就不要復制了,因為此時的復制只是在浪費CPU資源。
Block的復制操作執(zhí)行的是copy實例方法。不同類型的Block使用copy方法的效果如下表:

根據(jù)表得知,Block在堆中copy會造成引用計數(shù)增加,這與其它Objective-C對象是一樣的。雖然Block在棧中也是以對象身份存在,但是棧塊沒有引用計數(shù),因為不需要,我們都知道棧區(qū)的內(nèi)存由編譯器自動釋放。
不管Block存儲域在何處,用copy方法復制都不會引起任何問題,在不確定時調(diào)用copy方法即可。
在ARC有效時,多次調(diào)用copy方法完全沒有問題:
blk = [[[[blk copy] copy] copy] copy];
//經(jīng)過多次復制,變量blk仍然持有Block的強引用,該Block不會被廢棄
2.__block變量與__forwarding
在copy操作之后,既然__block變量也被copy到堆上去了,那么訪問該變量是訪問棧上的還是堆上的呢?

通過__forwarding,無論是在block中還是block外訪問__block變量,也不管該變量在棧上或堆上,都能順利地訪問同一個__block變量。
五、防止Block循環(huán)引用
Block循環(huán)引用的情況:
某個類將block作為自己的屬性變量,然后該類在block的方法體里面又使用了該類本身,如下:
self.someBlock = ^(Type var){
[self dosomething];
};
解決辦法:
1.ARC下:使用__weak
__weak typeof(self) weakSelf = self;
self.someBlock = ^(Type var) {
[weakSelf dosomething];
};
2.MRC下:使用__block
__block typeof(self) blockSelf = self;
self.someBlock = ^(Type var) {
[blockSelf dosomething];
};
值得注意的是,在ARC下,使用__block也有可能帶來循環(huán)引用的問題,如下:
//循環(huán)引用 self -> _attributBlock -> tmp -> self
typedef void (^Block)();
@interface TestObj : NSObject
{
Block _attributBlock;
}
@end
@implementation TestObj
- (id)init {
self = [super init];
__block id tmp = self;
self.attributBlock = ^{
NSLog(@"Self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock {
self.attributBlock();
}
@end
//使用類
id obj = [[TestObj alloc] init];
[obj execBlock];//如果不調(diào)用此方法,tmp永遠不會置nil,內(nèi)存泄露會一直在
六、Block的使用示例
1.Block作為變量(Xcode快捷鍵:inlineBlock)
//定義一個Block變量sum
int (^sum) (int, int);
//給Block變量賦值
//一般返回值省略:sum = ^(int a, int b)...
sum = ^int (int a, int b) {
return a + b;
};//賦值語句最后有分號
int a = sum(10, 20);//調(diào)用Block變量
2.Block作為屬性(Xcode快捷鍵:typedefBlock)
//1.給Calculate類型 sum變量 賦值『下定義』
typedef int (^Calculate)(int, int);//calculate就是類型名
Calculate sum = ^(int a, int b) {
return a + b;
};
int a = sum(10, 20);//調(diào)用sum變量
//2.作為對象的屬性聲明,copy后block會轉(zhuǎn)義到堆中和對象一起
@property (nonatomic, copy) Calculate sum; //使用typedef
@property (nonatomic, copy) int (^sum)(int, int); //不使用typedef
//聲明,類外
self.sum = ^(int a, int b) {
return a + b;
};
//調(diào)用,類內(nèi)
int a = self.sum(10, 20);
3.作為OC中的方法參數(shù)
//無參數(shù)傳遞的Block
- (CGFloat)testTimeConsume:(void (^)())middleBlock {
//執(zhí)行前記錄下當前時間
CFTimeInterval startTime = CACurrentMediaTime();
middleBlock();
//執(zhí)行后記錄下當前的時間
CFTimeInterval endTime = CACurrentMediaTime();
return endTime - startTime;
}
//調(diào)用
[self testTimeConsume:^{
//放入block中的代碼
}];
//有參數(shù)傳遞的Block
- (CGFloat)testTimeConsume:(void (^)(NSString *name))middleBlock {
//執(zhí)行前記錄下當前時間
CFTimeInterval startTime = CACurrentMediaTime();
middleBlock(name);
//執(zhí)行后記錄下當前的時間
CFTimeInterval endTime = CACurrentMediaTime();
return endTime - startTime;
}
//調(diào)用
[self testTimeConsume:^(NSString *name) {
//放入block中的代碼,可以使用參數(shù)name
//參數(shù)name是實現(xiàn)代碼中傳入的,在調(diào)用時只能使用,不能傳值
}];
4.Block回調(diào)
Block回調(diào)是關(guān)于Block最常用的內(nèi)容,比如網(wǎng)絡(luò)加載,我們可以用Block實現(xiàn)下載成功與失敗的反饋。開發(fā)者在block沒發(fā)布前,實現(xiàn)回調(diào)基本都是通過代理的方式進行的,比如負責網(wǎng)絡(luò)請求的原生類NSURLConnection類,通過多個協(xié)議方法實現(xiàn)請求中的事件處理。而在最新的環(huán)境下,使用的NSURLSession已經(jīng)采用block的方式處理任務(wù)請求了。各種第三方網(wǎng)絡(luò)請求框架也都在使用block進行回調(diào)處理,這種轉(zhuǎn)變很大一部分原因在于block使用簡單,邏輯氫氣,靈活等原因。
為了加深理解,再來一個簡單的小例子:
A,B兩個界面,A界面中有一個label,一個buttonA。點擊buttonA進入B界面,B界面中有一個UITextfield和一個buttonB,點擊buttonB退出B界面并將B界面中UITextfield的值傳到A界面中的label。
A界面中,也就是ViewController類中:
//關(guān)鍵demo
- (IBAction)buttonAction {
MyFirstViewController *myVC = [[MyFirstViewController alloc]] init];
[self presentViewController:myVC animated:YES completion:^{
}];
__weak typeof(self) weakSelf = self;//防止循環(huán)引用
//用屬性定義的注意:這里屬性是不會自動補全的,方法就會自動補全
[myVC setBlock:^(NSString *string) {
weakSelf.labelA.text = string;
}];
}
在B界面中,也就是MyFirstViewController類中.m文件:
- (IBAction)buttonAction {
[self dismissViewControllerAnimated:YES completion:^{
}];
self.block(_myTextfield.text);
}
.h文件
#import <UIKit/UIKit.h>
//typedef定義一下block,為了更好實用
typedef void(^MyBlock)(NSString *string);
@interface MyFirstViewController : UIViewController
@property (nonatomic, copy) MyBlock block;
@end
看了以上兩個Block回調(diào)示例,是不是感覺比delegate清爽了不少?