关于block--你想了解的几乎都在这里了
一.block定义
二.block的本质
三.block变量捕获(Capture)
四.block的类型
五.block的copy操作
六.block使用了对象类型的auto变量的分析
七.
__block
讲解八.block的循环引用问题
九.block的一些使用场景
block
在我们开发中随处可见,比如我们常用的AFN
框架和ReactiveCocoa
框架等,多线程中的GCD
,各种方法的回调等等,可见block
在我们开发中的重要性,今天我就详细的整理下关于block
的一些东西,如果有什么问题也欢迎大家一起讨论.
一.block定义
这里借用网上的一张截图,感觉还是比较详细的
上图声明的block
类型为:int (^)(int)
注意:
block
块中的代码并没有执行,只有明确调用的时候才会具体执行
同时可以使用inlineBlock
的快捷方式生成一段block
代码
使用typedef对block进行重定义
对于可能需要重复地声明多个相同返回值相同参数列表的block变量,如果总是重复地编写一长串代码来声明变量会非常繁琐,所以我们可以使用typedef来定义block类型
typedef int(^sumBlock)(int , int);
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
sumBlock myblock = ^(int a,int b)
{
return a+b;
};
myblock(20,30);
}
二.block的本质
block使用起来非常的方便,那么他的底层或者说是本质到底是什么呢? 下面我们先通过一个最简单的block来分析下它的底层到底是如何实现的:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"Hello World");
};
block();
}
return 0;
}
通过下面的命令将我们的.m文件转化成.cpp文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
生成的C++文件如下图所示,为了便于理解我去掉了部分强制转换的东西:
image通过以上的代码分析我们我们可以得出一个结论:
block本质上是一个OC对象,它的内部也有一个isa指针
当然我们也可以通过下面的代码来验证我们的结论:
void (^block)(void) = ^{
NSLog(@"Hello World");
};
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
执行结果:
block[4176:730867] __NSGlobalBlock__
block[4176:730867] __NSGlobalBlock
block[4176:730867] NSBlock
block[4176:730867] NSObject
可以看到block对象是继承自NSObject的一个OC对象.
变量捕获(Capture)
局部变量auto
因为上面定义的block太过简单,一些本质的东西我们无法看到,这里我们定义一个稍微复杂一点的block对象,代码如下:
int age = 10;//等价于 auto int age = 10;
void(^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 20;
block();
执行该block后打印的age的值是多少呢?
相信大部分同学立马就可以回答出来结果,但是为什么是这个结果呢? 相信大部分同学应该挺迷惑的,不着急我们一步步来为大家分析:
同样将该段代码转化为C++代码,(为了便于查看之后的所有关于强制转换的东西我都会删除)
image通过以上分析我们就可以清晰的看出block在内部捕获了age,并且是值传递,所以age在block外部修改之后并不会影响到其内部的值.
该段代码结构其实可以简化成下面这幅图:
image
局部变量Static
下面我们来看一个稍微复杂点的代码:
int age = 10;//等价于 auto int age = 10 ,auto可以省略
static int height = 10;
void(^block)(void) = ^{
NSLog(@"age is %d,height is %d",age,height);
};
age = 20;
height = 20;
block();
执行该block后打印的结果是多少呢?
老规矩,同样将该段代码转化为C++代码,如下图所示:
image
通过以上代码我们可以看到block可以同时捕获到age和height,但是用Static修饰的height和age是不一样的,age是一个值传递,而height是一个引用传递,所以在block外部即使height的数据变更我们也可以获取到最新的数据.
那么不知道大家有没有想过这样一个问题,为什么用static
修饰的变量是引用传递,而用auto修饰的变量就是值传递呢?
下面我们通过一段简单的代码来为大家分下一下:
其实就是作用域的问题,当代码执行到23行的时候也就是我们执行完test函数后,此时age的内存已经销毁,如果block不马上将age的值捕获到其内部,那么执行到24行的时候,访问到的age此时就是一个垃圾数据了,而用static修饰的height就不一样了,所以传递的是地址,将来height的数据即使更新了,我们也可以获取到最新的数据.
全局变量
上面的两种情况都是局部变量,下面我们在来看看全局变量的情况:
#import <Foundation/Foundation.h>
int age = 10;
static int height = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"age is %d,height is %d",age,height);
};
age = 20;
height = 20;
block();
}
return 0;
}
打印结果:
age is 20,height is 20
转化为C++代码:
通过上面代码我们可以看到,
__main_block_impl_0
内部并没有捕获age和height.所以打印结果就是最新的数据,通过上面的分析我们应该知道因为age和height都是全局变量,在任何地方都可以访问到他们,所以block并不用去捕获他们
总结:
关于block捕获变量我们可以总结成下面一个表格,清晰明了:
image
block的类型
先说结论:block对象有3种类型,分别为:
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
他们都是继承自NSBlock类型.我们都知道应用程序在内存中分为 代码段
数据段
堆段
和栈段
,其中堆段
是我们程序员自己动态分配的,并且需要我们手动释放内存(free()或者release()操作
),而栈段
是非常危险的,随时都有可能会被系统销毁.
因为下图比较经典我就直接拿过来用了
image
那么这三种类型之间如何区分以及有什么区别呢?下面我们先来看一幅图:
结论一:如果block没有访问auto变量则其类型为:
__NSGlobalBlock
对于这种类型的block
我们不需要考虑作用域的问题,而且对他进行copy
或者retain
操作也是无效的.
验证:
#import <Foundation/Foundation.h>
int height = 20;
int main(int argc, const char * argv[]) {
@autoreleasepool {
//没有访问任何变量
void (^block1)(void) = ^{
NSLog(@"Hello World");
};
//访问全局变量
void (^block2) (void) = ^{
NSLog(@"height is %d",height);
};
//访问static修饰的局部变量
static int age = 10;
void (^block3) (void) = ^{
NSLog(@"age is %d",age);
};
NSLog(@"%@--%@---%@",[block1 class],[block2 class],[block3 class]);
}
return 0;
}
打印结果:
block[4486:803768] __NSGlobalBlock__--__NSGlobalBlock__---__NSGlobalBlock__
结论二:如果block访问了auto修饰的变量则其类型为:__NSStackBlock
验证:
int age = 10;
void (^block) (void) = ^{
NSLog(@"age is %d",age);
};
NSLog(@"%@",[block class]);
打印结果:
block[4524:811710] __NSMallocBlock__
恩? 怎么是__NSMallocBlock__
?搞错了吧.....
不着急,因为我们的项目是ARC环境,编译器帮助我们做了一些事情,我们将代码改成MRC环境:
image
再次执行:
block[4577:821982] __NSStackBlock__
这才对嘛
但是因为__NSStackBlock__
的内存是分配在栈段
的,所以在使用的时候经常会出现一些问题,比如:
#import <Foundation/Foundation.h>
void (^block) (void);
void test()
{
int age = 10;
block = ^{
NSLog(@"age is %d",age);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
执行结果:
block[4919:908731] age is -272632360
纳尼😱?!!! 怎么是这么个乱七八糟的数据?
因为该block的类型是__NSStackBlock__
,所以当test()
函数执行完毕后,block对象中成员已经被释放掉了,所以当我们在执行block()
的时候拿到的数据就是垃圾数据了,而不是我们想要的10.
那么该如何解决该问题呢?
我们只需要将block进行copy操作即可.
void test()
{
int age = 10;
block = [^{
NSLog(@"age is %d",age);
} copy];
}
执行结果:
block[5004:929720] age is 10
所以在开发中,我们经常会对 block进行Copy操作,原因就是因为这个.
所以结论三 NSStackBlock 调用了copy后会变成 NSMallocBlock
每一种类型的block调用Copy操作后的结果如下图所示:
image
block的copy操作
在上文中我们的环境是MRC环境,如果是ARC环境的时候 block内部使用了auto修饰的局部变量时,该block的类型是NSMallocBlock,为什么会这样呢?因为在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况:
1. block作为函数返回值时
2. 将block赋值给__strong指针时
3. block作为Cocoa API中方法名含有`usingBlock`的方法参数时
4. block作为`GCD API`的方法参数时
5. ....
.
.
.
基于以上结论:
MRC下block属性的建议写法
@property (copy, nonatomic) void (^block)(void);
ARC下block属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
但是为了统一,其实我们更建议不管是ARC环境还是MRC环境下都用copy修饰.
block使用了对象类型的auto变量的分析
因为上面我们分析的结果都是基于基本数据类型的,有些问题涉及不到,这里我们分析一种复杂的情况 , 即 block内部使用了对象类型的变量
首先我们定义一个Person对象
@interface Person : NSObject
@property (nonatomic,assign) int age;
@end
在该对象内部调用dealloc
方法
#import "Person.h"
@implementation Person
-(void)dealloc{
NSLog(@"Person -------dealloc");
}
@end
image
执行程序后 代码执行到20行的时候 调用了Person对象的dealloc方法 , 哥们 你在逗我么 这个地球人都知道好吧.....
👌 不着急,我们一步步来 上面是预热
将我们的编译环境切换到MRC环境,执行下面的代码
image
断点停留在了25行,此时block还没有销毁,但是打印结果显示Person已经挂掉了
结论一:如果block是在栈上,将不会对auto变量产生强引用
同样的代码将编译环境切换到ARC,执行结果如下图:
ARC环境下,当代码执行到24行的时候person对象并没有销毁,说明block对象对person有一个强引用
如果我们将Person对象用__weak
修饰呢?结果又是什么样子呢?如下图所示:
那么其底层是如何操作的呢?执行下面的命令将代码转化为C++代码:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
image
总结:当block内部访问了对象类型的auto变量时
如果block是
__NSStackBlock__
类型,将不会对auto变量产生强引用
如果block被拷贝到堆上,则会调用block内部的copy函数,copy函数内部会执行
_Block_object_assign
函数,该函数内部会根据auto变量的修饰符__strong 、__weak而做出相应的操作,如果是__strong 则形成强引用,而__weak 则会形成弱引用关系.
如果block从堆上移除,则会调用block内部的dispose函数,dispose函数内部会调用
_Block_object_dispose
函数,该函数会自动释放引用的auto变量,类似于release操作.
练习题:
仔细看下面的代码,Person对象会在什么时候销毁?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
Person *person = [[Person alloc] init];
person.age = 18;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"age is %d",person.age);
});
NSLog(@"Hello World");
}
分析过程:因为GCD中的block是在堆段
中,所以block会一直存在直到执行完毕之后才会销毁,因为此处Person对象是一个__Strong
,所以block会对Person对象形成一个强引用,所以Person对象会在3秒后Blcok执行完毕后释放
执行结果:
__block讲解
在具体讲解__block
之前我们看一段代码:
我们想在block内部去修改age的值,但是此时系统编译失败并且提示我们缺少
__block
修饰符,首先我们来思考下为什么在block内部不能去修改auto修饰的局部变量呢?其实仔细看过上面的分析之后我们肯定知道答案了,因为作用域的原因,自动变量随时都有可能被销毁, 所以在block内部是不能去更改auto修饰的自动变量的.
除了系统给我们的提示, 添加__block
修饰符外,其实我们还可以通过添加static
修饰符,因为static
修饰的局部变量是一个地址传递,所以完全没有问题.
或者将age放到函数外部,也就是将age设置成一个全局变量,这样也是可以更改的
但是不管是static
修饰还是全局变量,age的性质就改变了 所以此处最好还是用__block
修饰,那么__block
内部做了什么事情呢?老规矩..
我们通过C++代码可以看到编译器会将__block
变量包装成一个对象,然后将age的地址值传递给该对象,通过修改该对象内部的age来达到目的.
注意:
__block可以用于解决block内部无法修改auto变量值的问题,但是不能修饰全局变量、静态变量(static)
__block的内存管理
当block在栈上时,仅仅是使用了__block变量,并没有对__block变量产生强引用
当block被copy到堆时
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign
函数
_Block_object_assign
函数会对__block
变量产生强引用(仅当ARC时才有效,MRC时候并不会对__block
变量产生强引用)
当block从堆中移除时
会调用block内部的dispose
函数
dispose
函数内部会调用_Block_object_dispose
函数
_Block_object_dispose
函数会自动释放引用的__block
变量
Block的循环引用问题
两个对象相互持有,这样就会造成循环引用,如下图所示:
2图中,对象A持有对象B,对象B持有对象A,相互持有,最终导致两个对象都不能释放。
列举个简单的例子:
Person.h
@interface Person : NSObject
/** 名字 */
@property (nonatomic, copy) NSString *name;
/** block */
@property (nonatomic, copy) void (^dosomethingBlock)();
@end
Person.m
@implementation Person
-(void)dealloc
{
NSLog(@"%s", __func__);
}
@end
在ViewController
中去实例化Person
对象:
- (void)viewDidLoad {
[super viewDidLoad];
Person *per = [[Person alloc] init];
per.name = @"Jack";
per.dosomethingBlock = ^{
NSLog(@"-----------%@", per.name);
};
}
@end
运行上面的代码,发现并没有调用 Person
对象中的 dealloc
方法.说明此时发生了循环引用的问题,
通过上文这么长时间的分析,我相信大家一定知道block内部做了什么事情, 因为 Person
对象是用__strong
修饰的,所以此时 __Block_Object_Assign方法
内部会自动产生一个【强引用】指向【对象Person】
那么此时我们该如何解决这个问题呢? 通常的做法是创建一个 __weak
修饰的弱引用 指向 person对象,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
Person *per = [[Person alloc] init];
__weak typeof(Person) *weakPerson = per;
per.name = @"Jack";
per.dosomethingBlock = ^{
NSLog(@"-----------%@", weakPerson.name);
};
}
运行上面的代码,发现 Person
对象调用了 dealloc
方法.
Block的嵌套
还是以上面的例子,我们在 per.dosomethingBlock
中延迟三秒后打印 weakPerson.name
,仔细查看下面的代码,你会发现什么问题?
- (void)viewDidLoad {
[super viewDidLoad];
Person *per = [[Person alloc] init];
__weak typeof(Person) *weakPerson = per;
per.name = @"Jack";
per.dosomethingBlock = ^{
NSLog(@"beign-------");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(),
^{
NSLog(@"after-----------%@", weakPerson.name);
});
};
per.dosomethingBlock();
}
控制台输出结果:
2017-08-14 17:48:17.441 block[23041:1182337] beign-------
2017-08-14 17:48:17.441 block[23041:1182337] -[Person dealloc]
2017-08-14 17:48:20.729 block[23041:1182337] after-----------(null)
通过打印结果我们可以看到当 Block
被执行的时候立马打印了 beign-------
信息,然后紧接着 Person
对象被销毁, 3秒以后打印了 after-------
信息,注意因为此时 Person
对象已经被销毁了,所以打印出了 Null
所以 在延迟执行的Block内部 为了保住 Person
对象不被销毁 我们需要使用一个强引用来保住 Person对象的命,稍微更改下代码:
- (void)viewDidLoad {
[super viewDidLoad];
Person *per = [[Person alloc] init];
__weak typeof(Person) *weakPerson = per;
per.name = @"Jack";
per.dosomethingBlock = ^{
NSLog(@"beign-------");
Person *strongPerson = weakPerson;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(),
^{
NSLog(@"after-----------%@", strongPerson.name);
});
};
per.dosomethingBlock();
}
控制台输出结果:
2017-08-14 17:50:04.947 block[23068:1184458] beign-------
2017-08-14 17:50:08.234 block[23068:1184458] after-----------Jack
2017-08-14 17:50:08.234 block[23068:1184458] -[Person dealloc]
通过上面的结果我们可以看到 在 dispatch_after
中使用了 一个强引用 strongPerson
来保住了 Person
对象的命,所以此时才是我们想要的结果.
block的一些使用场景
将block作为对象属性,恰当时机的时候才去调用
场景一
比如我们从A控制器Modal到B控制器,在B控制器中点击了某个按钮后需要给A控制器传递一些数据,当然这种也可以使用代理来实现,但是我感觉 使用block更为简洁:
B控制器:
@interface BViewController : UIViewController
@property (nonatomic, strong) void(^buttonClickBlock)(NSString *param);
@end
-(void)buttonClick{
if (_buttonClickBlock) {
_buttonClickBlock(@"需要传递的参数");
}
}
A控制器:
BViewController *modalVc = [[BViewController alloc] init];
modalVc.view.backgroundColor = [UIColor brownColor];
modalVc.buttonClickBlock = ^(NSString *param) {
NSLog(@"传递过来的参数:%@",param);
};
// 跳转
[self presentViewController:modalVc animated:YES completion:nil];
场景二:
有一个UITableView
,此时点击每一行都会做不同的事情,如果不用block 你是不是想着在点击的时候去判断行号,然后根据行号在去做对应的事情? 然而通过block我们可以简化这个流程:
在对应的实体模型中,我们只需要定义一个具体做事的block,例如:
@interface CellItem : NSObject
@property (nonatomic, strong) NSString *title;
// 保存每个cell做的事情
@property (nonatomic, strong) void(^doSomethingBlock)();
@end
然后在tableView
中:点击的时候执行对应的代码块就可以了,是不是一下轻松了许多
- (void)viewDidLoad {
[super viewDidLoad];
// 创建模型
CellItem *item1 = [CellItem itemWithTitle:@"打电话"];
// 把要做的事情(代码)保存到模型
item1.doSomethingBlock = ^{
NSLog(@"打电话");
};
CellItem *item2 = [CellItem itemWithTitle:@"发短信"];
item2.doSomethingBlock = ^{
NSLog(@"发短信");
};
CellItem *item3 = [CellItem itemWithTitle:@"发邮件"];
item3.doSomethingBlock = ^{
NSLog(@"发邮件");
};
_items = @[item1,item2,item3];
}
// 点击cell的时候执行对应的block代码块就可以了
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
CellItem *item = self.items[indexPath.row];
if (item.doSomethingBlock) {
item.doSomethingBlock();
}
}
将block当作函数参数来传递
当我们封装一个方法的时候,而这个方法具体要做什么事情不是方法内部能够决定的,但什么时候做是由内部决定的,(即内部决定执行时间,而外部传入具体做些什么)——这个时候就可以使用block来作为函数参数
列举一个经常使用的例子:
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion
对于系统提供的这个动画方法,要执行什么样的动画,执行完动画要做什么事情,该方法内部是不知道的,需要交给用户自己去实现
场景二:
要求封装一个计算器类CacultorManager
,该类提供一个计算方法:怎么计算是由外界决定的,什么时候计算由内部决定.
CacultorManager.h
@interface CacultorManager : NSObject
/***保存计算的结果**/
@property (nonatomic, assign) NSInteger result;
- (void)cacultor:(NSInteger(^)(NSInteger result))cacultorBlock;
@end
CacultorManager.m
@implementation CacultorManager
- (void)cacultor:(NSInteger (^)(NSInteger))cacultorBlock
{
if (cacultorBlock) {
_result = cacultorBlock(_result);
}
}
@end
具体使用该类:
- (void)viewDidLoad {
[super viewDidLoad];
// 创建计算器管理者
CacultorManager *mgr = [[CacultorManager alloc] init];
[mgr cacultor:^(NSInteger result){
result += 5;
result -= 6;
return result;
}];
NSLog(@"%ld",mgr.result);
}
@end
将block当作函数返回值来传递
相信大家之前都写过类似这样的代码
[View mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(anotherView);
make.left.equalTo(anotherView);
make.width.mas_equalTo(@60);
make.height.mas_equalTo(@60);
}];
类似于 Masonry
这种可以连续.top.equalTo(anotherView);
的写法我们称为链式编程
,而实现这种思路的重点在于方法的返回值必须是block,而block必须返回本身,block的参数就是我们需要操作的数据
下面我们就来写一个类似这样的小功能:还是拿上面的计算器类CalculatorManager
来举例子:
CalculatorManager.h
@interface CalculatorManager : NSObject
/***保存计算的结果**/
@property (nonatomic, assign) int result;
- (CalculatorManager *(^)(int))add;
- (CalculatorManager *(^)(int))sub;
@end
CalculatorManager.m
@implementation CalculatorManager
- (CalculatorManager *(^)(int))add
{
return ^(int value){
_result += value;
return self;
};
}
- (CalculatorManager *(^)(int))sub
{
return ^(int value){
_result -= value;
return self;
};
}
@end
在实现类中:
- (void)viewDidLoad {
[super viewDidLoad];
CalculatorManager *mgr = [[CalculatorManager alloc] init];
mgr.add(5).add(5).sub(6).add(5);
NSLog(@"%d",mgr.result);
}
@end