iOS Objective-C Block简介
1. 基本概念
block
:带有自动变量(局部变量)的匿名函数(Anonymous function
),也被称为闭包(closure
),但是本文并不会提及Swift
中的闭包。Block
是Objective-C
对于闭包的实现。Block
不仅可以被用作属性还以用作参数和返回值,其实Block
就是一个代码块,可以作为变量使用。
-
Block
的本质是个对象,可以是代码高度聚合 -
Block
可以嵌套定义,定义Block
方法和定义函数方法相似 -
Block
可以定义在方法内部或外部 - 只有调用
Block
的时候,才会执行其{}体内的代码
1.1 Block的定义
在Objective-C
中Block
主要由标识符^
、返回值类型、参数列表和代码块组成,可以没有返回值和参数。
^
返回值类型(参数列表){代码块}
按照返回值和参数,Block
的使用组合有如下四种:
-
- 无返回值 无参数
void(^myBlock)(void) = ^void(void) {
NSLog(@"无返回值 无参数");
};
// 可以简写成:
void(^myBlock)(void) = ^ {
NSLog(@"无返回值 无参数");
};
-
- 有返回值 无参数
int(^myBlock)(void) = ^int(void) {
NSLog(@"有返回值 无参数");
return 10;
};
// 可以简写成:
int(^myBlock)(void) = ^int {
NSLog(@"有返回值 无参数");
return 10;
};
-
- 无返回值 有参数
void(^myBlock)(int a) = ^void(int num) {
NSLog(@"无返回值 有参数---%d",num);
};
// 可以简写成:
void(^myBlock)(int a) = ^(int num) {
NSLog(@"无返回值 有参数---%d",num);
};
-
- 有返回值 有参数
int(^myBlock)(int a, int b) = ^int(int a, int b) {
NSLog(@"无返回值 有参数---%d----%d",a,b);
return a + b;
};
当返回值和参数为void
时我们都可以省略不写,在开发中我们可以使用typedef
定义block
,在属性中使用copy
修饰block
:
typedef void(^MyBlock)(int a, int b);
@property(nonatomic, copy) MyBlock myBlock;
self.myBlock = ^(int a, int b) {
NSLog(@"a + b = %d",a + b);
};
self.myBlock(10, 20);
1.2 Block与外界变量的关联
1.2.1 Block 使用外界变量
首先来看一个例子
@property(nonatomic, copy) void(^myBlock)(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
int c = 1;
self.myBlock = ^(int a, int b) {
NSLog(@"a + b + c = %d",a + b + c);
};
c = 2;
self.myBlock(10, 20);
}
在上面的代码中我们在Block
中使用变量c
去做计算,但是在执行Block
代码前,我们修改了变量c
的值,那么Block
内部会打印什么呢?我们运行后得到如下结果:a + b + c = 31
,由此可知Block
在获取外界变量的时候是拷贝了一份,此时无论外界在怎么修改,在Block
内部的变量都是不变的。
1.2.1 Block 使用外界变量
如果我们想在Block
内部修改外界变量编译器就会报错:
Variable is not assignable (missing __block type specifier)译文:(变量不可以被分配使用,因为缺少
__block
修饰符)
根据提示,我们给变量c
加上__block
修饰符,编译器就不会有错误提示了。示例代码如下:
@property(nonatomic, copy) void(^myBlock)(int a, int b);
- (void)viewDidLoad {
[super viewDidLoad];
__block int c = 1;
self.myBlock = ^(int a, int b) {
c = 3;
NSLog(@"a + b + c = %d",a + b + c);
};
self.myBlock(10, 20);
}
此时打印结果为:a + b + c = 33
此时无论我们是在外面还是在Block
内部修改变量c
的值都会使内外的值保持一致。此处就不在上更多的示例代码了。
Block
对于用__block
修饰的的外部变量的引用,实际是复制其引用地址来实现访问的,所以Block
也就可以修改__block
修饰的外部变量的值了。
1.3 Block循环引用
当我们使用Block
的时候最常见的问题就是循环引用了,因为Block
经常作为属性被self
持有,当我们在Block
内部使用self
的时候就会造成循环引用,如果在代码中造成了循环引用,编译器会报如下的警告:
Capturing 'self' strongly in this block is likely to lead to a retain cycle
译文:Block
中强引用了self
可能会造成循环引用。
那么该如何解决循环引用呢?下面我们来列举几种解决循环引用的方法:
1.3.1 __weak
weakSelf
是我们常用的解决Block
循环引用的方法,因其简单方便,深受广大开发者喜欢。示例代码:
@property (nonatomic, copy) void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;
- (void)test1 {
__weak typeof(self) weakSelf = self;
self.name = @"test1";
self.myBlock = ^{
NSLog(@"%@",weakSelf.name);
};
self.myBlock();
}
此处的原理是weakSelf
弱引用了self
,self
持有Block
,Block
内部持有weakSelf
,因为weakSelf
对self
的持有是弱引用,只是一个指针指向,并不会增加引用计数,此时就会打破循环引用。
weakSelf
-->self
——>Block
——>weakSelf
1.3.2 __weak + __strong
这也是我们常用的一种解决循环引用的方式,那么就会有人想问,不是弱引用就好了吗?为什么会用到strong
,这就是weakSelf
的坑点了,因为我们使用的weakSelf
弱引用,那么就要注意对象的释放时机了,weakSelf
对对象是弱引用,如果引用计数为0就会释放对象,但是我们还想让weakSelf
持有的对象做一些事情,比如说打印,那么就会造成打印为空的现象,所以这时候使用strongSelf
就可以完美的解决这个问题了,示例代码:
@property (nonatomic, copy) void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;
- (void)test2 {
__weak typeof(self) weakSelf = self;
self.name = @"test2";
self.myBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.myBlock();
}
在以上代码中,如果我们在一个UIViewController
中调用该方法,仅使用weakSelf
,在延迟时间没到前就pop
回去,就会造成打印为空的情况,因为VC
被释放,所以name
也就没有值了。在这里我们使用strongSelf
即使在延迟时间没到前pop
回去,也会保证name
的正确打印,并在打印后正常销毁控制器。
此处的使用strongSelf
对weakSelf
做了强引用,但是这个强引用是在Block
内部的,作用域只是Block
内部,当Block
执行完毕自然也就会释放了。
weakSelf
-->self
——>Block
-->(局部变量)strongSelf
——>weakSelf
1.3.3 不直接使用self
既然使用self
会造成循环引用,那么我们就不用self
@property (nonatomic, copy) void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;
- (void)test3 {
__block ViewController *vc = self;
self.name = @"test3";
self.myBlock = ^{
NSLog(@"%@",vc.name);
vc = nil;
};
self.myBlock();
}
- (void)test4 {
__block ViewController *vc = self;
self.name = @"test4";
self.myBlock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
self.myBlock();
}
这里我们通过创建一个ViewController
的对象,指向self
,使用__block
进行修饰,在Block
内部使用完将其置空,就不会引用着self
了,也就解决的循环引用的问题。
vc
——>self
——>Block
vc = nil 时此引用已经断开了。
1.3.4 将self作为参数
此处跟1.3.3
中的有异曲同工之妙,既然不能用self
那我们也可以传入self
,此处需定义一个有参数的Block
。示例代码如下:
@property (nonatomic, copy) void(^mmyBlock)(ViewController *vc);
@property (nonatomic, copy) NSString *name;
- (void)test5 {
self.name = @"test5";
self.mmyBlock = ^(ViewController *vc) {
NSLog(@"%@",vc.name);
};
self.mmyBlock(self);
}
- (void)test6 {
self.name = @"test6";
self.mmyBlock = ^(ViewController *vc) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.mmyBlock(self);
}
此时我们将self
作为参数传入Block
,参数在使用完毕后也就销毁了,所以并不会造成循环引用。
1.4 Block的种类
有时候面试官会问你,iOS中有几种block
,这个时候如果你回答说Block
还有几种?那只能回家等消息了,如果你说三种,那说明你对Block
有些研究,如果你能回答6
种,那么面试官会继续跟你好好的聊聊。
其实我们常用的Block
就是三种,另外三种都是系统级别的Block
,一般很少用。这6种Block
可以在Apple Opensource中的libclosure源码中的data.c
文件中看到。这里推荐一下LGCooci的libclosure-74-KCBuild,可以编译运行的libclosure
,可以运行并断点调试Block
底层的libclosure-74
源码。
void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };
以上就是我们说的6种Block
,其中我们常用的Block
有_NSConcreteStackBlock
、_NSConcreteMallocBlock
、_NSConcreteGlobalBlock
三种,另外_NSConcreteAutoBlock
、_NSConcreteFinalizingBlock
、_NSConcreteWeakBlockVariable
三种是系统级别的Block
在我们的日常开发中几乎用不到。
1.4.1 _NSConcreteGlobalBlock (NSGlobalBlock)
_NSConcreteGlobalBlock
即全局block
,不访问外界变量(包括堆区和栈区)
测试代码:
- (void)testGlobalBlock{
void (^block)(void) = ^{
NSLog(@"block");
};
block();
NSLog(@"%@",block);
}
打印结果:
1.4.2 _NSConcreteMallocBlock (NSMallocBlock)
_NSConcreteMallocBlock
是堆Block
,存在于堆内存中,是带一个引用计数的对象,需要自己进行内存管理。变量本身在栈中,因为Block
能够自动截获变量,为了访问到变量,会将变量从栈内存中copy
到堆内存中。
测试代码:
- (void)testMallocBlock{
int a = 10;
void (^block)(void) = ^{
NSLog(@"block, a的值是:%d", a);
};
block();
NSLog(@"%@",block);
}
打印结果:
1.4.3 _NSConcreteStackBlock (NSStackBlock)
_NSConcreteStackBlock
即栈bolck
,存储在栈中,目前看来只是一个中间状态了,现在很少有栈Block
了,在最新的Xcode12.2
中,如果不使用__weak
修饰Block
是打印不出__NSStackBlock__
的。
测试代码:
- (void)testStackBlock{
int a = 10;
void (^block)(void) = ^{
NSLog(@"block, a的值是:%d", a);
};
NSLog(@"%@",^{
NSLog(@"block, a的值是:%d", a);
});
// block();
// NSLog(@"%@",block);
}
打印结果:
使用Xcode11.6
打印:
使用Xcode12.2
打印:
同样的代码不同的打印结果,可见由于堆Block
的广泛使用,苹果对栈Block
应该是在逐步弱化中。
如果你确定要使用栈Block
就需要使用__weak
进行修饰了,代码如下:
- (void)testStackBlock2{
int a = 10;
void (^ __weak block)(void) = ^{
NSLog(@"block, a的值是:%d", a);
};
block();
NSLog(@"%@",block);
}
打印结果:
当使用__weak
修饰Block
后,编译器会有个警告⚠️:
Assigning block literal to a weak variable; object will be released after assignment
译文:将块文字赋值给弱变量;对象将在赋值后被释放
此时的警告也在告诉我们,如果这样用会导致Block
释放,当然在我们这个例子中不会因释放而导致其他问题,所以如果你特别确认要使用栈Block
在使用__weak
去修饰,如果不是很确定最好就不要这样做了。
1.4.4 小结
根据上面对常用三种Block
的分析我们得出如下结论:
-
Block
默认存储在全局区 - 如果
Block
需要访问外界变量,则需要对Block
进行拷贝操作- 首先将
Block
拷贝到栈区,然后在拷贝到堆区 - 在
Xcode12.2
以前(未验证,不严谨),如果并没有在Block
中使用外界变量前,直接打印Block
还是在栈区,在Xcode12.2
中Block
会直接在堆区 - 要想使用栈区
Block
需要使用__weak
修饰Block
- 可以简单理解为弱引用
Block
存储在栈区,强引用就要存储在堆区
- 首先将