本文为L_Ares个人写作,以任何形式转载请表明原文出处。
准备 : libclosure-73源码文件
一、Block的基本概念
1. 什么是Block
Block
是带有局部变量
的匿名函数
。Block
又被称作闭包
。
闭包 = 一个函数(或一个指向函数的指针) + 该函数执行的外部的上下文变量(也可以叫自由变量)
2. Block的作用
- 保存某一段代码块,在合适的地方再进行调用。
3. Block的定义和使用
- 通用声明格式 :
(返回值类型)(^Block块名称)(参数类型)
- 例如 :
(int)(^JDBlock)(int)
- 通用定义格式 :
^(参数类型 参数名){函数体}
- 例如 :
^(int num){return num * 10;}
- Block如果声明了返回值类型,则Block块内所有
return
的变量类型必须和声明的返回值类型一致。
3.1 无参数、无返回值的Block
1.无参数、无返回值的block
//声明
void(^block0)(void);
//定义
block0 = ^(void){
NSLog(@"无参数、无返回值的Block");
};
//调用
block0();
3.2 无参数、有返回值的Block
2.无参数、有返回值的block
/** 假设返回值类型为 : int **/
//声明
int(^block1)(void);
//定义
block1 = ^{
NSLog(@"无参数、有返回值的Block");
return 1;
};
//调用
NSLog(@"block1返回了 : %d",block1());
由3.1和3.2可以看出,无参数的
Block
,无论是否有返回值,在其定义的时候,参数(void)
可以省略不写。
3.3 有参数、无返回值的Block
3.有参数、无返回值的block
/** 假设存在2个参数,且参数类型为 : int和NSString **/
//声明
void(^block2)(int num, NSString *value);
//定义
block2 = ^(int a, NSString *b){
NSLog(@"有参数、无返回值的Block");
NSLog(@"%d --- %@",a,b);
};
//调用
block2(1,@"我是参数2");
3.4 有参数、有返回值的Block
/** 4.有参数、有返回值的block **/
/** 假设 :
返回值类型为 : int
存在2个参数,且参数类型为 : int和NSString
*/
//声明
int(^block3)(int, NSString*);
//定义
block3 = ^(int a, NSString *b){
NSLog(@"有参数、无返回值的Block");
NSLog(@"%d --- %@",a,b);
return a + [b intValue];
};
//调用
NSLog(@"block3返回了 : %d",block3(1,@"2"));
由3.3和3.4可以看出,有参数的
block
具有 :
- 声明时,可以随意命名参数名,也可以不写参数名,只写参数类型。
- 但是定义的时候,参数类型必须和声明中的一致,并且必须随意写明参数名。
3.5 实际开发中的Block
在我们实际的开发中,通常会使用
typedef
来定义一个Block
,这时,Block
就会变成一个Block类型
。
- 声明格式 :
typedef 返回值类型 (^Block类型的名称)(参数列表)
- 定义格式 :
Block类型对象 = ^(返回值类型)(参数列表){函数体};
例如 :
#import "ViewController.h"
typedef int(^JDBlock)(int,int);
@interface ViewController ()
@property (nonatomic,copy) JDBlock jd_block;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self jd_block_basic];
}
- (void)jd_block_basic
{
//typedef的Block
self.jd_block = ^int(int a, int b) {
return a + b;
};
NSLog(@"JDBlock的结果 : %d",self.jd_block(1,2));
}
4. Block与外部变量的关系
4.1 只捕获外部变量,不做修改
Block
是将外部变量其复制到Block
自己的数据结构中。Block
只针对Block内部使用的外部变量
进行捕获,未被Block
内部使用的则不捕获。- 默认情况下,
Block
只能捕获,但是不能修改外部的局部变量的值。Block
捕获外部的局部变量后,即使外部的局部变量发生了改变,Block
捕获到的变量也依然是捕获时候的值,不会跟着改变。
关系图 :
举例 :
- (void)jd_block_test1
{
int number = 8;
void(^test0_block)(void) = ^{
NSLog(@"number = %d",number);
};
test0_block();
number = 10;
test0_block();
}
举例结果 :
4.2 捕获外部变量,并且修改
- 对于使用
__block
修饰的外部局部变量,Block
在捕获后,可以修改它的值。- 对于使用
__block
修饰的外部局部变量,Block
在捕获后,可以获取它修改后的值__block
的作用是复制外部局部变量的引用地址到Block
的数据结构内。
关系图 :
举例 :
- (void)jd_block_test2
{
__block int number = 8;
void(^test1_block)(void) = ^{
NSLog(@"number = %d",number);
};
test1_block();
number = 10;
test1_block();
}
举例结果 :
二、Block的分类
1. 普通Block的分类
- 执行 :
- (void)jd_block_test3
{
//1.GlobalBlock
void(^GlobalBlock)(void) = ^{
NSLog(@"GlobalBlock");
};
NSLog(@"%@",GlobalBlock);
//2.MallocBlock && StackBlock
int a = 10;
void(^MallocBlock)(void) = ^{
NSLog(@"MallocBlock - %d",a);
};
NSLog(@"%@",MallocBlock);
}
- ARC下执行结果 :
- 非ARC下执行结果 :
- 单独给文件设置
非ARC模式
的方法如下图 :
设置
-fobjc-arc
就是ARC
模式。
设置-fno-objc-arc
就是非ARC
模式。
- 结论 :
普通的
Block
拥有3种分类 :
1.NSGlobalBlock
: 全局Block
2.NSStackBlock
: 栈Block
3.NSMallocBlock
: 堆Block
2.Block总共分类
打开准备的libclosure-73
文件,打开data.c
文件,可以看到 :
Block
实际上有6种分类。
Block公有6种分类 :
1.void * _NSConcreteStackBlock[32] = { 0 };
2.void * _NSConcreteMallocBlock[32] = { 0 };
3.void * _NSConcreteAutoBlock[32] = { 0 };
4.void * _NSConcreteFinalizingBlock[32] = { 0 };
5.void * _NSConcreteGlobalBlock[32] = { 0 };
6.void * _NSConcreteWeakBlockVariable[32] = { 0 };
三、Block的常见用法
1. Block作为属性
这个就太常见了,例子就放两个控制器之间传值,不再过多解释了。
举例 :
Controller1 :
#import "ViewController.h"
#import "JDViewController.h"
@interface ViewController ()
@property (nonatomic,copy) NSString *str;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%@",self.str);
}
- (IBAction)btnClick:(id)sender {
JDViewController *vc = [[JDViewController alloc] init];
__weak typeof(self)weakSelf = self;
vc.testBlock = ^(NSString *str){
weakSelf.str = str;
};
[self.navigationController pushViewController:vc animated:YES];
}
Controller2 :
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef void(^TestBlock)(NSString *);
@interface JDViewController : UIViewController
@property (nonatomic,copy) TestBlock testBlock;
@end
NS_ASSUME_NONNULL_END
#import "JDViewController.h"
@interface JDViewController ()
@property (nonatomic,weak) UIButton *button;
@end
@implementation JDViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self createButton];
}
- (void)createButton
{
self.view.backgroundColor = [UIColor redColor];
UIButton *btn = [[UIButton alloc] init];
[btn setBackgroundColor:[UIColor greenColor]];
btn.frame = CGRectMake(80.f, 80.f, 80.f, 80.f);
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[btn setTitle:@"POP" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(popBack) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
self.button = btn;
}
- (void)popBack
{
self.testBlock(@"传个值回去");
[self.navigationController popViewControllerAnimated:YES];
}
@end
举例结果 :
2. Block作为函数入参
在很多的第三方库中,可以看到
Block
作为参数,比如AFN
的response
代码都会写在它的block
参数中,这是响应式编程的一种思想。
建立一个JDPerson
类,继承于NSObject
。
举例 :
JDPerson :
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface JDPerson : NSObject
@property (nonatomic,copy) NSString *subjuect;
@property (nonatomic,copy) NSString *score;
- (void)study:(NSString *(^)(NSString *))work;
@end
NS_ASSUME_NONNULL_END
#import "JDPerson.h"
@implementation JDPerson
- (void)study:(NSString *(^)(NSString *))work
{
self.score = work(self.subjuect);
}
@end
Controller :
- (void)jd_block_param
{
JDPerson *person = [[JDPerson alloc] init];
person.subjuect = @"数学";
[person study:^NSString * _Nonnull(NSString * param) {
return [NSString stringWithFormat:@"%@ = 100",param];
}];
NSLog(@"%@",person.score);
}
举例结果 :
3. Block作为返回值
最经典又常见的应该是绘制
UI
时候用到的Masonry
框架,作为OC链式编程
的经典框架,内部就是将Block
作为返回值,达到可以一直用.
语法进行设值。
其实现原理简述 :
- 利用对象的
Getter
方法,获取对象方法。- 对象方法的返回值类型是一个
Block
类型。- 该
Block
有参数、有返回值,又因为本身就是代码块,可以执行内部的内容。Block
的返回值则是当前对象。
举例 :
JDPerson :
@interface JDPerson : NSObject
- (JDPerson *(^)(id))eat;
@end
@implementation JDPerson
- (JDPerson *(^)(id))eat
{
return ^JDPerson *(id param){
NSLog(@"param is %@",param);
return self;
};
}
@end
Controller :
#pragma mark - Block作为函数返回值
- (void)jd_block_return_value
{
JDPerson *person = [[JDPerson alloc] init];
person.eat(@"食物").subjuect = @"物理";
NSLog(@"subject is %@",person.subjuect);
}
举例结果 :
四、Block的循环引用问题
1. 循环引用的产生
Block的循环引用代码 :
依然利用三、Block的常见用法 --> 1.Block作为属性
中的举例代码,两个控制器ViewController
和JDViewController
来进行举例。
ViewController :
//随便加一个按钮,ViewController的rootController是UINavigationController
- (IBAction)btnClick:(id)sender {
JDViewController *vc = [[JDViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}
JDViewController :
#import "JDViewController.h"
typedef void(^JDVCBlock)(void);
@interface JDViewController ()
@property (nonatomic,copy) JDVCBlock jd_vc_block;
@property (nonatomic,copy) NSString *name;
@end
@implementation JDViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self jd_block_retain_cycle];
}
#pragma mark - Block循环引用
- (void)jd_block_retain_cycle
{
self.name = @"JD";
self.jd_vc_block = ^{
NSLog(@"%@",self.name);
};
self.jd_vc_block();
}
- (void)dealloc
{
NSLog(@"dealloc来了");
}
看到xcode
的提示,造成了循环引用。
循环引用的图示 :
假设存在对象A
和对象B
。
正常的引用应该是 :
-
对象A
和对象B
在ARC环境
下,生成的时候,默认都是strong
强引用。 - 当
对象A
持有对象B
的时候,对象A
会给对象B
发送一个retain
的信号,对象B
的引用计数
进行+1
的操作。 - 当
对象A
要使用dealloc
进行析构的时候,则会向它持有的对象B
发送一个release
信号。对象B
接收到对象A
发送来的release
信号,就会判断自身的引用计数是否为0。- 如果为0,
对象B
调用自身的dealloc
进行析构。 - 如果不为0,
对象B
无法进行析构。
- 如果为0,
循环引用则是 :
-
对象A
和对象B
互相持有,又都是默认的strong
强引用。 -
对象A
想要使用dealloc
进行析构,就需要对象B
向它发送release
信号,将对象A
自己的引用计数
置为0。 -
对象B
想要使用dealloc
进行析构,也需要对象A
向它发送release
信号,将对象B
自己的引用计数
置为0。 - 而发送
release
信号的方式则是通过dealloc
,偏偏对象A
和对象B
因为互相引用,所以引用计数都不为0,也就无法调用自身的dealloc
,无法向对方发送release
信号,于是就造成了循环引用,二者都无法进行析构。
2. 解决Block的循环引用
2.1 __weak
修改上面1中的Block的循环引用代码
。
- (void)jd_block_retain_cycle
{
self.name = @"JD";
__weak typeof(self)weakSelf = self;
self.jd_vc_block = ^{
NSLog(@"%@",weakSelf.name);
};
self.jd_vc_block();
}
其中,__weak typeof(self)weakSelf = self;
这句代码,表达了 :
weakSelf
持有了self
并且因为是__weak
,所以他们会被存入弱引用表中。
于是,循环引用从最开始的 :
self
——>jd_vc_block
——>self
就变成了 :
weakSelf
——>self
——>jd_vc_block
——>weakSelf
但是因为是__weak
,所以weakSelf
和self
只是存入弱引用表,没有使self
的引用计数
增加。
所以,当self
的pop
的时候,self
的引用计数
就会变成0,会调用自身的dealloc
方法进行析构变成nil
:
引用链就变成了 :
weakSelf
——>self
——>nil
——>jd_vc_block
——>weakSelf
weakSelf
持有的就是nil
而不是jd_vc_block
,而jd_vc_block
因为self
的dealloc
从而接受到了release
信号,于是引用计数-1
,使得jd_vc_block
的引用计数
变为0,jd_vc_block
也就可以正常的析构。
这是一种典型的
中介者模式
的设计。
2.2 __strong
问题 :先看下面一段代码 :
- (void)jd_block_retain_cycle
{
self.name = @"JD";
__weak typeof(self)weakSelf = self;
self.jd_vc_block = ^{
//2秒后
dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
//主线程
dispatch_queue_t main_queue = dispatch_get_main_queue();
//2秒后,主线程再执行,但是,如果在2秒前,self就pop了
dispatch_after(timer, main_queue, ^{
NSLog(@"%@",weakSelf.name);
});
};
self.jd_vc_block();
}
循环引用是解决了,但是如果Block
内的self
要在2秒后才被用到,但是2秒前,self
就进行了析构,那么结果就会变成如下图 :
原因 :
- 因为没有对象对
self
进行强引用的持有,所以self
进行pop
之后,发现自己引用计数
已经为0,可以进行析构,于是就调用了自身的dealloc
方法。dealloc
动作就会向self
所持有的所有对象全都发送一个release
信号,所以,self.name
也就release
了,自然就变成了图中的null
。
解决 :
利用局部的__strong
修饰weakSelf
。
- (void)jd_block_retain_cycle
{
self.name = @"JD";
__weak typeof(self)weakSelf = self;
self.jd_vc_block = ^{
//2秒后
dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
//主线程
dispatch_queue_t main_queue = dispatch_get_main_queue();
//在Block内部定义一个__strong修饰的strongSelf对weakSelf进行强引用持有
__strong typeof(weakSelf) strongSelf = weakSelf;
//2秒后,主线程再执行,但是,如果在2秒前,self就pop了
dispatch_after(timer, main_queue, ^{
NSLog(@"%@",strongSelf.name);
});
};
self.jd_vc_block();
}
在jd_vc_block
的内部,让weakSelf
被strongSelf
强引用持有。于是引用链就变成了 :
strongSelf
——>weakSelf
——>self
——>jd_vc_block
——>weakSelf
于是,weakSelf——>self
这对弱引用表中的一对就被strongSelf
进行了强引用持有,引用计数+1
,在self
进行pop
的时候,self
的引用计数-1
,但是不为0,所以无法调用dealloc
进行析构,需要等待strongSelf
析构后,发送release
信号,才会让self
的引用计数
为0,才可以析构。
而strongSelf
是属于jd_vc_block
内部的局部变量,作用域只有jd_vc_block
内部,完成任务就会被最近的自动释放池回收释放,也就会进行析构,这时,才会再向weakSelf——>self
发送release
信号,self
的引用计数-1
,置为0,才会析构,调用dealloc
向持有的jd_vc_block
发送release
信号,完成所有对象的析构释放。
2.3 手动释放
既然循环引用的造成因素是两个对象互相持有,都无法析构,无法向对方发送release
信号,那么除了利用__weak
让引用关系发生改变,让其中一个对象可以析构外,也可以手动的将其中一个对象置为nil
,从而解决循环引用。
- (void)jd_block_retain_cycle
{
self.name = @"JD";
__block JDViewController *jd_vc = self;
self.jd_vc_block = ^{
dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_after(timer, main_queue, ^{
NSLog(@"%@",jd_vc.name);
jd_vc = nil;
});
};
self.jd_vc_block();
}
引用链为 :
jd_vc
——>self
——>jd_vc_block
——>jd_vc
虽然也是循环引用,但是在jd_vc_block
内部,手动的将jd_vc
置为了nil
。于是self
收到release
信号,调用dealloc
,继续发送release
给jd_vc_block
,也调用自己的dealloc
。
这里注意,最后的
self.jd_vc_block();
一定要调用,否则jd_vc
没有被执行,也就不为nil
,也依然无法破坏掉循环引用。
2.4 引用对象作为Block的参数
当把引用对象作为Block
的参数传入的时候,Block
会将参数copy一份,放入自己的结构当中,自然也就不存在Block
对引用对象进行持有,也就不存在循环引用。
- (void)jd_block_retain_cycle
{
self.name = @"JD";
self.jd_vc_block = ^(JDViewController *vc) {
dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_after(timer, main_queue, ^{
NSLog(@"%@",vc.name);
});
};
self.jd_vc_block(self);
}
五、总结
本文主要是为后面的Block
深入的探索学习做一个基础的铺垫,后面将要开启Block
的深入探索。