iOS中Block的用法酱床,舉例羊赵,解析與底層原理(這可能是最詳細(xì)的Block解析)

本文Demo傳送門:BlockTestApp

【摘要】這篇文章,首先在第1節(jié)中介紹Block的定義扇谣,以及與C里面函數(shù)的對(duì)比昧捷。然后,第2節(jié)介紹實(shí)際開發(fā)中經(jīng)常會(huì)用到的Block語法形式罐寨,以供讀者日后查閱靡挥。只知道怎么用卻不知什么時(shí)候用?所以隨后的第3節(jié)將介紹Block的應(yīng)用場景鸯绿。然而跋破,用Block不當(dāng)導(dǎo)致了Crash?所以楞慈,第4節(jié)有必要了解Block捕獲變量的特性幔烛,以及循環(huán)引用的解決。另外囊蓝,千萬不要懶饿悬,一碰到Block就weak,要區(qū)分哪些不會(huì)引起循環(huán)引用聚霜。然而狡恬,如果對(duì)Block的內(nèi)存機(jī)制不熟悉,也會(huì)導(dǎo)致Crash蝎宇,所以第5節(jié)會(huì)介紹Block的內(nèi)存機(jī)制弟劲。學(xué)到這里已經(jīng)夠用了。然而姥芥,你卻想進(jìn)一步了解Block的實(shí)現(xiàn)機(jī)制兔乞?第6節(jié)將簡單介紹下clang的編譯與Block的實(shí)現(xiàn)及其原理。

1. 前言


Block:帶有自動(dòng)變量(局部變量)的匿名函數(shù)。它是C語言的擴(kuò)充功能庸追。之所以是拓展霍骄,是因?yàn)镃語言不允許存在這樣匿名函數(shù)。

1.1 匿名函數(shù)

匿名函數(shù)是指不帶函數(shù)名稱函數(shù)淡溯。C語言中读整,函數(shù)是怎樣的呢?類似這樣:

int func(int count);

調(diào)用的時(shí)候:

int result = func(10);

func就是它的函數(shù)名咱娶。也可以通過指針調(diào)用函數(shù)米间,看起來沒用到函數(shù)名:

int result = (*funcptr)(10);

實(shí)際,在賦值給函數(shù)指針時(shí)膘侮,必須通過函數(shù)的名稱才能獲得該函數(shù)的地址屈糊。完整的步驟應(yīng)該是:

int (*funcptr)(int) = &func;
int result = (*funcptr)(10);

而通過Block,就能夠使用匿名函數(shù)喻喳,即不帶函數(shù)名稱的函數(shù)另玖。

1.2 帶有自動(dòng)變量

關(guān)于“帶有自動(dòng)變量(局部變量)”的含義,這是因?yàn)锽lock擁有捕獲外部變量的功能表伦。在Block中訪問一個(gè)外部的局部變量,Block會(huì)持用它的臨時(shí)狀態(tài)慷丽,自動(dòng)捕獲變量值蹦哼,外部局部變量的變化不會(huì)影響它的的狀態(tài)。

捕獲外部變量要糊,看一個(gè)經(jīng)典block面試題:

int val = 10; 
void (^blk)(void) = ^{
    printf("val=%d\n",val);
}; 
val = 2; 
blk(); 

上面這段代碼纲熏,輸出值是:val = 10,而不是2锄俄。

block 在實(shí)現(xiàn)時(shí)就會(huì)對(duì)它引用到的它所在方法中定義的棧變量進(jìn)行一次只讀拷貝局劲,然后在 block 塊內(nèi)使用該只讀拷貝;換句話說block截獲自動(dòng)變量的瞬時(shí)值奶赠;或者block捕獲的是自動(dòng)變量的副本鱼填。

由于block捕獲了自動(dòng)變量的瞬時(shí)值,所以在執(zhí)行block語法后毅戈,即使改寫block中使用的自動(dòng)變量的值也不會(huì)影響block執(zhí)行時(shí)自動(dòng)變量的值苹丸。

所以,上面的面試題的結(jié)果是10苇经,不是2赘理。

解決block不能修改自動(dòng)變量的值,這一問題的另外一個(gè)辦法是使用__block修飾符扇单。

__block int val = 10;  
void (^blk)(void) = ^{printf("val=%d\n",val);};  
val = 2;  
blk(); 

上面的代碼商模,跟第一個(gè)代碼段相比只是多了一個(gè)__block修飾符。但是輸出結(jié)果確是2。

2. Block語法大全


約定:用法中的符號(hào)含義列舉如下:

  • return_type 表示返回的對(duì)象/關(guān)鍵字等(可以是void施流,并省略)
  • blockName 表示block的名稱
  • var_type 表示參數(shù)的類型(可以是void凉倚,并省略)
  • varName 表示參數(shù)名稱

2.1 Block聲明及定義語法,及其變形

(1) 標(biāo)準(zhǔn)聲明與定義
return_type (^blockName)(var_type) = ^return_type (var_type varName) {
    // ...
};
blockName(var);
(2) 當(dāng)返回類型為void
void (^blockName)(var_type) = ^void (var_type varName) {
    // ...
};
blockName(var);

可省略寫成

void (^blockName)(var_type) = ^(var_type varName) {
    // ...
};
blockName(var);
(3) 當(dāng)參數(shù)類型為void
return_type (^blockName)(void) = ^return_type (void) {
    // ...
};
blockName();

可省略寫成

return_type (^blockName)(void) = ^return_type {
    // ...
};
blockName();
(4) 當(dāng)返回類型和參數(shù)類型都為void
void (^blockName)(void) = ^void (void) {
    // ...
};
blockName();

可省略寫成

void (^blockName)(void) = ^{
    // ...
};
blockName();
(5) 匿名Block

Block實(shí)現(xiàn)時(shí)嫂沉,等號(hào)右邊就是一個(gè)匿名Block稽寒,它沒有blockName,稱之為匿名Block:

^return_type (var_type varName)
{
    //...
};

2.2 typedef簡化Block的聲明

利用typedef簡化Block的聲明:

  • 聲明
typedef return_type (^BlockTypeName)(var_type);
  • 例子1:作屬性
//聲明
typedef void(^ClickBlock)(NSInteger index);
//block屬性
@property (nonatomic, copy) ClickBlock imageClickBlock;
  • 例子2:作方法參數(shù)
//聲明
typedef void (^handleBlock)();
//block作參數(shù)
- (void)requestForRefuseOrAccept:(MessageBtnType)msgBtnType messageModel:(MessageModel *)msgModel handle:(handleBlock)handle{
  ...

2.3 Block的常見用法

2.3.1 局部位置聲明一個(gè)Block型的變量
  • 位置
return_type (^blockName)(var_type) = ^return_type (var_type varName) {
    // ...
};
blockName(var);
  • 例子
void (^globalBlockInMemory)(int number) = ^(int number){
     printf("%d \n",number);
};
globalBlockInMemory(90);
2.3.2 @interface位置聲明一個(gè)Block型的屬性
  • 位置
@property(nonatomic, copy)return_type (^blockName) (var_type);
  • 例子
//按鈕點(diǎn)擊Block
@property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);
2.3.3 在定義方法時(shí)趟章,聲明Block型的形參
  • 用法
- (void)yourMethod:(return_type (^)(var_type))blockName;
  • 例子

UIView+AddClickedEvent.h

- (void)addClickedBlock:(void(^)(id obj))clickedAction;
2.3.4 在調(diào)用如上方法時(shí)杏糙,Block作實(shí)參
  • 例子

UIView+AddClickedEvent.m

- (void)addClickedBlock:(void(^)(id obj))clickedAction{
    self.clickedAction = clickedAction;
    // :先判斷當(dāng)前是否有交互事件,如果沒有的話蚓土。宏侍。。所有g(shù)esture的交互事件都會(huì)被添加進(jìn)gestureRecognizers中
    if (![self gestureRecognizers]) {
        self.userInteractionEnabled = YES;
        // :添加單擊事件
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];
        [self addGestureRecognizer:tap];
    }
}

- (void)tap{
    if (self.clickedAction) {
        self.clickedAction(self);
    }
}

2.4 Block的少見用法

2.4.1 Block的內(nèi)聯(lián)用法

這種形式并不常用蜀漆,匿名Block聲明后立即被調(diào)用:

^return_type (var_type varName)
{
    //...
}(var);
2.4.2 Block的遞歸調(diào)用

Block內(nèi)部調(diào)用自身谅河,遞歸調(diào)用是很多算法基礎(chǔ),特別是在無法提前預(yù)知循環(huán)終止條件的情況下确丢。注意:由于Block內(nèi)部引用了自身绷耍,這里必須使用__block避免循環(huán)引用問題。

__block return_type (^blockName)(var_type) = [^return_type (var_type varName)
{
    if (returnCondition)
    {
        blockName = nil;
        return;
    }
    // ...
    // 【遞歸調(diào)用】
    blockName(varName);
} copy];

【初次調(diào)用】
blockName(varValue);
2.4.3 Block作為返回值

方法的返回值是一個(gè)Block鲜侥,可用于一些“工廠模式”的方法中:

  • 用法:
- (return_type(^)(var_type))methodName
{
    return ^return_type(var_type param) {
        // ...
    };
}
  • 例子:Masonry框架里面的??
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
            NSMutableArray *children = NSMutableArray.new;
            for (id attr in attribute) {
                MASViewConstraint *viewConstraint = [self copy];
                viewConstraint.secondViewAttribute = attr;
                [children addObject:viewConstraint];
            }
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            compositeConstraint.delegate = self.delegate;
            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
            return compositeConstraint;
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

3. Block應(yīng)用場景

3.1 響應(yīng)事件

情景:UIViewContoller有個(gè)UITableView并是它的代理褂始,通過UITableView加載CellView。現(xiàn)在需要監(jiān)聽CellView中的某個(gè)按鈕(可以通過tag值區(qū)分)描函,并作出響應(yīng)崎苗。

如上面 2.3.2節(jié)在CellView.h中@interface位置聲明一個(gè)Block型的屬性,為了設(shè)置激活事件調(diào)用Block舀寓,接著我們?cè)贑ellView.m中作如下設(shè)置:

// 激活事件
#pragma mark - 按鈕點(diǎn)擊事件
- (IBAction)btnClickedAction:(UIButton *)sender {
    if (self.btnClickedBlock) {
        self.btnClickedBlock(sender);
    }
}

隨后胆数,在ViewController.m的適當(dāng)位置(- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{...代理方法)中通過setter方法設(shè)置CellView的Block屬性。Block寫著當(dāng)按鈕被點(diǎn)擊后要執(zhí)行的邏輯互墓。

// 響應(yīng)事件
cell.btnClickedBlock = ^(UIButton *sender) {
    //標(biāo)記消息已讀
    [weakSelf requestToReadedMessageWithTag:sender.tag];
    //刷新當(dāng)前cell
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};

其實(shí)必尼,即使Block不傳遞任何參數(shù),也可以傳遞事件的轰豆。但這種情況胰伍,無法區(qū)分事件的激活方(cell里面的哪一個(gè)按鈕?)酸休。即:

//按鈕點(diǎn)擊Block
@property (nonatomic, copy) void (^btnClickedBlock)(void);
// 激活事件
#pragma mark - 按鈕點(diǎn)擊事件
- (IBAction)btnClickedAction:(UIButton *)sender {
    if (self.btnClickedBlock) {
        self.btnClickedBlock();
    }
}
// 響應(yīng)事件
cell.btnClickedBlock = ^{
    //標(biāo)記消息已讀
    [weakSelf requestToReadedMessageWithTag:nil];
    //刷新當(dāng)前cell
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};

3.2 傳遞數(shù)據(jù)

上面的響應(yīng)事件骂租,其實(shí)也是傳遞數(shù)據(jù),只是它傳遞的對(duì)象是UIButton斑司。如下所示渗饮,SubTableView是VC的一個(gè)屬性和子視圖但汞。

  • 傳遞數(shù)值

SubTableView.h

@property (strong, nonatomic) void (^handleDidSelectedItem)(int indexPath);

SubTableView.m

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    _handleDidSelectedItem ? _handleDidSelectedItem(indexPath) : NULL;
}

VC.m

[_subView setHandleDidSelectedItem:^(int indexPath) {
        [weakself handleLabelDidSearchTableSelectedItem:indexPath];
    }];
- (void)handleLabelDidSearchTableSelectedItem:(int )indexPath {
    if (indexPath==0) {
        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"telprompt:%@", self.searchNullView.telLabel.text]]];
    }else if (indexPath==1){
        [self.navigationController popViewControllerAnimated:YES];
    }
}
  • 傳遞對(duì)象

例如HYBNetworking網(wǎng)絡(luò)框架中請(qǐng)求成功時(shí)傳遞接口返回?cái)?shù)據(jù)對(duì)象的Block:

[HYBNetworking postWithUrl:kSearchProblem refreshCache:NO params:params success:^(id response) {
        
        typeof(weakSelf) strongSelf = weakSelf;
//        [KVNProgress dismiss];
        NSString *stringData = [response mj_JSONString];
        stringData = [DES3Util decrypt:stringData];
        NSLog(@"stirngData: %@", stringData);
       ...
}

3.3 鏈?zhǔn)秸Z法

鏈?zhǔn)骄幊趟枷?/strong>:核心思想為將block作為方法的返回值,且返回值的類型為調(diào)用者本身互站,并將該方法以setter的形式返回私蕾,這樣就可以實(shí)現(xiàn)了連續(xù)調(diào)用,即為鏈?zhǔn)骄幊獭?/p>

Masonry的一個(gè)典型的鏈?zhǔn)骄幊逃梅ㄈ缦拢?/p>

[self.containerView addSubview:self.bannerView];
[self.bannerView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.equalTo(self.containerView.mas_leading);
    make.top.equalTo(self.containerView.mas_top);
    make.trailing.equalTo(self.containerView.mas_trailing);
    make.height.equalTo(@(kViewWidth(131.0)));
}];

現(xiàn)在胡桃,簡單使用鏈?zhǔn)骄幊趟枷雽?shí)現(xiàn)一個(gè)簡單計(jì)算器的功能:

3.3.1 在CaculateMaker.h文件中聲明一個(gè)方法add:
  • CaculateMaker.h
//  CaculateMaker.h
//  ChainBlockTestApp

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface CaculateMaker : NSObject

@property (nonatomic, assign) CGFloat result;

- (CaculateMaker *(^)(CGFloat num))add;

@end
3.3.2 在CaculateMaker.m文件中實(shí)現(xiàn)add方法:
  • CaculateMaker.m
//  CaculateMaker.m
//  ChainBlockTestApp


#import "CaculateMaker.h"

@implementation CaculateMaker

- (CaculateMaker *(^)(CGFloat num))add;{
    return ^CaculateMaker *(CGFloat num){
        _result += num;
        return self;
    };
}

@end
3.3.3 在viewController里面導(dǎo)入CaculateMaker.h文件踩叭,然后調(diào)用add方法就完成了鏈?zhǔn)秸Z法:
  • ViewController.m
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);

4. Block使用注意

4.1 截獲自動(dòng)變量與__block說明符

前面講過block所在函數(shù)中的,捕獲自動(dòng)變量翠胰。但是不能修改它容贝,不然就是“編譯錯(cuò)誤”。但是可以改變全局變量之景、靜態(tài)變量斤富、全局靜態(tài)變量。其實(shí)這兩個(gè)特點(diǎn)不難理解:

  • 不能修改自動(dòng)變量的值是因?yàn)椋篵lock捕獲的是自動(dòng)變量的const值锻狗,名字一樣满力,不能修改

  • 可以修改靜態(tài)變量的值:靜態(tài)變量屬于類的,不是某一個(gè)變量轻纪。由于block內(nèi)部不用調(diào)用self指針油额。所以block可以調(diào)用。

解決block不能修改自動(dòng)變量的值桐磁,這一問題的另外一個(gè)辦法是使用__block修飾符悔耘。

4.2 截獲對(duì)象

對(duì)于捕獲ObjC對(duì)象,不同于基本類型我擂;Block會(huì)引起對(duì)象的引用計(jì)數(shù)變化。

@interface MyClass : NSObject {  
    NSObject* _instanceObj;  
}  
@end  
  
@implementation MyClass  
  
NSObject* __globalObj = nil;  
  
- (id) init {  
    if (self = [super init]) {  
        _instanceObj = [[NSObject alloc] init];  
    }  
    return self;  
}  
  
- (void) test {  
    static NSObject* __staticObj = nil;  
    __globalObj = [[NSObject alloc] init];  
    __staticObj = [[NSObject alloc] init];  
  
    NSObject* localObj = [[NSObject alloc] init];  
    __block NSObject* blockObj = [[NSObject alloc] init];  
  
    typedef void (^MyBlock)(void) ;  
    MyBlock aBlock = ^{  
        NSLog(@"%@", __globalObj);  
        NSLog(@"%@", __staticObj);  
        NSLog(@"%@", _instanceObj);  
        NSLog(@"%@", localObj);  
        NSLog(@"%@", blockObj);  
    };  
    aBlock = [[aBlock copy] autorelease];  
    aBlock();  
  
    NSLog(@"%d", [__globalObj retainCount]);  
    NSLog(@"%d", [__staticObj retainCount]);  
    NSLog(@"%d", [_instanceObj retainCount]);  
    NSLog(@"%d", [localObj retainCount]);  
    NSLog(@"%d", [blockObj retainCount]);  
}  
@end  
  
int main(int argc, charchar *argv[]) {  
    @autoreleasepool {  
        MyClass* obj = [[[MyClass alloc] init] autorelease];  
        [obj test];  
        return 0;  
    }  
}  

執(zhí)行結(jié)果為1 1 1 2 1缓艳。

__globalObj__staticObj在內(nèi)存中的位置是確定的校摩,所以Block copy時(shí)不會(huì)retain對(duì)象。

_instanceObj在Block copy時(shí)也沒有直接retain _instanceObj對(duì)象本身阶淘,但會(huì)retain self衙吩。所以在Block中可以直接讀寫_instanceObj變量。
localObj在Block copy時(shí)溪窒,系統(tǒng)自動(dòng)retain對(duì)象坤塞,增加其引用計(jì)數(shù)。
blockObj在Block copy時(shí)也不會(huì)retain澈蚌。

4.3 Block引起的循環(huán)引用

一般來說我們總會(huì)在設(shè)置Block之后摹芙,在合適的時(shí)間回調(diào)Block,而不希望回調(diào)Block的時(shí)候Block已經(jīng)被釋放了宛瞄,所以我們需要對(duì)Block進(jìn)行copy浮禾,copy到堆中,以便后用。

Block可能會(huì)導(dǎo)致循環(huán)引用問題盈电,因?yàn)閎lock在拷貝到堆上的時(shí)候蝴簇,會(huì)retain其引用的外部變量,那么如果block中如果引用了他的宿主對(duì)象匆帚,那很有可能引起循環(huán)引用熬词,如:

  • TestCycleRetain
- (void) dealloc {
    NSLog(@"no cycle retain");
} 

- (id) init {
    self = [super init];
    if (self) {

        #if TestCycleRetainCase1
        //會(huì)循環(huán)引用
        self.myblock = ^{
            [self doSomething];
        };
  
        #elif TestCycleRetainCase2
        //會(huì)循環(huán)引用
        __block TestCycleRetain * weakSelf = self;
        self.myblock = ^{
            [weakSelf doSomething];
        };

        #elif TestCycleRetainCase3
        //不會(huì)循環(huán)引用
        __weak TestCycleRetain * weakSelf = self;
        self.myblock = ^{
            [weakSelf doSomething];
        };

        #elif TestCycleRetainCase4
        //不會(huì)循環(huán)引用
        __unsafe_unretained TestCycleRetain * weakSelf = self;
        self.myblock = ^{
            [weakSelf doSomething];
        };

        #endif NSLog(@"myblock is %@", self.myblock);
    }
    return self;
} 

- (void) doSomething {
    NSLog(@"do Something");
}
  • main
int main(int argc, char * argv[]) {
    @autoreleasepool {
        TestCycleRetain * obj = [[TestCycleRetain alloc] init];
        obj = nil;
        return 0;
    }
}
  • MRC情況下,用__block可以消除循環(huán)引用吸重。
  • ARC情況下互拾,必須用弱引用才可以解決循環(huán)引用問題,iOS 5之后可以直接使用__weak晤锹,之前則只能使用__unsafe_unretained了摩幔,__unsafe_unretained缺點(diǎn)是指針釋放后自己不會(huì)置

在上述使用 block中,雖說使用__weak鞭铆,但是此處會(huì)有一個(gè)隱患或衡,你不知道 self 什么時(shí)候會(huì)被釋放,為了保證在block內(nèi)不會(huì)被釋放车遂,我們添加__strong封断。更多的時(shí)候需要配合strongSelf使用,如下:

__weak __typeof(self) weakSelf = self; 
self.testBlock =  ^{
       __strong __typeof(weakSelf) strongSelf = weakSelf;
       [strongSelf test]; 
});

4.4 實(shí)用宏定義:避免Block引起循環(huán)引用

  • 第一步

在工程的TestAPP-Prefix.pch的文件中直接(不推薦)或在其導(dǎo)入的頭文件中間接寫入以下宏定義:

//----------------------強(qiáng)弱引用----------------------------
#ifndef weakify
#if DEBUG
#if __has_feature(objc_arc)
#define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
#endif
#else
#if __has_feature(objc_arc)
#define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
#endif
#endif
#endif

#ifndef strongify
#if DEBUG
#if __has_feature(objc_arc)
#define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
#endif
#else
#if __has_feature(objc_arc)
#define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
#endif
#endif
#endif
  • 第二步

在設(shè)置Block體的時(shí)候舶担,像如下這樣使用即可坡疼。

@weakify(self);
[footerView setClickFooterBlock:^{
        @strongify(self);
        [self handleClickFooterActionWithSectionTag:section];
}];

4.5 所有的Block里面的self必須要weak一下?

很顯然答案不都是衣陶,有些情況下是可以直接使用self的柄瑰,比如調(diào)用系統(tǒng)的方法:

[UIView animateWithDuration:0.5 animations:^{
        NSLog(@"%@", self);
    }];

因?yàn)檫@個(gè)block存在于靜態(tài)方法中,雖然block對(duì)self強(qiáng)引用著剪况,但是self卻不持有這個(gè)靜態(tài)方法教沾,所以完全可以在block內(nèi)部使用self。

另外译断,來看一個(gè)Masonry代碼布局的例子授翻,這里面的self會(huì)不會(huì)造成循環(huán)引用呢?

[self.headView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.centerY.equalTo(self.otherView.mas_centerY);
}];

并不是 block 就一定會(huì)造成循環(huán)引用孙咪,是不是循環(huán)引用要看是不是相互持有強(qiáng)引用堪唐。block 里用到了 self,那 block 會(huì)保持一個(gè) self 的引用翎蹈,但是 self 并沒有直接或者間接持有 block淮菠,所以不會(huì)造成循環(huán)引用⊙畹埃可以看一下Masonry的源代碼:

  • View+MASAdditions.m
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
  • MASConstraintMaker.m
- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;
    
    self.view = view;
    self.constraints = NSMutableArray.new;
    
    return self;
}

持有鏈?zhǔn)沁@樣的兜材,并沒有形成引用循環(huán):

self ->self.headView ··· MASConstraintMaker構(gòu)造block->self

注意觀察理澎,這個(gè)作為方法參數(shù)的Block體并沒有被任何方持有。因此曙寡,我們放心在Masonry中使用self.xxx 不會(huì)循環(huán)引用的糠爬。而且這個(gè)block里面用weakSelf還有可能會(huì)出問題,因?yàn)閙as_qeual如果得到一個(gè)nil參數(shù)的話應(yīng)該會(huì)導(dǎo)致程序崩潰举庶。

因?yàn)閁IView未強(qiáng)持有block执隧,所以這個(gè)block只是個(gè)棧block,而且構(gòu)不成循環(huán)引用的條件户侥。棧block有個(gè)特性就是它執(zhí)行完畢之后就出棧镀琉,出棧了就會(huì)被釋放掉∪锾疲看mas_makexxx的方法實(shí)現(xiàn)會(huì)發(fā)現(xiàn)這個(gè)block很快就被調(diào)用了屋摔,完事兒就出棧銷毀,構(gòu)不成循環(huán)引用替梨,所以可以直接放心的使self钓试。另外,這個(gè)與網(wǎng)絡(luò)請(qǐng)求里面使用self道理是一樣的副瀑。

5. Block與內(nèi)存管理

根據(jù)Block在內(nèi)存中的位置分為三種類型:

  • NSGlobalBlock是位于全局區(qū)的block,它是設(shè)置在程序的數(shù)據(jù)區(qū)域(.data區(qū))中挽鞠。

  • NSStackBlock是位于棧區(qū)狈孔,超出變量作用域信认,棧上的Block以及 __block變量都被銷毀均抽。

  • NSMallocBlock是位于堆區(qū),在變量作用域結(jié)束時(shí)不受影響到忽。

注意:在 ARC 開啟的情況下,將只會(huì)有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block清寇。

正如它們名字顯示得一樣华烟,表明了block的三種存儲(chǔ)方式:棧、全局负饲、堆。獲取block對(duì)象中的isa的值妥泉,可以得到上面其中一個(gè)盲链,下面開始說明哪種block存儲(chǔ)在棧迟杂、堆排拷、全局。

5.1 位于全局區(qū):GlobalBlock

生成在全局區(qū)block有兩種情況:

  • 定義全局變量的地方有block語法時(shí)
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
 
}
  • block語法的表達(dá)式中沒有使用應(yīng)截獲的自動(dòng)變量時(shí)
int(^block)(int count) = ^(int count) {
        return count;
    };
 block(2);

雖然布蔗,這個(gè)block在循環(huán)內(nèi)何鸡,但是blk的地址總是不變的。說明這個(gè)block在全局段骡男。注:針對(duì)沒有捕獲自動(dòng)變量的block來說隔盛,雖然用clang的rewrite-objc轉(zhuǎn)化后的代碼中仍顯示_NSConcretStackBlock吮炕,但是實(shí)際上不是這樣的访得。

5.2 位于棧內(nèi)存:StackBlock

這種情況悍抑,在非ARC下是無法編譯的搜骡,在ARC下可以編譯记靡。

  • block語法的表達(dá)式中使用截獲的自動(dòng)變量時(shí)
NSInteger i = 10; 
block = ^{ 
     NSLog(@"%ld", i); 
};
block;

設(shè)置在棧上的block团驱,如果其作用域結(jié)束嚎花,該block就被銷毀贩幻。同樣的两嘴,由于__block變量也配置在棧上憔辫,如果其作用域結(jié)束贰您,則該__block變量也會(huì)被銷毀锦亦。

另外,例如

typedef void (^block_t)() ;  

-(block_t)returnBlock{  
    __block int add=10;  
    return ^{
        printf("add=%d\n",++add);
    };  
}  

5.3 位于堆內(nèi)存:MallocBlock

堆中的block無法直接創(chuàng)建顾瞪,其需要由_NSConcreteStackBlock類型的block拷貝而來(也就是說block需要執(zhí)行copy之后才能存放到堆中)陈醒。由于block的拷貝最終都會(huì)調(diào)用_Block_copy_internal函數(shù)钉跷。

void(^block)(void);

int main(int argc, const char * argv[]) {
   @autoreleasepool {

       __block NSInteger i = 10;
       block = [^{
           ++i;
       } copy];
       ++i; 
       block();
       NSLog(@"%ld", i);
   }
   return 0;
}

我們對(duì)這個(gè)生成在棧上的block執(zhí)行了copy操作爷辙,Block和__block變量均從棧復(fù)制到堆上朦促。上面的代碼思灰,有跟沒有copy洒疚,在非ARC和ARC下一個(gè)是stack一個(gè)是Malloc油湖。這是因?yàn)锳RC下默認(rèn)為Malloc(即使如此,ARC下還是有一些例外乏德,下面會(huì)講)喊括。

block在ARC和非ARC下有巨大差別郑什。多數(shù)情況下蘑拯,ARC下會(huì)默認(rèn)把棧block被會(huì)直接拷貝生成到堆上。那么弯蚜,什么時(shí)候棧上的Block會(huì)復(fù)制到堆上呢碎捺?

  • 調(diào)用Block的copy實(shí)例方法時(shí)
  • Block作為函數(shù)返回值返回時(shí)
  • 將Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時(shí)
  • 將方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時(shí)

block在ARC和非ARC下的巨大差別

  • 在 ARC 中牵寺,捕獲外部了變量的 block 的類會(huì)是 NSMallocBlock 或者 NSStackBlock帽氓,如果 block 被賦值給了某個(gè)變量黎休,在這個(gè)過程中會(huì)執(zhí)行 _Block_copy 將原有的 NSStackBlock 變成 NSMallocBlock势腮;但是如果 block 沒有被賦值給某個(gè)變量漫仆,那它的類型就是 NSStackBlock盲厌;沒有捕獲外部變量的 block 的類會(huì)是 NSGlobalBlock 即不在堆上,也不在棧上没隘,它類似 C 語言函數(shù)一樣會(huì)在代碼段中右蒲。

  • 在非 ARC 中赶熟,捕獲了外部變量的 block 的類會(huì)是 NSStackBlock钧大,放置在棧上啊央,沒有捕獲外部變量的 block 時(shí)與 ARC 環(huán)境下情況相同瓜饥。

例如

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self testBlockForHeapOfARC];
}
-(void)testBlockForHeapOfARC{
    int val =10;
    typedef void (^blk_t)(void);
    blk_t block = ^{
        NSLog(@"blk0:%d",val);
    };
    block();
}
image.png

即使如此乓土,ARC下還是有一些例外:

例外

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self testBlockForHeap0];
}

#pragma mark - testBlockForHeap0 - crash
-(NSArray *)getBlockArray0{
    int val =10;
    return [NSArray arrayWithObjects:
            ^{NSLog(@"blk0:%d",val);},
            ^{NSLog(@"blk1:%d",val);},nil];
}


-(void)testBlockForHeap0{
    
    NSArray *tempArr = [self getBlockArray0];
    NSMutableArray *obj = [tempArr mutableCopy];
    typedef void (^blk_t)(void);
    blk_t block = (blk_t){[obj objectAtIndex:0]};
    block();
}

這段代碼在最后一行blk()會(huì)異常狡相,因?yàn)閿?shù)組中的block是棧上的食磕。因?yàn)関al是棧上的彬伦。解決辦法就是調(diào)用copy方法单绑。這種場景搂橙,ARC也不會(huì)為你添加copy,因?yàn)锳RC不確定唯袄,采取了保守的措施:不添加copy恋拷。所以ARC下也是會(huì)異常退出蔬顾。

image.png

例外的改進(jìn)1

調(diào)用block 的copy函數(shù),將block拷貝到堆上:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self testBlockForHeap1];
}

-(void)testBlockForHeap1{
    
    NSArray *tempArr = [self getBlockArray1];
    NSMutableArray *obj = [tempArr mutableCopy];
    typedef void (^blk_t)(void);
    blk_t block = (blk_t){[obj objectAtIndex:0]};
    block();
}

-(NSArray *)getBlockArray1{
    int val =10;
    return [NSArray arrayWithObjects:
            [^{NSLog(@"blk0:%d",val);} copy],
            [^{NSLog(@"blk1:%d",val);} copy],nil];
}

打個(gè)斷點(diǎn)可見舷胜,該Block的類型:

image.png

例外的改進(jìn)2

例如下面代碼中烹骨,在addBlockToArray方法中的block還是_NSConcreteStackBlock類型的沮焕,在testBlockForHeap2方法中就被復(fù)制到了堆中峦树,成為_NSConcreteMallocBlock類型的block:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self testBlockForHeap2];
}

- (void)addBlockToArray:(NSMutableArray *)array {

    int val =10;
    [array addObjectsFromArray:@[
         ^{NSLog(@"blk0:%d",val);},
         ^{NSLog(@"blk1:%d",val);}]];
}

- (void)testBlockForHeap2{

    NSMutableArray *array = [NSMutableArray array];
    [self addBlockToArray:array];
    typedef void (^blk_t)(void);
    blk_t block = (blk_t){[array objectAtIndex:0]};
    block();
}

打個(gè)斷點(diǎn)可見魁巩,其中Block的類型:

5.4 Block的復(fù)制

  • 在全局block調(diào)用copy什么也不做
  • 在棧上調(diào)用copy那么復(fù)制到堆上
  • 在堆上調(diào)用block 引用計(jì)數(shù)增加
-(void) stackOrHeap{  
    __block int val =10;  
    blkt1 s= ^{  
        return ++val;};  
    s();  
    blkt1 h = [s copy];  
    h();  
}  

不管block配置在何處谷遂,用copy方法復(fù)制都不會(huì)引起任何問題埋凯。在ARC環(huán)境下白对,如果不確定是否要copy這個(gè)block甩恼,那盡管copy即可条摸。

最后的強(qiáng)調(diào)钉蒲,在 ARC 開啟的情況下,除非上面的例外踏枣,默認(rèn)只會(huì)有 NSConcreteGlobalBlockNSConcreteMallocBlock 類型的 block茵瀑。

6. Block的底層研究方法

6.1 研究工具:clang

為了研究編譯器是如何實(shí)現(xiàn) block 的马昨,我們需要使用 clang鸿捧。clang 提供一個(gè)命令笛谦,可以將 Objetive-C 的源碼改寫成 c 語言的饥脑,借此可以研究 block 具體的源碼實(shí)現(xiàn)方式灶轰。

首先cd到代碼文件目錄

cd /Users/ChenMan/iOSTest/BlockTestApp

然后執(zhí)行clang命令

clang -rewrite-objc main.m

其中笋颤,main.m的代碼寫好如下

#include <stdio.h>

int main(int argc, char * argv[]) {
    @autoreleasepool {
        typedef void (^blk_t)(void);
        blk_t block = ^{
            printf("Hello, World!\n");
        };
        block();
//        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

執(zhí)行情況:

你會(huì)看到main.cpp

6.2 實(shí)現(xiàn)分析

這里只選取部分關(guān)鍵代碼。

不難看出int main(int argc, char * argv[]) {就是主函數(shù)的實(shí)現(xiàn)非凌。

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        typedef void (*blk_t)(void);
        blk_t block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
}

其中敞嗡,__main_block_impl_0是block的一個(gè)C++的實(shí)現(xiàn)(最后面的_0代表是main中的第幾個(gè)block)喉悴,也就是說也是一個(gè)結(jié)構(gòu)體箕肃。

(1) __main_block_impl_0

__main_block_impl_0定義如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
(2) __block_impl

如上勺像,其中__block_impl的定義如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

其結(jié)構(gòu)體成員如下:

  • isa咏删,指向所屬類的指針督函,也就是block的類型
  • Flags辰狡,標(biāo)志變量垄分,在實(shí)現(xiàn)block的內(nèi)部操作時(shí)會(huì)用到
  • Reserved薄湿,保留變量
  • FuncPtr豺瘤,block執(zhí)行時(shí)調(diào)用的函數(shù)指針

可以看出坐求,它包含了isa指針(包含isa指針的皆為對(duì)象)桥嗤,也就是說block也是一個(gè)對(duì)象(runtime里面泛领,對(duì)象和類都是用結(jié)構(gòu)體表示)师逸。

(3) __main_block_desc_0

__main_block_desc_0的定義如下:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

其結(jié)構(gòu)成員含義如下:

  • reserved:保留字段
  • Block_size:block大小(sizeof(struct __main_block_impl_0))

以上代碼在定義__main_block_desc_0結(jié)構(gòu)體時(shí),同時(shí)創(chuàng)建了__main_block_desc_0_DATA皿伺,并給它賦值鸵鸥,以供在main函數(shù)中對(duì)__main_block_impl_0進(jìn)行初始化妒穴。

(4) __main_block_func_0

如上的main函數(shù)中讼油,__main_block_func_0也是block的一個(gè)C++的實(shí)現(xiàn)

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
            printf("Hello, World!\n");
        }
總結(jié):綜合可知
  • __main_block_impl_0isa 指針指向了_NSConcreteStackBlock矮台。
  • 從main函數(shù)的main.cpp中看瘦赫,__main_block_impl_0FuncPtr 指向了函數(shù)__main_block_func_0确虱。
  • __main_block_impl_0Desc 也指向了定義__main_block_desc_0時(shí)就創(chuàng)建的__main_block_desc_0_DATA校辩,其中紀(jì)錄了block結(jié)構(gòu)體大小等信息召川。

以上就是根據(jù)編譯轉(zhuǎn)換的結(jié)果荧呐。當(dāng)然倍阐,由于 clang 改寫的具體實(shí)現(xiàn)方式和 LLVM 不太一樣峰搪,有急切底層興趣的讀者可以進(jìn)行更深入的研究概耻。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鞠柄,一起剝皮案震驚了整個(gè)濱河市厌杜,隨后出現(xiàn)的幾起案子夯尽,更是在濱河造成了極大的恐慌匙握,老刑警劉巖圈纺,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茫叭,居然都是意外死亡揍愁,警方通過查閱死者的電腦和手機(jī)莽囤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門朽缎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來话肖,“玉大人最筒,你說我怎么就攤上這事床蜘⌒暇猓” “怎么了弹囚?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵蛮穿,是天一觀的道長毁渗。 經(jīng)常有香客問我践磅,道長,這世上最難降的妖魔是什么灸异? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任府适,我火速辦了婚禮,結(jié)果婚禮上肺樟,老公的妹妹穿的比我還像新娘檐春。我一直安慰自己么伯,他們只是感情好疟暖,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著田柔,像睡著了一般俐巴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上硬爆,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天欣舵,我揣著相機(jī)與錄音,去河邊找鬼缀磕。 笑死缘圈,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的袜蚕。 我是一名探鬼主播准验,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼廷没!你這毒婦竟也來了糊饱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤颠黎,失蹤者是張志新(化名)和其女友劉穎另锋,沒想到半個(gè)月后滞项,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夭坪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年文判,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片室梅。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡戏仓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出亡鼠,到底是詐尸還是另有隱情赏殃,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布间涵,位于F島的核電站仁热,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏勾哩。R本人自食惡果不足惜抗蠢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望思劳。 院中可真熱鬧迅矛,春花似錦、人聲如沸潜叛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钠导。三九已至嚎尤,卻和暖如春攀涵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谎替。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國打工扼睬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逮栅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓窗宇,卻偏偏與公主長得像措伐,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子军俊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容