iOS中Block的用法,示例,應用場景,與底層原理解析(這可能是最詳細的Block解析)

陳滿iOS發表於2018-04-23

本文Demo傳送門:BlockTestApp

【摘要】這篇文章,首先在第1節中介紹Block的定義,以及與C裡面函式的對比。然後,第2節介紹實際開發中經常會用到的Block語法形式,以供讀者日後查閱。只知道怎麼用卻不知什麼時候用?所以隨後的第3節將介紹Block的應用場景。然而,用Block不當導致了Crash?所以,第4節有必要了解Block捕獲變數的特性,以及迴圈引用的解決。另外,千萬不要懶,一碰到Block就weak,要區分哪些不會引起迴圈引用。然而,如果對Block的記憶體機制不熟悉,也會導致Crash,所以第5節會介紹Block的記憶體機制。學到這裡已經夠用了。然而,你卻想進一步瞭解Block的實現機制?第6節將簡單介紹下clang的編譯與Block的實現及其原理。

iOS中Block的用法,示例,應用場景,與底層原理解析(這可能是最詳細的Block解析)

1. 前言


Block:帶有自動變數(區域性變數)的匿名函式。它是C語言的擴充功能。之所以是擴充,是因為C語言不允許存在這樣匿名函式。

1.1 匿名函式

匿名函式是指不帶函式名稱函式。C語言中,函式是怎樣的呢?類似這樣:

int func(int count);
複製程式碼

呼叫的時候:

int result = func(10);
複製程式碼

func就是它的函式名。也可以通過指標呼叫函式,看起來沒用到函式名:

int result = (*funcptr)(10);
複製程式碼

實際,在賦值給函式指標時,必須通過函式的名稱才能獲得該函式的地址。完整的步驟應該是:

int (*funcptr)(int) = &func;
int result = (*funcptr)(10);
複製程式碼

而通過Block,就能夠使用匿名函式,即不帶函式名稱的函式。

1.2 帶有自動變數

關於“帶有自動變數(區域性變數)”的含義,這是因為Block擁有捕獲外部變數的功能。在Block中訪問一個外部的區域性變數,Block會持用它的臨時狀態,自動捕獲變數值,外部區域性變數的變化不會影響它的的狀態。

捕獲外部變數,看一個經典block面試題:

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

上面這段程式碼,輸出值是:val = 10,而不是2。

block 在實現時就會對它引用到的它所在方法中定義的棧變數進行一次只讀拷貝,然後在 block 塊內使用該只讀拷貝;換句話說block截獲自動變數的瞬時值;或者block捕獲的是自動變數的副本。

由於block捕獲了自動變數的瞬時值,所以在執行block語法後,即使改寫block中使用的自動變數的值也不會影響block執行時自動變數的值。

所以,上面的面試題的結果是10,不是2。

解決block不能修改自動變數的值,這一問題的另外一個辦法是使用__block修飾符。

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

上面的程式碼,跟第一個程式碼段相比只是多了一個__block修飾符。但是輸出結果確是2。

2. Block語法大全


約定:用法中的符號含義列舉如下:

  • return_type 表示返回的物件/關鍵字等(可以是void,並省略)
  • blockName 表示block的名稱
  • var_type 表示引數的型別(可以是void,並省略)
  • varName 表示引數名稱

2.1 Block宣告及定義語法,及其變形

(1) 標準宣告與定義
return_type (^blockName)(var_type) = ^return_type (var_type varName) {
    // ...
};
blockName(var);
複製程式碼
(2) 當返回型別為void
void (^blockName)(var_type) = ^void (var_type varName) {
    // ...
};
blockName(var);
複製程式碼

可省略寫成

void (^blockName)(var_type) = ^(var_type varName) {
    // ...
};
blockName(var);
複製程式碼
(3) 當引數型別為void
return_type (^blockName)(void) = ^return_type (void) {
    // ...
};
blockName();
複製程式碼

可省略寫成

return_type (^blockName)(void) = ^return_type {
    // ...
};
blockName();
複製程式碼
(4) 當返回型別和引數型別都為void
void (^blockName)(void) = ^void (void) {
    // ...
};
blockName();
複製程式碼

可省略寫成

void (^blockName)(void) = ^{
    // ...
};
blockName();
複製程式碼
(5) 匿名Block

Block實現時,等號右邊就是一個匿名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:作方法引數
//宣告
typedef void (^handleBlock)();
//block作引數
- (void)requestForRefuseOrAccept:(MessageBtnType)msgBtnType messageModel:(MessageModel *)msgModel handle:(handleBlock)handle{
  ...
複製程式碼

2.3 Block的常見用法

2.3.1 區域性位置宣告一個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位置宣告一個Block型的屬性
  • 位置
@property(nonatomic, copy)return_type (^blockName) (var_type);
複製程式碼
  • 例子
//按鈕點選Block
@property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);
複製程式碼
2.3.3 在定義方法時,宣告Block型的形參
  • 用法
- (void)yourMethod:(return_type (^)(var_type))blockName;
複製程式碼
  • 例子

UIView+AddClickedEvent.h

- (void)addClickedBlock:(void(^)(id obj))clickedAction;
複製程式碼
2.3.4 在呼叫如上方法時,Block作實參
  • 例子

UIView+AddClickedEvent.m

- (void)addClickedBlock:(void(^)(id obj))clickedAction{
    self.clickedAction = clickedAction;
    // :先判斷當前是否有互動事件,如果沒有的話。。。所有gesture的互動事件都會被新增進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的內聯用法

這種形式並不常用,匿名Block宣告後立即被呼叫:

^return_type (var_type varName)
{
    //...
}(var);
複製程式碼
2.4.2 Block的遞迴呼叫

Block內部呼叫自身,遞迴呼叫是很多演算法基礎,特別是在無法提前預知迴圈終止條件的情況下。注意:由於Block內部引用了自身,這裡必須使用__block避免迴圈引用問題。

__block return_type (^blockName)(var_type) = [^return_type (var_type varName)
{
    if (returnCondition)
    {
        blockName = nil;
        return;
    }
    // ...
    // 【遞迴呼叫】
    blockName(varName);
} copy];

【初次呼叫】
blockName(varValue);
複製程式碼
2.4.3 Block作為返回值

方法的返回值是一個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應用場景

3.1 響應事件

情景:UIViewContoller有個UITableView並是它的代理,通過UITableView載入CellView。現在需要監聽CellView中的某個按鈕(可以通過tag值區分),並作出響應。

如上面 2.3.2節在CellView.h中@interface位置宣告一個Block型的屬性,為了設定啟用事件呼叫Block,接著我們在CellView.m中作如下設定:

// 啟用事件
#pragma mark - 按鈕點選事件
- (IBAction)btnClickedAction:(UIButton *)sender {
    if (self.btnClickedBlock) {
        self.btnClickedBlock(sender);
    }
}
複製程式碼

隨後,在ViewController.m的適當位置(- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{...代理方法)中通過setter方法設定CellView的Block屬性。Block寫著當按鈕被點選後要執行的邏輯。

// 響應事件
cell.btnClickedBlock = ^(UIButton *sender) {
    //標記訊息已讀
    [weakSelf requestToReadedMessageWithTag:sender.tag];
    //重新整理當前cell
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};
複製程式碼

其實,即使Block不傳遞任何引數,也可以傳遞事件的。但這種情況,無法區分事件的啟用方(cell裡面的哪一個按鈕?)。即:

//按鈕點選Block
@property (nonatomic, copy) void (^btnClickedBlock)(void);
複製程式碼
// 啟用事件
#pragma mark - 按鈕點選事件
- (IBAction)btnClickedAction:(UIButton *)sender {
    if (self.btnClickedBlock) {
        self.btnClickedBlock();
    }
}
複製程式碼
// 響應事件
cell.btnClickedBlock = ^{
    //標記訊息已讀
    [weakSelf requestToReadedMessageWithTag:nil];
    //重新整理當前cell
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};
複製程式碼

3.2 傳遞資料

上面的響應事件,其實也是傳遞資料,只是它傳遞的物件是UIButton。如下所示,SubTableView是VC的一個屬性和子檢視。

  • 傳遞數值

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];
    }
}
複製程式碼
  • 傳遞物件

例如HYBNetworking網路框架中請求成功時傳遞介面返回資料物件的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 鏈式語法

鏈式程式設計思想:核心思想為將block作為方法的返回值,且返回值的型別為呼叫者本身,並將該方法以setter的形式返回,這樣就可以實現了連續呼叫,即為鏈式程式設計。

Masonry的一個典型的鏈式程式設計用法如下:

[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)));
}];
複製程式碼

現在,簡單使用鏈式程式設計思想實現一個簡單計算器的功能:

3.3.1 在CaculateMaker.h檔案中宣告一個方法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檔案中實現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裡面匯入CaculateMaker.h檔案,然後呼叫add方法就完成了鏈式語法:
  • ViewController.m
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);
複製程式碼

4. Block使用注意

4.1 截獲自動變數與__block說明符

前面講過block所在函式中的,捕獲自動變數。但是不能修改它,不然就是“編譯錯誤”。但是可以改變全域性變數靜態變數全域性靜態變數。其實這兩個特點不難理解:

  • 不能修改自動變數的值是因為:block捕獲的是自動變數的const值,名字一樣,不能修改

  • 可以修改靜態變數的值:靜態變數屬於類的,不是某一個變數。由於block內部不用呼叫self指標。所以block可以呼叫。

解決block不能修改自動變數的值,這一問題的另外一個辦法是使用__block修飾符。

4.2 截獲物件

對於捕獲ObjC物件,不同於基本型別;Block會引起物件的引用計數變化。

@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;  
    }  
}  
複製程式碼

執行結果為1 1 1 2 1。

__globalObj__staticObj在記憶體中的位置是確定的,所以Block copy時不會retain物件。

_instanceObj在Block copy時也沒有直接retain _instanceObj物件本身,但會retain self。所以在Block中可以直接讀寫_instanceObj變數。 localObj在Block copy時,系統自動retain物件,增加其引用計數。 blockObj在Block copy時也不會retain

4.3 Block引起的迴圈引用

一般來說我們總會在設定Block之後,在合適的時間回撥Block,而不希望回撥Block的時候Block已經被釋放了,所以我們需要對Block進行copy,copy到堆中,以便後用。

Block可能會導致迴圈引用問題,因為block在拷貝到堆上的時候,會retain其引用的外部變數,那麼如果block中如果引用了他的宿主物件,那很有可能引起迴圈引用,如:

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

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

        #if TestCycleRetainCase1
        //會迴圈引用
        self.myblock = ^{
            [self doSomething];
        };
  
        #elif TestCycleRetainCase2
        //會迴圈引用
        __block TestCycleRetain * weakSelf = self;
        self.myblock = ^{
            [weakSelf doSomething];
        };

        #elif TestCycleRetainCase3
        //不會迴圈引用
        __weak TestCycleRetain * weakSelf = self;
        self.myblock = ^{
            [weakSelf doSomething];
        };

        #elif TestCycleRetainCase4
        //不會迴圈引用
        __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可以消除迴圈引用。
  • ARC情況下,必須用弱引用才可以解決迴圈引用問題,iOS 5之後可以直接使用__weak,之前則只能使用__unsafe_unretained了,__unsafe_unretained缺點是指標釋放後自己不會置

在上述使用 block中,雖說使用__weak,但是此處會有一個隱患,你不知道 self 什麼時候會被釋放,為了保證在block內不會被釋放,我們新增__strong。更多的時候需要配合strongSelf使用,如下:

__weak __typeof(self) weakSelf = self; 
self.testBlock =  ^{
       __strong __typeof(weakSelf) strongSelf = weakSelf;
       [strongSelf test]; 
});
複製程式碼

4.4 實用巨集定義:避免Block引起迴圈引用

  • 第一步

在工程的TestAPP-Prefix.pch的檔案中直接(不推薦)或在其匯入的標頭檔案中間接寫入以下巨集定義:

//----------------------強弱引用----------------------------
#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
複製程式碼
  • 第二步

在設定Block體的時候,像如下這樣使用即可。

@weakify(self);
[footerView setClickFooterBlock:^{
        @strongify(self);
        [self handleClickFooterActionWithSectionTag:section];
}];
複製程式碼

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

很顯然答案不都是,有些情況下是可以直接使用self的,比如呼叫系統的方法:

[UIView animateWithDuration:0.5 animations:^{
        NSLog(@"%@", self);
    }];
複製程式碼

因為這個block存在於靜態方法中,雖然block對self強引用著,但是self卻不持有這個靜態方法,所以完全可以在block內部使用self。

另外,來看一個Masonry程式碼佈局的例子,這裡面的self會不會造成迴圈引用呢?

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

並不是 block 就一定會造成迴圈引用,是不是迴圈引用要看是不是相互持有強引用。block 裡用到了 self,那 block 會保持一個 self 的引用,但是 self 並沒有直接或者間接持有 block,所以不會造成迴圈引用。可以看一下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;
}
複製程式碼

持有鏈是這樣的,並沒有形成引用迴圈:

self ->self.headView ··· MASConstraintMaker構造block->self

注意觀察,這個作為方法引數的Block體並沒有被任何方持有。因此,我們放心在Masonry中使用self.xxx 不會迴圈引用的。而且這個block裡面用weakSelf還有可能會出問題,因為mas_qeual如果得到一個nil引數的話應該會導致程式崩潰。

因為UIView未強持有block,所以這個block只是個棧block,而且構不成迴圈引用的條件。棧block有個特性就是它執行完畢之後就出棧,出棧了就會被釋放掉。看mas_makexxx的方法實現會發現這個block很快就被呼叫了,完事兒就出棧銷燬,構不成迴圈引用,所以可以直接放心的使self。另外,這個與網路請求裡面使用self道理是一樣的。

5. Block與記憶體管理

根據Block在記憶體中的位置分為三種型別:

  • NSGlobalBlock是位於全域性區的block,它是設定在程式的資料區域(.data區)中。

  • NSStackBlock是位於棧區,超出變數作用域,棧上的Block以及 __block變數都被銷燬。

  • NSMallocBlock是位於堆區,在變數作用域結束時不受影響。

注意:在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 型別的 block。

正如它們名字顯示得一樣,表明了block的三種儲存方式:棧、全域性、堆。獲取block物件中的isa的值,可以得到上面其中一個,下面開始說明哪種block儲存在棧、堆、全域性。

5.1 位於全域性區:GlobalBlock

生成在全域性區block有兩種情況:

  • 定義全域性變數的地方有block語法時
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
 
}
複製程式碼
  • block語法的表示式中沒有使用應截獲的自動變數時
int(^block)(int count) = ^(int count) {
        return count;
    };
 block(2);
複製程式碼

雖然,這個block在迴圈內,但是blk的地址總是不變的。說明這個block在全域性段。注:針對沒有捕獲自動變數的block來說,雖然用clang的rewrite-objc轉化後的程式碼中仍顯示_NSConcretStackBlock,但是實際上不是這樣的。

5.2 位於棧記憶體:StackBlock

這種情況,在非ARC下是無法編譯的,在ARC下可以編譯。

  • block語法的表示式中使用截獲的自動變數時
NSInteger i = 10; 
block = ^{ 
     NSLog(@"%ld", i); 
};
block;
複製程式碼

設定在棧上的block,如果其作用域結束,該block就被銷燬。同樣的,由於__block變數也配置在棧上,如果其作用域結束,則該__block變數也會被銷燬。

另外,例如

typedef void (^block_t)() ;  

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

5.3 位於堆記憶體:MallocBlock

堆中的block無法直接建立,其需要由_NSConcreteStackBlock型別的block拷貝而來(也就是說block需要執行copy之後才能存放到堆中)。由於block的拷貝最終都會呼叫_Block_copy_internal函式。

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;
}
複製程式碼

我們對這個生成在棧上的block執行了copy操作,Block和__block變數均從棧複製到堆上。上面的程式碼,有跟沒有copy,在非ARC和ARC下一個是stack一個是Malloc。這是因為ARC下預設為Malloc(即使如此,ARC下還是有一些例外,下面會講)。

block在ARC和非ARC下有巨大差別。多數情況下,ARC下會預設把棧block被會直接拷貝生成到堆上。那麼,什麼時候棧上的Block會複製到堆上呢?

  • 呼叫Block的copy例項方法時
  • Block作為函式返回值返回時
  • 將Block賦值給附有__strong修飾符id型別的類或Block型別成員變數時
  • 將方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時

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

  • 在 ARC 中,捕獲外部了變數的 block 的類會是 NSMallocBlock 或者 NSStackBlock,如果 block 被賦值給了某個變數,在這個過程中會執行 _Block_copy 將原有的 NSStackBlock 變成 NSMallocBlock;但是如果 block 沒有被賦值給某個變數,那它的型別就是 NSStackBlock;沒有捕獲外部變數的 block 的類會是 NSGlobalBlock 即不在堆上,也不在棧上,它類似 C 語言函式一樣會在程式碼段中。

  • 在非 ARC 中,捕獲了外部變數的 block 的類會是 NSStackBlock,放置在棧上,沒有捕獲外部變數的 block 時與 ARC 環境下情況相同。

例如

- (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()會異常,因為陣列中的block是棧上的。因為val是棧上的。解決辦法就是呼叫copy方法。這種場景,ARC也不會為你新增copy,因為ARC不確定,採取了保守的措施:不新增copy。所以ARC下也是會異常退出。

image.png

例外的改進1

呼叫block 的copy函式,將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];
}
複製程式碼

打個斷點可見,該Block的型別:

image.png

例外的改進2

例如下面程式碼中,在addBlockToArray方法中的block還是_NSConcreteStackBlock型別的,在testBlockForHeap2方法中就被複制到了堆中,成為_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();
}
複製程式碼

打個斷點可見,其中Block的型別:

iOS中Block的用法,示例,應用場景,與底層原理解析(這可能是最詳細的Block解析)

5.4 Block的複製

  • 在全域性block呼叫copy什麼也不做
  • 在棧上呼叫copy那麼複製到堆上
  • 在堆上呼叫block 引用計數增加
-(void) stackOrHeap{  
    __block int val =10;  
    blkt1 s= ^{  
        return ++val;};  
    s();  
    blkt1 h = [s copy];  
    h();  
}  
複製程式碼

不管block配置在何處,用copy方法複製都不會引起任何問題。在ARC環境下,如果不確定是否要copy這個block,那儘管copy即可。

最後的強調,在 ARC 開啟的情況下,除非上面的例外,預設只會有 NSConcreteGlobalBlockNSConcreteMallocBlock 型別的 block。

6. Block的底層研究方法

6.1 研究工具:clang

為了研究編譯器是如何實現 block 的,我們需要使用 clang。clang 提供一個命令,可以將 Objetive-C 的原始碼改寫成 c 語言的,藉此可以研究 block 具體的原始碼實現方式。

首先cd到程式碼檔案目錄

cd /Users/ChenMan/iOSTest/BlockTestApp
複製程式碼

然後執行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]));
    }
}
複製程式碼

執行情況:

iOS中Block的用法,示例,應用場景,與底層原理解析(這可能是最詳細的Block解析)

你會看到main.cpp

iOS中Block的用法,示例,應用場景,與底層原理解析(這可能是最詳細的Block解析)

6.2 實現分析

這裡只選取部分關鍵程式碼。

不難看出int main(int argc, char * argv[]) {就是主函式的實現。

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的一個C++的實現(最後面的_0代表是main中的第幾個block),也就是說也是一個結構體。

(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;
};
複製程式碼

其結構體成員如下:

  • isa,指向所屬類的指標,也就是block的型別
  • flags,標誌變數,在實現block的內部操作時會用到
  • Reserved,保留變數
  • FuncPtr,block執行時呼叫的函式指標

可以看出,它包含了isa指標(包含isa指標的皆為物件),也就是說block也是一個物件(runtime裡面,物件和類都是用結構體表示)。

(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)};
複製程式碼

其結構成員含義如下:

  • reserved:保留欄位
  • Block_size:block大小(sizeof(struct __main_block_impl_0))

以上程式碼在定義__main_block_desc_0結構體時,同時建立了__main_block_desc_0_DATA,並給它賦值,以供在main函式中對__main_block_impl_0進行初始化。

(4) __main_block_func_0

如上的main函式中,__main_block_func_0也是block的一個C++的實現

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
            printf("Hello, World!\n");
        }
複製程式碼
總結:綜合可知
  • __main_block_impl_0isa 指標指向了_NSConcreteStackBlock
  • 從main函式的main.cpp中看,__main_block_impl_0FuncPtr 指向了函式__main_block_func_0
  • __main_block_impl_0Desc 也指向了定義__main_block_desc_0時就建立的__main_block_desc_0_DATA,其中紀錄了block結構體大小等資訊。

以上就是根據編譯轉換的結果。當然,由於 clang 改寫的具體實現方式和 LLVM 不太一樣,有急切底層興趣的讀者可以進行更深入的研究。

相關文章