前言
在iOS中最常用到block
和Delegate
,本篇文章本着再学习再改造的心态整理一下block
。如有不妥,望请指教🙏🙏🙏。
正文
简介
概念
block
是苹果公司从iOS 4
开始引入的对C语言的扩展,是用来实现匿名函数特性的。block
是一种特殊的数据类型,它可以正常定义变量、作为参数、作为返回值,特殊的block
还可以保存一段代码。
语法
语法表达式
^返回值类型(参数列表){表达式}
Block几种常见写法
无返回值无参数:
1 | void (^block)(void) = ^() { |
无返回值有单一参数
1 | void (^block)(int) = ^(int a) { |
无返回值有多参数
1 | void (^block)(int, int) = ^(int a, int b) { |
有返回值有单一参数
1 | int (^block)(int) = ^(int a) { |
有返回值有多参数
1 | int (^block)(int, int) = ^(int a, int b) { |
常见类型
block
常见类型有三种:
NSGlobalBlock
NSMallocBlock
NSStackBlock
NSGlobalBlock
是全局block
,不引用外部变量,相当于一个函数,存放在静态数据区;
NSMallocBlock
是栈Block
,block
在初始化的时候,是存放在栈内存中的,所以叫做NSMallocBlock
,当NSMallocBlock
的作用域一结束,该block
所占有的内存就会被系统释放;
NSStackBlock
是堆Block
,由于block
在初始化的时候是存放在栈上的,所以在block
的作用域结束的时候,其所占内存空间就会被系统释放,再从作用域外访问该block
时会造成程序崩溃,为避免这一情况的发生,需要堆NSMallocBlock
进行copy,得到堆Block
,并且产生引用计数。
循环引用
概念
循环引用就是两个对象间各有一个强引用指向对方,而iOS
进行内存管理时是采用引用计数的,当有强引用指向的时候,会使引用计数+1
,导致任何时候都不引用计数都不会为0,所占有的内存空间无法得到释放,从而导致内存消耗过高,性能变差甚至闪退等。
block
中出现循环引用
的示例代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23#import "ViewController.h"
typedef void (^KPBlock)(ViewController);
@interface ViewController ()
@property(nonatomic, strong) KPBlock block;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.block = ^{
NSLog(@"%@",self.title);
};
self.block();
}
@end
以上是一段存在循环引用的高危代码
题外话:Xcode
真的越来越强大了,当我打这段代码的时候,Xcode
尽然有警告,相较于几年前写Objective-C
的时候越来越强大了。Xcode
警告如下
Capturing ‘self’ strongly in this block is likely to lead to a retain cycle
抛开警告认识一下循环引用,block
被声明成ViewController
的属性,说明ViewController
对block
有一个强引用,同时在block
调用了ViewController
的name
属性,这样的话,说明block
也会对ViewController
有一个强引用。导致当前ViewController
和Block
所占的内存空间无法得以释放。
解决方案
说明:以下是针对上面示例的解决方案,修改代码是针对viewDidLoad
的修改,并进行以下简单分析。
解决方案一
1 | - (void)viewDidLoad { |
由上面代码可以看出先是使用了__weak
,然后又使用了__strong
来修饰self
。为什么这么做呢?使用__weak
修饰的对象,引用计数不会+1
,也就是说在self
对象释放的时候,__weak
会将引用的self
对象置为nil
,这样做的话ViewController
也就可以正常释放了,但是这么做会导致可能self
被释放了,而block
无法正常执行,于是在block
内部使用__strong
,使用__strong
进行强引用self
对象,这样的话,在block
执行的过程中,ViewController
不会被释放,而在block
执行完也就是作用域结束后,ViewController
的引用计数就会-1
。
此处额外分享一个宏定义
,简写__weak
和__strong
1 | #define weakify(...) \ |
解决方案二
1 | - (void)viewDidLoad { |
由以上代码可以看到,使用__block
来修饰vc,并且在block
内部将修饰的对象置为nil
,其实__block
的作用是提升外部变量的作用域, 将外部变量拷贝到堆内存中,同时外部变量也就产生了内存计数,所以需要在block
作用域完成时,将其置为nil
。
解决方案三
1 | #import "ViewController.h" |
由上面的代码不难看出,我新增了block
的参数,参数相当于零时持有的成员变量,之所以这么解决是利用了block
的作用域。
底层原理
底层研究过程
不引用外部变量的block
:1
2
3
4
5
6
7
8
9#include "stdio.h"
int main() {
void(^block)(void) = ^{
printf("\n Hello Word");
};
block();
return 0;
}
通过clang
命令将上述C
代码转化成C++
代码,命令如下:
clang -rewrite-objc 需转换文件名.c -o 目标文件名.cpp
得到以下C++
代码后,打开一看就好几百行代码,可能如我一样瞬间懵逼,但是作为新时代的四无青年,我们还是有一双能找到重要地方的慧眼的,我们把转化后的C++
文件滑冻到最下面,就找到了我们灰常熟悉的main
函数。如下所示:1
2
3
4
5int main() {
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
从上面的代码可以看出在转换得到的C++
代码里面,block
在定义的时候调用__main_block_impl_0
函数,并且将__main_block_impl_0
函数的地址赋值给了block
。继续查看__main_block_impl_0
,如下所示:1
2
3
4
5
6
7
8
9
10struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
有上述源码可以看出来__main_block_impl_0
依旧是个结构体,同时还包含了一个同名构造函数,构造函数中对一些变量进行赋值之后最终返回一个结构体。那么就是说最终将一个__main_block_impl_0
结构体的地址赋值给了block
变量。我们在__main_block_impl_0
结构体内的构造函数可以发现__main_block_impl_0
构造函数传入了2个参数(__main_block_func_0
和&__main_block_desc_0_DATA
),因为flags
是有默认值的,所以省略没传。接下来我们查看一下__main_block_func_0
和&__main_block_desc_0_DATA
这两个参数分别代表着什么?发现代码里面有关__main_block_func_0
有如下一段代码:
1 | static void __main_block_func_0(struct __main_block_impl_0 *__cself) { |
由上面代码,我们不难发现,__main_block_func_0
函数里面就是我们在OC
中定义的block
内部的那段代码。由此不难看出,__main_block_func_0
函数中其实粗处着我们在block
中写的代码。而__main_block_impl_0
函数中传入的是(void *)__main_block_func_0
,也就是将我们写在block
块中的代码封装成了__main_block_func_0
函数,并将__main_block_func_0
函数地址传入了__main_block_impl_0
的构造函数中保存在结构体中。
那么&__main_block_desc_0_DATA
又是什么?1
2
3
4static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
我们从上述代码可以看出来,结构体__main_block_desc_0
中包含了两个参数,分别是reserved
和Block_size
,并且reserved
被赋值为0,而Block_size
则是存储这__main_block_impl_0
占有的空间大小。最终将__main_block_desc_0
结构体的地址传入__main_block_func_0
中,赋值给了Desc
。
综上得出的结论:block
底层是个结构体或者说是对象。其中存储了对象地址,也可以说是类型地址。block
代码块中的代码被封装成函数,存放在结构体中,还存储着结构体所占内存大小。
引用外部变量的block
:1
2
3
4
5
6
7
8
9
10#include "stdio.h"
int main() {
int a = 100;
void(^block)(void) = ^{
printf("\n a = %d", a);
};
block();
return 0;
}
依旧是使用clang
命令,将上述的c
语言代码转换成c++
,还是走之前的路子,发现main
函数1
2
3
4
5
6int main() {
int a = 100;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
与之前block
不引用外部变量的时候得到的main
函数对比,多了一些东西,除了main声明的a
变量以外,还发现__main_block_impl_0
多了一个参数a
。然后我们重新查看__main_block_impl_0
,返现这个结构体里面相较于之前不引用外部变量时,多出了一个 int a
。1
2
3
4
5
6
7
8
9
10
11struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这时候,我们可以得出一个结论:当block
引用外部变量时,可以自动捕获外部变量。
我们继续查看我们block
传入的参数,还是那个一模一样的名字__main_block_func_0
,这个东东的代码:1
2
3
4
5static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("\n a = %d", a);
}
于是我们发现int a = __cself->a;
,这个时候我们不难发现源码里面的int a = 100
和上述代码里面的int a = __cself->a
中的a
不是同一个a
。
这个时候,我们又可以得到一个结论,当如上述所说的引用外部变量时,不能直接修改外部变量的值。
综上得出的结论:block
引用外部变量时,可以自动捕获外部变量;block
自动捕获的外部变量实际上已经保存在block
中了,也就是在__main_block_impl_0
结构体中,所以block
不可以直接修改所引用的外部变量的值。
修改外部变量的block
:
上面简单研究了以下block
引用外部变量,那么block
怎么才能修改引用的外部变量咧?作为多年的码畜,可能第一个想法就是__block
修改需要修改的外部变量值,多年来都这么干的,但是为毛这样就行了,在学习block
的时候还是要研究以下的。废话多了,继续干。1
2
3
4
5
6
7
8
9
10#include "stdio.h"
int main() {
__block int a = 10;
void(^block)(void) = ^{
printf("\n a++ = %d", a++);
};
block();
return 0;
}
还是那一套用clang
命令将上述代码转成c++
代码,还是那一套路找最熟悉的main
函数。1
2
3
4
5
6int main() {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
通过上面代码跟之前的只是单纯引用外部变量的代码对比,很明显多出了一行:1
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
通过上面的代码中的&a
可以看出,将外部变量的指针空间传入进来了,这个空间就存在堆中,block
在内部修改外部引用变量时是通过内部持有的外部引用变量的指针进行修改的,由于外部变量的指针传入到block
内部,内部引用的外部变量的指针和外部引用变量的指针是一致的,所以在block
内部修改外部引用变量的值时,所引用的外部引用变量也发生了变化。
综上得到的结论:block
可以通过__block
来修改引用的外部变量值。
总结
应用场景
- 响应事件,如:按钮点击事件的实现等。
- 传递数据,如:个人信息页面A,跳转到编辑个人资料页面B,当编辑页面编辑完成后,pop到个人信息页面,可以使用
block
进行个人资料信息传值等。 - 链式语法,如: 对
AutoLayout
进行分装的第三方框架Masonry等。
面试相关
block
为什么需要使用copy
来修饰?
block
通过存储方式可以分为栈区block
,堆区block
,全局block
。block
在MRC
环境下常用copy
来修饰,在MRC
中,block
的内存地址显示在栈区,栈区的特点是创建的对象随时可能会被系统销毁,一旦被销毁后再次调用,可能会造成程序Crash,对block
进行copy
后,block会被存放在堆区,存放在堆上的block
,也就有了引用计数,后续的复制操作都不会真的执行复制,而是增加block
对象的引用计数,这样block
就不会被系统销毁,而是需要开发者释放了(在MRC
中引用计数需要手动管理,在ARC
中引用计数可以通过系统管理)。在ARC
中Block
都会在堆区,系统会默认对block
进行copy
操作。
block
为什么常使用__block
来访问外部变量?
外部变量可能存在栈
内存中,而block
存在堆
中,block
要想访问外部变量,需要把他们放在同一内存空间中,使用__block
来将外部变量拷贝到堆区
中,通过__block
修饰的外部引用变量的指针会被block
持有,当需要在block
中修改外部变量时,需要使用__block
修饰外部变量。
block
和delegate
的区别?
block
和delegate
都属于回调,block
面向结果,delegate
是面向过程的.
block
的优点:
1.代码紧凑,只需要实现声明属性和实现就行了;
2.它是一个轻量级的回调,可以直接访问上下文,由于block
的代码是内联的,所以运行效率更高,它也可以是一个对象,实现匿名函数的功能。
block
的缺点:
1.block
容易造成循环引用,而且不易察觉;
2.block
运行成本高,在出栈的时候需要将使用的数据从栈拷贝到堆,使用完之后需要置为nil
。
delegate
的优点:
1.代码可读性更强,可以避免不必要的循环引用问题;
2.delegate
只保存一个对象指针,直接回调,没有额外开销;
delegate
的缺点:
1.实现上较复杂一些,需要声明协议、声明代理属性、遵循协议、实现协议里的方法等;
2.如果多个对象设置的代理是同一个对象,就需要在delegate方法中判断当前执行代理的是哪个对象;
个人觉得在选择block
和delegate
上大同小异,可以遵循优先使用block
;当回调的状态很多的时候,多于3个使用delegate
;如果回调的频繁的话,也是使用delegate
的原则,不过在实际开发过程中的话,适合选择回调就行,如果能避免一切不好的事情发生的话,条条大路都通罗马咯。
总结
- 使用
typedef
给常用的block创建别名,以便于区分和读写; - 在使用
block
时注意避免循环引用。