本文为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的深入探索。