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
posted @ 2022-01-06 17:58  任淏  阅读(167)  评论(0编辑  收藏  举报