
原文連結:
www.imlifengfeng.com一、概述
閉包 = 一個函數「或指向函數的指針」+ 該函數執行的外部的上下文變量「也就是自由變量」;Block 是 Objective-C 對于閉包的實作。
其中,Block:
- 可以嵌套定義,定義 Block 方法和定義函數方法相似
- Block 可以定義在方法内部或外部
- 隻有調用 Block 時候,才會執行其{}體内的代碼
- 本質是對象,使代碼高聚合
使用 clang 将 OC 代碼轉換為 C++ 檔案檢視 block 的方法:
- 在指令行輸入代碼 clang -rewrite-objc 需要編譯的OC檔案.m
- 這時檢視目前的檔案夾裡 多了一個相同的名稱的 .cpp 檔案,在指令行輸入 open main.cpp 檢視檔案
二、Block的定義與使用
1、無參數無傳回值
//1,無參數,無傳回值,聲明和定義
void(^MyBlockOne)(void) = ^(void){
NSLog(@"無參數,無傳回值");
};
MyBlockOne();//block的調用
2、有參數無傳回值
//2,有參數,無傳回值,聲明和定義
void(^MyblockTwo)(int a) = ^(int a){
NSLog(@"@ = %d我就是block,有參數,無傳回值",a);
};
MyblockTwo(100);
3、有參數有傳回值
//3,有參數,有傳回值
int(^MyBlockThree)(int,int) = ^(int a,int b){
NSLog(@"%d我就是block,有參數,有傳回值",a + b);returna + b;
};
MyBlockThree(12,56);
4、無參數有傳回值(很少用到)
//4,無參數,有傳回值
int(^MyblockFour)(void) = ^{NSLog(@"無參數,有傳回值");
return45;
};
MyblockFour();
5、實際開發中常用typedef 定義Block
例如,用typedef定義一個block:
typedef int (^MyBlock)(int , int);
這時,MyBlock就成為了一種Block類型
在定義類的屬性時可以這樣:
@property (nonatomic,copy) MyBlock myBlockOne;
使用時:
self.myBlockOne = ^int (int ,int){
//TODO
}
三、Block與外界變量
1、截獲自動變量(局部變量)值
(1)預設情況
對于 block 外的變量引用,block 預設是将其複制到其資料結構中來實作通路的。也就是說block的自動變量截獲隻針對block内部使用的自動變量, 不使用則不截獲, 因為截獲的自動變量會存儲于block的結構體内部, 會導緻block體積變大。特别要注意的是預設情況下block隻能通路不能修改局部變量的值。
[圖檔上傳中...(image-73164a-1566284363727-7)]
int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
輸出結果:
age = 10
(2) __block 修飾的外部變量
對于用 __block 修飾的外部變量引用,block 是複制其引用位址來實作通路的。block可以修改__block 修飾的外部變量的值。
[圖檔上傳中...(image-d7e15-1566284363727-6)]
__block int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();
輸出為:
age = 18
為什麼使用__block 修飾的外部變量的值就可以被block修改呢?
我們使用 clang 将 OC 代碼轉換為 C++ 檔案:
clang -rewrite-objc 源代碼檔案名
便可揭開其真正面紗:
__block int val = 10;
轉換成
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
會發現一個局部變量加上__block修飾符後竟然跟block一樣變成了一個__Block_byref_val_0結構體類型的自動變量執行個體!!!!
此時我們在block内部通路val變量則需要通過一個叫__forwarding的成員變量來間接通路val變量(下面會對__forwarding進行詳解)
四、Block的copy操作
1、Block的存儲域及copy操作
在開始研究Block的copy操作之前,先來思考一下:Block是存儲在棧上還是堆上呢?
我們先來看看一個由C/C++/OBJC編譯的程式占用記憶體分布的結構:
[圖檔上傳中...(image-2a5d0-1566284363727-5)]
其實,block有三種類型:
- 全局塊(_NSConcreteGlobalBlock)
- 棧塊(_NSConcreteStackBlock)
- 堆塊(_NSConcreteMallocBlock)
這三種block各自的存儲域如下圖:
[圖檔上傳中...(image-8a9af1-1566284363727-4)]
- 全局塊存在于全局記憶體中, 相當于單例.
- 棧塊存在于棧記憶體中, 超出其作用域則馬上被銷毀
- 堆塊存在于堆記憶體中, 是一個帶引用計數的對象, 需要自行管理其記憶體
簡而言之,存儲在棧中的Block就是棧塊、存儲在堆中的就是堆塊、既不在棧中也不在堆中的塊就是全局塊。
遇到一個Block,我們怎麼這個Block的存儲位置呢?
(1)Block不通路外界變量(包括棧中和堆中的變量)
Block 既不在棧又不在堆中,在代碼段中,ARC和MRC下都是如此。此時為全局塊。
(2)Block通路外界變量
MRC 環境下:通路外界變量的 Block 預設存儲棧中。
ARC 環境下:通路外界變量的 Block 預設存儲在堆中(實際是放在棧區,然後ARC情況下自動又拷貝到堆區),自動釋放。
ARC下,通路外界變量的 Block為什麼要自動從棧區拷貝到堆區呢?
棧上的Block,如果其所屬的變量作用域結束,該Block就被廢棄,如同一般的自動變量。當然,Block中的__block變量也同時被廢棄。如下圖:
[圖檔上傳中...(image-ea535c-1566284363727-3)]
為了解決棧塊在其變量作用域結束之後被廢棄(釋放)的問題,我們需要把Block複制到堆中,延長其生命周期。開啟ARC時,大多數情況下編譯器會恰當地進行判斷是否有需要将Block從棧複制到堆,如果有,自動生成将Block從棧上複制到堆上的代碼。Block的複制操作執行的是copy執行個體方法。Block隻要調用了copy方法,棧塊就會變成堆塊。
如下圖:
[圖檔上傳中...(image-a178b3-1566284363727-2)]
例如下面一個傳回值為Block類型的函數:
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count) { return rate * count; };
}
分析可知:上面的函數傳回的Block是配置在棧上的,是以傳回函數調用方時,Block變量作用域就結束了,Block會被廢棄。但在ARC有效,這種情況編譯器會自動完成複制。
在非ARC情況下則需要開發者調用copy方法手動複制,由于開發中幾乎都是ARC模式,是以手動複制内容不再過多研究。
将Block從棧上複制到堆上相當消耗CPU,是以當Block設定在棧上也能夠使用時,就不要複制了,因為此時的複制隻是在浪費CPU資源。
Block的複制操作執行的是copy執行個體方法。不同類型的Block使用copy方法的效果如下表:
[圖檔上傳中...(image-cc36bd-1566284363727-1)]
根據表得知,Block在堆中copy會造成引用計數增加,這與其他Objective-C對象是一樣的。雖然Block在棧中也是以對象的身份存在,但是棧塊沒有引用計數,因為不需要,我們都知道棧區的記憶體由編譯器自動配置設定釋放。關于堆區和棧區詳細内容可以參考下峰哥之前的文章:《
總結:堆、棧、隊列》
不管Block存儲域在何處,用copy方法複制都不會引起任何問題。在不确定時調用copy方法即可。
在ARC有效時,多次調用copy方法完全沒有問題:
blk = [[[[blk copy] copy] copy] copy];
// 經過多次複制,變量blk仍然持有Block的強引用,該Block不會被廢棄。
2、__block變量與__forwarding
在copy操作之後,既然__block變量也被copy到堆上去了, 那麼通路該變量是通路棧上的還是堆上的呢?__forwarding 終于要閃亮登場了,如下圖:
[圖檔上傳中...(image-16f5c5-1566284363726-0)]
通過__forwarding, 無論是在block中還是 block外通路__block變量, 也不管該變量在棧上或堆上, 都能順利地通路同一個__block變量。
五、防止 Block 循環引用
Block 循環引用的情況:
某個類将 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 也有可能帶來的循環引用,如下:
// 循環引用 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;
};
}
- (void)execBlock {
self.attributBlock();
}
@end
// 使用類
id obj = [[TestObj alloc] init];
[obj execBlock]; // 如果不調用此方法,tmp 永遠不會置 nil,記憶體洩露會一直在
六、Block的使用示例
1、Block作為變量(Xcode快捷鍵:inlineBlock)
int (^sum) (int, int); // 定義一個 Block 變量 sum
// 給 Block 變量指派
// 一般 傳回值省略:sum = ^(int a,int b)…
sum = ^int (int a,int b){
return a+b;
}; // 指派語句最後有 分号
int a = sum(10,20); // 調用 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); // 調用 sum變量
// 2\. 作為對象的屬性聲明,copy 後 block 會轉移到堆中和對象一起
@property (nonatomic, copy) Calculate sum; // 使用 typedef
@property (nonatomic, copy) int (^sum)(int, int); // 不使用 typedef
// 聲明,類外
self.sum = ^(int a,int b){
return a+b;
};
// 調用,類内
int a = self.sum(10,20);
3、作為 OC 中的方法參數
// ---- 無參數傳遞的 Block ---------------------------
// 實作
- (CGFloat)testTimeConsume:(void(^)())middleBlock {
// 執行前記錄下目前的時間
CFTimeInterval startTime = CACurrentMediaTime();
middleBlock();
// 執行後記錄下目前的時間
CFTimeInterval endTime = CACurrentMediaTime();
return endTime - startTime;
}
// 調用
[self testTimeConsume:^{
// 放入 block 中的代碼
}];
// ---- 有參數傳遞的 Block ---------------------------
// 實作
- (CGFloat)testTimeConsume:(void(^)(NSString * name))middleBlock {
// 執行前記錄下目前的時間
CFTimeInterval startTime = CACurrentMediaTime();
NSString *name = @"有參數";
middleBlock(name);
// 執行後記錄下目前的時間
CFTimeInterval endTime = CACurrentMediaTime();
return endTime - startTime;
}
// 調用
[self testTimeConsume:^(NSString *name) {
// 放入 block 中的代碼,可以使用參數 name
// 參數 name 是實作代碼中傳入的,在調用時隻能使用,不能傳值
}];
4、Block回調
Block回調是關于Block最常用的内容,比如網絡下載下傳,我們可以用Block實作下載下傳成功與失敗的回報。開發者在block沒釋出前,實作回調基本都是通過代理的方式進行的,比如負責網絡請求的原生類NSURLConnection類,通過多個協定方法實作請求中的事件處理。而在最新的環境下,使用的NSURLSession已經采用block的方式處理任務請求了。各種第三方網絡請求架構也都在使用block進行回調處理。這種轉變很大一部分原因在于block使用簡單,邏輯清晰,靈活等原因。
如下:
//DownloadManager.h
#import <Foundation/Foundation.h>
@interface DownloadManager : NSObject <NSURLSessionDownloadDelegate>
// block 重命名
typedef void (^DownloadHandler)(NSData * receiveData, NSError * error);
- (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(DownloadHandler)handler ;
@end
//DownloadManager.m
#import "DownloadManager.h"
@implementation DownloadManager
- (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(DownloadHandler)handler
{
NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]];
NSURLSession * session = [NSURLSession sharedSession];
//執行請求任務
NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(data,error);
});
}
}];
[task resume];
}
上面通過封裝NSURLSession的請求,傳入一個處理請求結果的block對象,就會自動将請求任務放到工作線程中執行實作,我們在網絡請求邏輯的代碼中調用如下:
- (IBAction)buttonClicked:(id)sender {
#define DOWNLOADURL @"https://codeload.github.com/AFNetworking/AFNetworking/zip/master"
//下載下傳類
DownloadManager * downloadManager = [[DownloadManager alloc] init];
[downloadManager downloadWithURL: DOWNLOADURL parameters:nil handler:^(NSData *receiveData, NSError *error) {
if (error) {
NSLog(@"下載下傳失敗:%@",error);
}else {
NSLog(@"下載下傳成功,%@",receiveData);
}
}];
}
為了加深了解,再來一個簡單的小例子:
A,B兩個界面,A界面中有一個label,一個buttonA。點選buttonA進入B界面,B界面中有一個UITextfield和一個buttonB,點選buttonB退出B界面并将B界面中UITextfield的值傳到A界面中的label。
A界面中,也就是ViewController類中:
//關鍵demo:
- (IBAction)buttonAction {
MyFirstViewController *myVC = [[MyFirstViewController alloc] init];
[self presentViewController:myVC animated:YES completion:^{
}];
__weak typeof(self) weakSelf = self;//防止循環引用
//用屬性定義的注意:這裡屬性是不會自動補全的,方法就會自動補全
[myVC setBlock:^(NSString *string){
weakSelf.labelA.text = string;
}];
}
B界面中,也就是MyFirstViewController類中.m檔案:
- (IBAction)buttonBAction {
[self dismissViewControllerAnimated:YES completion:^{
}];
self.block(_myTextfielf.text);
}
.h檔案:
#import <UIKit/UIKit.h>
//typedef定義一下block,為了更好用
typedef void(^MyBlock)(NSString *string);
@interface MyFirstViewController : UIViewController
@property (nonatomic, copy) MyBlock block;
@end
看了以上兩個Block回調示例,是不是感覺比delegate清爽了不少?