什么是block
Block是将函数及其上下文封装起来的对象。
源码分析
编译器是如何实现block的?
新建Objective-C文件命名为MyClass,在.m文件中实现如下代码:
#import "MyClass.h"
@implementation MyClass
- (void)block{
int b = 10;
int (^MyBlock)(int parames);
MyBlock = ^int(int a){
return a*b;
};
MyBlock(2);
}
@end
使用命令行,在MyClass.m文件目录下,运行命令
clang -rewrite-objc MyClass.m
clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的
在当前目录下可以看到,clang输出了一个MyClass.cpp的文件。
关键代码如下:
static void _I_MyClass_block(MyClass * self, SEL _cmd) {
int b = 10;
int (*MyBlock)(int parames);
MyBlock = ((int (*)(int))&__MyClass__block_block_impl_0((void *)__MyClass__block_block_func_0, &__MyClass__block_block_desc_0_DATA, b));
((int (*)(__block_impl *, int))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock, 2);
}
struct __MyClass__block_block_impl_0 {
struct __block_impl impl;
struct __MyClass__block_block_desc_0* Desc;
int b;
__MyClass__block_block_impl_0(void *fp, struct __MyClass__block_block_desc_0 *desc, int _b, int flags=0) : b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; //函数指针
};
static int __MyClass__block_block_func_0(struct __MyClass__block_block_impl_0 *__cself, int a) {
int b = __cself->b; // bound by copy
return a*b;
}
__MyClass__block_block_impl_0就是该block的实现。
什么是block的调用
block调用即是函数的调用。
截获变量
capture过来的变量
int b = 10;
int (^MyBlock)(int parames);
MyBlock = ^int(int a){
return a*b;
};
b = 100;
NSLog(@"看一下结果%d",MyBlock(2));
输出结果 20
截获变量要看是对什么样的变量进行截获
有局部变量(基本数据类型、对象类型)、全局变量、静态局部变量、静态全局变量。
截获变量的特性是什么?
对于基本数据类型的局部变量截获其值。
对于对象类型的局部变量连同所有权修饰符一起截获。
以指针形式截获静态局部变量。
不截获全局变量和静态全局变量
源码分析
看下面代码
#import "MyClass.h"
@implementation MyClass
// 全局变量
int global_var = 4;
// 静态全局变量
static int static_global_var = 5;
- (void)myBlockMethod{
// 基本数据类型的局部变量
int b = 10;
// 静态局部变量
static int static_var = 3;
// 对象类型的局部变量 所有权修饰符__unsafe_unretained __strong
__unsafe_unretained id unsafe_obj = nil;
__strong id strong_obj = nil;
void(^MyBlock)(void);
MyBlock = ^{
NSLog(@"局部变量<基本数据类型> var=%d",b);
NSLog(@"局部变量<静态变量> static_var=%d",static_var);
NSLog(@"局部变量<__unsafe_unretained 修饰的对象类型> var=%@",unsafe_obj);
NSLog(@"局部变量 __strong 修饰的对象类型> var=%@",strong_obj);
NSLog(@"全局变量 %d",global_var);
NSLog(@"静态全局变量 %d",static_global_var);
};
MyBlock();
}
@end
运行命令 clang -rewrite-objc -fobjc-arc MyClass.m
-fobjc-arc 支持ARC -fno-objc-arc 不支持ARC
关键代码如下:
int global_var = 4;
static int static_global_var = 5;
struct __MyClass__myBlockMethod_block_impl_0 {
struct __block_impl impl;
struct __MyClass__myBlockMethod_block_desc_0* Desc;
int b;
int *static_var;
__unsafe_unretained id unsafe_obj;
__strong id strong_obj;
__MyClass__myBlockMethod_block_impl_0(void *fp, struct __MyClass__myBlockMethod_block_desc_0 *desc, int _b, int *_static_var, __unsafe_unretained id _unsafe_obj, __strong id _strong_obj, int flags=0) : b(_b), static_var(_static_var), unsafe_obj(_unsafe_obj), strong_obj(_strong_obj) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void _I_MyClass_myBlockMethod(MyClass * self, SEL _cmd) {
int b = 10;
static int static_var = 3;
__attribute__((objc_ownership(none))) id unsafe_obj = __null;
__attribute__((objc_ownership(strong))) id strong_obj = __null;
void(*MyBlock)(void);
MyBlock = ((void (*)())&__MyClass__myBlockMethod_block_impl_0((void *)__MyClass__myBlockMethod_block_func_0, &__MyClass__myBlockMethod_block_desc_0_DATA, b, &static_var, unsafe_obj, strong_obj, 570425344));
((void (*)(__block_impl *))((__block_impl *)MyBlock)->FuncPtr)((__block_impl *)MyBlock);
}
我们看下下面这段代码的运行结果是什么
int b = 2;
int (^MyBlock)(int param);
MyBlock = ^int(int a){
return a*b;
};
b = 4;
NSLog(@"结果: %d",MyBlock(7));
结果如下:结果: 14
为什么是14而不是28,因为对于基本数据类型的局部变量截获其值,也就是2.
然后我们在 局部变量前面加上static
static int b = 2;
int (^MyBlock)(int param);
MyBlock = ^int(int a){
return a*b;
};
b = 4;
NSLog(@"结果: %d",MyBlock(7));
结果如下:结果: 28
为什么是28呢,因为对静态局部变量是以指针形式截获的。int b = 4;时地址上值已经被修改成4了
如果我们想在block内部对b的值进行修改,我们就要用到__block.
__block修饰符
我们什么时候要用到__block修饰符呢?
一般情况下,对被截获变量进行赋值操作需添加__block修饰符。
需要注意的是 赋值 ≠ 使用
下面看一些关于__block的一些笔试题
NSMutableArray *arr = [[NSMutableArray alloc] init];
void (^MyBlock)(NSString *str);
MyBlock = ^(NSString *str){
[arr addObject:str];
};
MyBlock(@"客户甲");
NSLog(@"客户列表 %@",arr);
arr里的值是
description of arr:
<__NSArrayM 0x6000028de490>(
客户甲
)
这里不需要__block,因为这里只是使用arr,并没有赋值。
看下面这种情况
__block NSMutableArray *arr = nil;
void (^MyBlock)(NSString *str);
MyBlock = ^(NSString *str){
arr = [[NSMutableArray alloc] init];
[arr addObject:str];
};
MyBlock(@"客户甲");
这里才是对arr的赋值,所以这时候需要使用__block。
对变量进行赋值具体有什么特点?
对变量进行赋值时,局部变量,不管是基本类型的局部变量还是对象类型的局部变量,都需要使用__block修饰符。
静态类型的局部变量本身就是以指针类型截获的,所以不需要。
全局变量和静态全局变量不截获,所以也不需要。
思考一下下面的代码结果是什么
__block int b = 2;
int (^MyBlock)(int param);
MyBlock = ^int(int a){
return a*b;
};
b = 4;
NSLog(@"结果: %d",MyBlock(5));
结果是10还是20呢?结果: 20
好奇怪为什么不是10呢?
__block修饰符所起的作用来回答这个问题。
__block修饰的变量最后变成了对象。使用clang命令查看一下源码:
struct __Block_byref_b_0 {
void *__isa;
__Block_byref_b_0 *__forwarding;//指向同类型的指针 __forwarding
int __flags;
int __size;
int b;
};
struct __MyClass__myBlockMethod_block_impl_0 {
struct __block_impl impl;
struct __MyClass__myBlockMethod_block_desc_0* Desc;
__Block_byref_b_0 *b; // by ref
__MyClass__myBlockMethod_block_impl_0(void *fp, struct __MyClass__myBlockMethod_block_desc_0 *desc, __Block_byref_b_0 *_b, int flags=0) : b(_b->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
b变成了struct __Block_byref_b_0对象。而对b = 4;进行赋值实际上就是(b.__forwarding->b) = 4;
该段代码的运行是在栈上进行的,栈上有一个__block修饰的变量,且有一个.__forwarding的指针指向它。
Block的三种类型
在Objective-C中Block一共有三种类型:
-
_NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量;
-
_NSConcreteStackBlock 保存在栈中的block,当函数返回时被销毁;
-
_NSConcreteMallocBlock 保存在堆中的block,当引用计数器为0时被销毁
Block的内存管理
内存从高到低依次分为栈区、堆区、全局区、常量区、代码区。
需要知道的是全局的静态block保存在已初始化的全局静态区。
Block的copy操作
我们在何时需要对block进行copy操作?那么就要知道copy的效果是什么样的。
Block类别 | 源 | Copy结果 |
---|---|---|
_NSConcreteGlobalBlock | 数据区 | 什么也不做 |
_NSConcreteStackBlock | 栈 | 堆 |
_NSConcreteMallocBlock | 堆 | 增加引用计数 |
我们在站上有一个Block,在这个block当中,有一个__block修饰的变量,当我们对栈上的Block进行copy的时候,会在堆上生成一个和栈上一样对应的Block和__block变量,分占两块空间,左侧是在栈上,右侧是在堆上,随着变量作用域的结束,站上的Block被释放,但是堆上对应的Block和__block变量仍然存在。
请关注下面问题:
当我们对栈上的Block进行copy操作之后,假如说是在MRC环境下,是否会引起内存泄漏;答案是肯定。解析:假如说我们进行了copy操作之后,同时堆上面的这个Block没有其他成员变量指向它,那么和我们去alloc出来一个对象而没有去调用release操作是一样的。会产生内存泄漏。
究竟对栈上的__block进行copy操作,之后发生了什么呢?
我们在栈上有一个__block变量,__block下有一个__forwarding指针,栈上的__forwarding指针是指向它自身的;当对栈上的__block变量进行copy之后,实际在堆上面会产生一个__block变量,和栈上的是完全一致的,只不过分占两块内存空间。且栈上的__forwarding指针实际上指向的是堆上面的__block变量,堆上的__forwarding指针指向的是其自身。
(b.__forwarding->b) = 4;
所以,当我们对栈上__block修饰的变量b做出修改,假如说我们已经对b做了copy操作,那么我们修改的不是栈上面的__block变量b对应的值,而是通过栈上的__forwarding指针,找到它指向的堆上的__block变量b对应的值。
__forwarding指针是用来干什么的?
栈上的__forwarding是指向自身的,那么为什么需要这个指针呢?那么换句话说,我们没有这个指针的情况下,可以直接通过对成员变量的访问来对b进行赋值,那__forwading指针是不是多余了?从上面我们可以知道,当对栈上的__block变量进行copy之后,栈上的__forwading指针实际上指向堆上面的__block变量。并不是多余的。总结,不论在任何内存位置,我们都可以通过__forwading指针顺利的访问同一个__block变量。
Block的循环引用
看代码一:
_arr = [NSMutableArray arrayWithObject:@"张三"];
MyBlock = ^NSString*(NSString*str){
return [NSString stringWithFormat:@"你好 %@",_arr[0]];
};
MyBlock(@"5");
代码会报出警告⚠️
Capturing ‘self’ strongly in this block is likely to lead to a retain cycle
在这个block中,有一个对self的循环引用 。MyBlock和_arr都是当前对象self的成员变量,对_array一般用strong关键字来修饰,对block一般用copy关键字来修饰。当前对象通过copy属性关键字声明的MyBlock,所以当前对象对MyBlock有一个强引用在的。而这个Block的表达式中又使用到了当前对象的_arr成员变量,截获变量对于对象类型的局部变量是连同其所有权修饰符一起截获的。_arr是通过__strong修饰的,所以在block中就有了一个strongly类型的指针指向原来的对象
如何避免这个问题?修正代码:
_arr = [NSMutableArray arrayWithObject:@"张三"];
__weak NSArray *Weak_Array = _arr;
MyBlock = ^NSString*(NSString*str){
return [NSString stringWithFormat:@"你好 %@",Weak_Array[0]];
};
MyBlock(@"5");
采用避免产生循环引用的方式来解除它的循环引用。
那么为什么通过__weak修饰的成员变量就可以解除循环引用呢?因为block对其所截获的变量,如果是对象类型的,是连同其所有权修饰符一起截获。外部是__weak修饰符,所以里面也是__weak。
看代码二:
__block MyClass* blockSelf = self;
MyBlock = ^int(int num){
// b = 5
return num*blockSelf.b;
};
MyBlock(2);
在这个栈上面,有一个__block修饰的变量来指向self,即对象本身,同时当前对象的成员变量MyBlock创建时,表达式中有使用到self.b,这里用的是blockSelf,而不是直接使用当前对象。
这段代码在MRC下,不会产生循环引用。在ARC下,会产生循环引用,引起内存泄漏。
代码中存在一个大环引用:
self对象持有Block,Block中又使用了__block变量,而这个__block 变量又指向原有的这个对象。这种循环引用就是大环引用。
怎么解除这个循环引用呢?
我们一般使用断环的方式来解除这种循环引用。断开__block变量对原对象的持有,就可以规避循环引用。
修改后的代码:
__block MyClass* blockSelf = self;
MyBlock = ^int(int num){
// b = 5
int result = num * blockSelf.b;
blockSelf = nil;
return result;
};
MyBlock(2);
对blockSelf进行一个nil赋值,就可以规避这个循环引用。但是这个解决方案有一个弊端,假如我们很长一段时间,或者一直都没调用这个block的话,那么这个循环引用的环就会一直存在。
小结
什么是Block?
Block是对函数及其上下文封装起来的对象。
为什么Block会产生循环引用?
第一方面,自循环引用:如果说当前Block对当前对象的某一成员变量进行截获,那么这个Block会对对应变量有一个强引用,而当前对象对Block也有一个强引用,就产生了一个自循环引用方式的循环引用问题,我们通过声明一个__weak修饰的变量,来消除循环引用。
第二方面,我们定义一个__block修饰的变量,也会产生循环引用,但是是区别场景的。在ARC下会产生循环引用,有内存泄漏;在MRC下是没有问题的。我们可以在ARC下通过断环的方式避免循环引用,但是有一个弊端,如果block一直得不到调用,那么循环引用就无法解除,环就一直存在。
怎么理解Block截获变量的特性?
对于基本数据类型的局部变量,是对值进行截获。
对于对象类型的局部变量,是连同其所有权修饰符一起截获;
对于静态类型的局部变量,是以指针形式截获的;
对全局变量和静态全局变量不截获。
你都遇到过哪些循环引用?你有事怎样解决的?
我们会遇到Block所引起的循环引用,比如
1、Block所捕获的成员变量,也是当前对象的成员变量,而block也是当前对象的成员变量,就会造成自循环的循环引用。
解决:通过加__weak修饰符开解除这种自循环引用;
2、__block在ARC下也会产生循环引用,采用断环的方式来避免循环引用。