Block深入淺出

一、概述

閉包 = 一個函數(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只能訪問不能修改局部變量的值。

WX20190708-145855.png
int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();
//輸出結(jié)果:
//age = 10;

2.__block修飾的外部變量
對于用__block修飾的外部變量引用,block是復制其引用地址來實現(xiàn)訪問的。block可以修改__block修飾的外部變量的值


WX20190708-150145.png
__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):

WX20190708-151828.png

其實,block有三種類型:

  • 全局塊(_NSConcreteGlobalBlock)
  • 棧塊(_NSConcreteStackBlock)
  • 堆塊(_NSConcreteMallocBlock)

這三種block各自的存儲域如下圖:


WX20190708-153416.png
  • 全局塊存在于全局內(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變量也同時被廢棄,如下圖:

WX20190708-154925.png

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

例如下面一個返回值為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方法的效果如下表:


WX20190708-160113.png

根據(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到堆上去了,那么訪問該變量是訪問棧上的還是堆上的呢?

WX20190708-160903.png

通過__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清爽了不少?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容