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的定义常见的有四种形式:
- 无参数,无返回值
- 无参数,有返回值
- 有参数,无返回值
- 有参数,有返回值
我们具体看一下四种常见的形式以及各个形式的类型都长什么样:
- (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