1. 程式人生 > 其它 >iOS底層原理探索-Block基本使用

iOS底層原理探索-Block基本使用

block是什麼?

block其實是一段程式碼塊,其作用是儲存一段程式碼塊,在真正呼叫block的時候,才執行block裡面的程式碼。

在程式裡面輸入inlineBlock,就可以得到block的宣告與定義形式:

/**
 等號前面是block的宣告;
 等號後面是block的定義;
 returnType:block宣告的返回型別
 blockName:block的名字
 parameterTypes:block宣告的引數型別
 parameters:block定義的引數型別及引數值
 statements:block程式碼塊
 */
returnType(^blockName)(parameterTypes) = ^(parameters) {
    statements
};//末尾的;不能省略
block的定義常見的有四種形式:
  1. 無引數,無返回值
  2. 無引數,有返回值
  3. 有引數,無返回值
  4. 有引數,有返回值

我們具體看一下四種常見的形式以及各個形式的型別都長什麼樣:

- (void)viewDidLoad {
    [super viewDidLoad];
    /**
     1.無引數,無返回值
     定義的時候,沒有返回值可以不寫()及裡面的內容
     其block型別是:void(^)(void)
     */
    void(^block1)(void) = ^{
        NSLog(@"block1");
    };
    
    /**
     2.1無引數,有返回值
     其block型別是:int(^)(void)
     */
    int(^block21)(void) = ^int{
        NSLog(@"block21");
        return 21;
    };
    
    /**
     2.2無引數,有返回值
     定義的時候,返回值可以省略
     其block型別是:int(^)(void)
     */
    int(^block22)(void) = ^{
        NSLog(@"block22");
        return 22;
    };
    
    /**
     3.有引數,無返回值
     有引數的情況下,宣告的引數型別必須寫
     有引數的情況下,定義的引數型別和引數名必須寫
     其block型別是:void(^)(int)
     */
    void(^block3)(int) = ^(int a){
        NSLog(@"block3---%d", a);
    };
    
    /**
     4.有引數,有返回值
     定義的時候,返回值int可以省略
     其block型別是:int(^)(int)
     */
    int(^block4)(int) = ^int(int a){
        NSLog(@"block4---%d", a);
        return 4;
    };
    
    /**block的呼叫*/
    block1();
    int a = block21();
    NSLog(@"%d", a);
    int b = block22();
    NSLog(@"%d", b);
    block3(3);
    NSLog(@"%d", block4(4));
}

執行結果:

2020-03-06 15:23:18.860990+0800 test001[3456:921319] block1
2020-03-06 15:23:18.861075+0800 test001[3456:921319] block21
2020-03-06 15:23:18.861100+0800 test001[3456:921319] 21
2020-03-06 15:23:18.861122+0800 test001[3456:921319] block22
2020-03-06 15:23:18.861141+0800 test001[3456:921319] 22
2020-03-06 15:23:18.861161+0800 test001[3456:921319] block3---3
2020-03-06 15:23:18.861190+0800 test001[3456:921319] block4---4
2020-03-06 15:23:18.861211+0800 test001[3456:921319] 4

通過以上程式碼,我們可以得出一下結論:

block在定義的時候,引數為空的時候,可以將定義裡面的()以及內容都省略
block在定義的時候,引數不為空的時候,引數值和引數名都不可以省略
block在定義的時候,返回值不管有或者沒有都可以省略

block的宣告

在interface裡面宣告block的時候,有兩種方法:

typedef void(^BLOCK2)(void);

@interface ViewController ()
/**在ARC下,strong和copy修飾block都可以*/

/**直接宣告,按照block的宣告樣式寫就可以*/
@property (strong, nonatomic) void(^block1)(void);

/**將BLOCK2進行定義型別,然後在定義變數block2*/
@property (strong, nonatomic) BLOCK2 block2;
@end
block使用場景-反向傳值

我們知道,兩個物件的傳值有兩種:正向傳值反向傳值
其中,正向傳值可以直接將值賦值過去完成傳值動作。
而反向傳值,一般有三種方法:代理、block和通知,然後,我們介紹一下block的反向傳值。

/**
ViewController
*/
#import "ViewController.h"
#import "TestViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    TestViewController *testVc = [[TestViewController alloc] init];
    testVc.myBlock = ^(int value) {
        NSLog(@"value = %d", value);
    };
    
    [self presentViewController:testVc animated:YES completion:nil];
}

/**TestViewController*/
@interface TestViewController : UIViewController
@property (strong, nonatomic) void(^myBlock)(int value);
@end

#import "TestViewController.h"
@interface TestViewController ()
@end

@implementation TestViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (self.myBlock) {
        self.myBlock(100);
    }
}
@end

兩次點選後,列印如下:
2020-03-06 17:49:31.981064+0800 test001[3497:936661] value = 100

第一次點選螢幕,會撥出TestViewController,
第二次點選螢幕,會呼叫myBlock(100),其實它是做了如下操作:

在TestViewController中myBlock(100);的呼叫等價於:
if (self.myBlock) {
        value = 100;
        ^(int value) {
            NSLog(@"value = %d", value);
        }();
    }

block在MRC下的記憶體儲存地址

首先我們要明確一點的是,block其實是一個物件,那麼,block這個物件,儲存在什麼區呢?
我們知道,記憶體分為五大區:
棧stack(系統) | 堆malloc、heap(手動) | 靜態區(全域性區) | 常量區 | 方法區(程式程式碼區)

block儲存在哪個區呢?

這個,需要根據專案是MRC或者ARC做具體的判斷。

在講MRC或者ARC前,我們先了解一些基本知識點:

ARC管理原則:
只要一個物件沒有被強指標引用,該物件就會被銷燬。
預設區域性變數物件都是使用強指標引用,並存放在堆裡面。
MRC管理原則:
MRC沒有strong、weak修飾指標,區域性變數物件做基本資料型別處理,基本資料型別統一放在棧區。
MRC常用知識點:
MRC給屬性賦值的時候,一定要用set方法,不能直接訪問下劃線成員屬性。因為,在MRC下的set方法會做一些其他事情,而直接用_成員屬性就不會做這些事情。

在MRC下,@property (retain, nonatomic) NSString *name;該句程式碼的set方法的實現轉換為下面程式碼:
- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

首先,我們看下在MRC環境下block的儲存位置
將Build Setting下的Objective-C Automatic Reference Counting設定為NO即是在MRC環境下

void(^block)(void) = ^{
    NSLog(@"呼叫了block");
};
NSLog(@"%@", block);
結果:<__NSGlobalBlock__: 0x1006a0080>

int a = 3;//區域性變數
void(^block)(void) = ^{
    NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSStackBlock__: 0x16f971328>

__block int a = 3;
void(^block)(void) = ^{
    NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSStackBlock__: 0x16f935310>

static int a = 3;//static修飾區域性變數
void(^block)(void) = ^{
    NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSGlobalBlock__: 0x1008ec080>

int global = 5;
- (void)viewDidLoad {
    [super viewDidLoad];
    void(^block)(void) = ^{
        NSLog(@"呼叫了block, global = %d", global);
    };
    NSLog(@"%@", block);
}
結果:<__NSGlobalBlock__: 0x100934080>

static int global = 5;
- (void)viewDidLoad {
    [super viewDidLoad];
    void(^block)(void) = ^{
        NSLog(@"呼叫了block, global = %d", global);
    };
    NSLog(@"%@", block);
}
結果:<__NSGlobalBlock__: 0x100be8080>

__block int global = 5;//__block修飾全域性變數,報錯

通過程式碼可以看出,如果使用retain修飾block,則self.block存放在棧區。棧是由系統自動控制,則在程式碼塊{}執行完畢後,self.block將被回收,而在touchesBegan方法中還呼叫self.block,報野指標錯誤。
然後,我們看下使用copy修飾block:

@property (copy, nonatomic) void(^block)(void);

結果:<__NSMallocBlock__: 0x28237b2d0>
觸控點選列印:呼叫了block, a = 3

(其他程式碼與上面相同)

這是因為,使用copy修飾block,self.block儲存在堆區,而堆記憶體區域是由程式設計師自己控制的,因此,在viewDidLoad方法執行完畢後,self.block的記憶體地址並沒有被回收,因此在touchesBegan方法中呼叫self.block();沒有問題。

總結:

在MRC下
block屬性必須使用copy,而不能使用retain修飾

void(^block)(void) = ^{
    NSLog(@"呼叫了block");
};
NSLog(@"%@", block);
結果:<__NSGlobalBlock__: 0x100b5c098>

int a = 3;//區域性變數
void(^block)(void) = ^{
    NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSMallocBlock__: 0x28343ea60>

__block int a = 3;
void(^block)(void) = ^{
    NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSMallocBlock__: 0x2805b11d0>

static int a = 3;//static修飾區域性變數
void(^block)(void) = ^{
    NSLog(@"呼叫了block, a = %d", a);
};
NSLog(@"%@", block);
結果:<__NSGlobalBlock__: 0x100f38098>

int global = 5;
- (void)viewDidLoad {
    [super viewDidLoad];
    void(^block)(void) = ^{
        NSLog(@"呼叫了block, global = %d", global);
    };
    NSLog(@"%@", block);
}
結果:<__NSGlobalBlock__: 0x100564098>

static int global = 5;
- (void)viewDidLoad {
    [super viewDidLoad];
    void(^block)(void) = ^{
        NSLog(@"呼叫了block, global = %d", global);
    };
    NSLog(@"%@", block);
}
結果:<__NSGlobalBlock__: 0x100b54098>

__block int global = 5;//__block修飾全域性變數,報錯

總結:

在ARC下
block本身是儲存在全域性區;
如果block引用了外部區域性變數,或者引用了被__block修飾的外部區域性變數,則存放在堆區。
被__block修飾的block還是區域性變數;
被static修飾的區域性變數,改變區域性變數的宣告週期。
在ARC中使用copy修飾block

@interface ViewController ()
@property (copy, nonatomic) void(^block)(void);
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    int a = 3;
    void(^block)(void) = ^{
        NSLog(@"呼叫了block, a = %d", a);
    };
    self.block = block;
    NSLog(@"%@", self.block);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    self.block();
}

結果:<__NSMallocBlock__: 0x28065c0f0>
觸控點選列印:呼叫了block, a = 3

在ARC中使用strong修飾block

@property (strong, nonatomic) void(^block)(void);

結果:<__NSMallocBlock__: 0x2802b5ef0>
觸控點選列印:呼叫了block, a = 3

在ARC中使用weak修飾block

@property (weak, nonatomic) void(^block)(void);

結果: <__NSMallocBlock__: 0x282cdb3c0>
觸控點選崩潰,報EXC_BAD_INSTRUCTION錯誤

在ARC中使用assign修飾block

在ARC下
block屬性可以使用copy或者strong修飾,不能使用weak或者assign修飾
通過ARC管理機制我們知道,如果沒有任何強指標引用,則物件會被清空。所以,用weak或者assign沒有對block進行強指標引用,因此,在viewDidLoad方法執行完畢後,block就被清空,再次使用self.block會造成野指標錯誤。
在ARC下,string和block用copy還是strong?

其實兩個都可以使用,但是還是建議使用strong。
string使用copy修飾,只是淺拷貝,並沒有建立新的物件,所以,strong就可以滿足。
block使用copy修飾,也沒有新的物件建立,所以,strong就可以滿足。
而,strong和copy不一樣的地方在於,使用copy修飾的屬性,在set方法中,會有一些列的copy操作,而strong並不需要,從效能上說,strong高一些。

Block的迴圈引用注意事項

block會對裡面所有的外部變數物件進行強引用。

int a = 3;//區域性變數
void(^block)(void) = ^{
    NSLog(@"呼叫了block, a = %d", a);
};
a = 4;
block();
結果:呼叫了block, a = 3

static int a = 3;//static修飾區域性變數
結果:呼叫了block, a = 4

__block int a = 3;//__block修飾區域性變數
結果:呼叫了block, a = 4

int a = 3;//全域性變數
結果:呼叫了block, a = 4

static int a = 3;//static修飾全域性變數
結果:呼叫了block, a = 4

總結

block呼叫區域性變數是值傳遞;
使用static或者__block修飾的區域性變數是指標傳遞;
全域性變數和使用static修飾的全域性變數,block沒有捕獲全域性變數,因此,在block內部可以修改全域性變數
上面的總結第三點其實是不對的,為什麼呢?

為什麼是這樣的結果呢???

想了解更多的小夥伴,可以看這兩篇文章,為你答疑解惑。
iOS之Block本質(一)
iOS之Block本質(二)

block作為引數

block作為引數使用,在UIView的動畫裡面很常見,例如:

[UIView animateWithDuration:3.0 animations:^{   
}];

其方法是:
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations

(void (^)(void))是一個引數為void,返回值為void的block型別,animations是block變數名。
可以看出,block作為引數時,使用 (block型別)block變數名 的形式定義。

我們什麼時候讓block作為引數使用呢?

這個可以想下,我們什麼時候使用動畫。
在3.0內,執行animations裡面的內容,什麼時候執行這個方法,是由UIView呼叫animateWithDuration方法決定的,至於裡面要執行的什麼動畫,則是一個block,是一個程式碼塊,是可以由程式設計師自己定義自己寫的。

換言之,block程式碼塊裡面的內容是程式設計師儲存的一端程式碼,寫完並沒有立馬執行。什麼時候執行呢?是由UIView呼叫animateWithDuration方法執行的。

舉一個例子:

@interface Calculator : NSObject
@property (assign, nonatomic) int result;
//定義一個方法,引數型別是int(^)(int),引數名是block
- (void)jisuan:(int(^)(int))block;
@end

@implementation Calculator
//方法的實現
- (void)jisuan:(int(^)(int result))block
{
    if (block) {
        //呼叫block,並將block呼叫的結果賦值給result
        _result = block(_result);
    }
}
@end

Calculator *calculator = [[Calculator alloc] init];
[calculator jisuan:^int(int result) {//block的引數名不可省略,block的返回值型別可以省略(第一個int)
    result += 5;
    result *= 2;
    return result;
}];

NSLog(@"%d", calculator.result);
結果:10

這個例子中,jisuan:方法裡面的引數是一個block,當呼叫這個方法的時候,呼叫的block,執行block裡面的程式碼。

block作為返回值

block作為返回值的經典代表就是Masonry三方框架,基本上整個框架都是使用的Block作為返回值。

拿一個簡單的Masonry佈局程式碼進行分析:

[thirdView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.right.equalTo(bottomView);
        make.top.equalTo(_secondView.mas_bottom);
        make.height.equalTo(@70);
}];

make.left.right.equalTo(bottomView);
程式碼中,從前往後執行,首先是make.left,該語法是一個get方法,自動尋找是否有left方法,並返回物件本身(型別MASConstraintMaker),make.left.right以及make.left.right.equalTo都是返回物件本身(型別MASConstraintMaker),則最後是(MASConstraintMaker型別)(bottomView)。其實iMASConstraintMaker型別是一個block型別,最後的呼叫是對該block進行了呼叫,引數是bottomView。

來個簡單的block作為返回值的例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.test();
}
- (void(^)(void))test{
    //方法一:定義一個變數名為block的blocke,並返回
    void(^block)(void) = ^{
        NSLog(@"呼叫test函式返回block");
    };
    return block;
    
    //方法二:直接返回一個block
    return ^{
        NSLog(@"呼叫test函式返回block");
    };
}

結果:呼叫test函式返回block

接下來,我們做一個簡單的連續進行加法計算的例子

@interface Calculator : NSObject
@property (assign, nonatomic) int result;
//定義一個方法,返回值是其本身
- (Calculator *)jisuanAdd:(int)value;
@end

@implementation Calculator
//方法的實現
- (Calculator *)jisuanAdd:(int)value
{
    self.result += value;
    return self;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    Calculator *calculator = [[Calculator alloc] init];
    [[[calculator jisuanAdd:2] jisuanAdd:5] jisuanAdd:3];
    NSLog(@"%d", calculator.result);
}

結果:10

通過上面的例子,我們可以實現連續進行加法的計算,我們對例子進行改進,使其可以跟Masonry一樣,進行鏈式程式設計。

@interface Calculator : NSObject
@property (assign, nonatomic) int result;
//定義一個方法,返回值是一個block,該block是一個引數為int,返回值為Calculator的型別
- (Calculator *(^)(int))jisuanAdd;
@end

@implementation Calculator
//方法的實現
- (Calculator *(^)(int))jisuanAdd
{
	//返回一個block
    return ^(int value){
        self.result += value;
        return self;
    };
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    Calculator *calculator = [[Calculator alloc] init];
    calculator.jisuanAdd(2).jisuanAdd(5).jisuanAdd(3);
    NSLog(@"%d", calculator.result);
}

結果:10