问题
1.什么是block,block的本质是什么?
2.block的属性修饰词为什么是copy?使用block有哪些使用注意?
3.block为什么会发生循环引用?
4.block的变量捕获究竟是怎样进行的?
5.block在修改NSMutableArray,需不需要添加__block?
6.__block和__weak的作用是什么?有什么使用注意点?
7.block中访问的对象和变量什么时候会销毁?
让我们带着这一系列问题,开始探寻block的本质
1.什么是block,block的本质是什么
- block本质上也是一个对象,而这个对象是一个结构体,其内部含有一个isa指针指向自己的类。
- block是封装了函数调用以及函数调用环境的OC对象。
- block是封装函数及其上下文的OC对象。
首先写一个简单的block
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
void(^block)(int ,int) = ^(int a, int b){
NSLog(@"this is block,a = %d,b = %d",a,b);
NSLog(@"this is block,age = %d",age);
};
block(3,5);
}
return 0;
}
使用命令行将代码转化为c++查看其内部结构
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;//描述block的信息
int age;
// 构造函数(类似于OC的init方法),返回结构体对象
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; //block内代码地址
Desc = desc; // 储存block对象占用的大小
}
};
// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_0,a,b);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_1,age);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 10;
// 定义block变量以及实现
void(*block)(int , int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 执行block内部的代码
((void (*)(__bloc k_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
return 0;
}
}
从定义和实现block变量开始
// 定义block变量
void(*block)(int , int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
抛开无关紧要的代码,block除了传入两个int的参数还调用了__main_block_impl_0
这个函数,并将函数的地址赋值给了block,于是找到方法的定义
__main_block_imp_0结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; //block内代码地址
Desc = desc; // 储存block描述
}
};
观察结构体内,有2个结构体变量和1个基本变量,同时还有一个构造函数,而我们上面block实现传入的函数不正是这个函数吗?
而这个函数最终返回的不就是一个名为__main_block_impl_0
的结构体呀
那么也就是说最终将一个__main_block_imp_0
结构体的地址赋值给了block变量
所以我们需要 看一看这个__main_block_imp_0
结构体里到底有什么猫腻?仔细一看,哇、美滋滋,只有4个参数 void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0
。
再看看block实现的时候调用__main_block_imp_0
函数传入了什么(void *)__main_block_func_0, &__main_block_desc_0_DATA, age
。居然只有三个参数,很好nice,说明flags默认传的0基本与我们没有和什么关系,继续看第一个参数__main_block_func_0
这就是我们的fp
呀看看__main_block_func_0
中有些什么。
- flags,标志变量,在实现block的内部操作时会用到
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_0,a,b);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_1,age);
}
在__main_block_func_0
函数中首先取出block中age
的值,紧接着可以看到两个熟悉的NSLog,可以发现这两段代码恰恰是我们在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
的构造函数中保存在结构体内。
也就是说我们的fp
就是__main_block_func_0
函数的地址。
而这个时候我们再回到__main_block_imp_0
结构体的构造函数。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; //block内代码地址
Desc = desc; // 储存block描述
}
};
这个魂淡居然把我们的block实现地址传给了impl
的FunctionPtr
,找到impl
的定义,居然又是一个结构体__block_impl
,好的,忍它一次,点进去。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
简单,又是2个清爽的成员变量和2个清爽的指针变量,到了这里我只知道flags是系统传的我不管,然后我block的实现地址传给了FuncPtr
,还有两个参数是什么东东呢?
此时此刻我们看着isa这个指针不就是指向所属类的指针,也就是block的类型吗
但是__main_block_impl_0
构造函数传的是&_NSConcreteStackBlock
这个东东,这个东东又是什么👻?哇好蒙蔽,这都不知道是哪里传过来的,不要怕点进去看
__OBJC_RW_DLLIMPORT void *_NSConcreteStackBlock[32];
德玛西亚,这个block其实就是当前类所属的指针的地址,只是苹果对它做了堆栈的处理。
而Reserved
这个只是保留变量而已
总结一下:
- isa,指向所属类的指针,也就是block的类型
- flags,标志变量,在实现block的内部操作时会用到
- Reserved,保留变量
- FuncPtr,block执行时调用的函数指针
可以看出,它包含了isa指针(包含isa指针的皆为对象),也就是说block也是一个对象(runtime里面,对象和类都是用结构体表示)。
此时此刻 __main_block_func_0
函数的地址我们已经看吃透了,看__main_block_imp_0
的第二个参数&__main_block_desc_0_DATA
点进去看是一个__main_block_desc_0
结构体
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
它也传入了2个参数,reserved系统传了0,而Block_size
传入的不正是当前block的size大小吗,又是苹果最喜欢的内存处理,这也是ios的魅力所在呀。
到了这里block的定义和实现已经走完了,可是还没有调用呀?
block的调用
// 执行block内部的代码
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 3, 5);
通过上述代码可以发现调用block是通过block找到__block_impl
的FunPtr
直接调用
通过上面分析我们知道block指向的是__main_block_impl_0
类型结构体,但是我们发现__main_block_impl_0
结构体中并不直接就可以找到FunPtr
,而FunPtr是存储在__block_impl
中的,为什么block可以直接调用__block_impl
中的FunPtr
呢?
重新查看上述源代码可以发现,(__block_impl *)block
将block强制转化为__block_impl
类型的,因为__block_impl
是__main_block_impl_0
结构体的第一个成员,相当于将__block_impl
结构体的成员直接拿出来放在__main_block_impl_0
中,那么也就说明__block_impl
的内存地址就是__main_block_impl_0
结构体的内存地址开头。所以可以转化成功。并找到FunPtr成员。
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_0,a,b);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kz_2rvx1kjx5p75z7v2crt0nw8h0000gn_T_main_6253a2_mi_1,age);
}
上面我们知道,FunPtr
中存储着通过代码块封装的函数地址,那么调用此函数,也就是会执行代码块中的代码。并且回头查看__main_block_func_0
函数,可以发现第一个参数就是__main_block_impl_0
类型的指针。也就是说将block传入__main_block_func_0
函数中,便于重中取出block捕获的值。
总结:
-
block结构体内部之间的关系
-
block底层的数据结构
此时此刻你以为结束了吗?
nonono
look
block的类型
block分为三种类型
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
纳尼,这不是我们刚刚__main_block_impl_0
中的impl
的isa
指向的&_NSConcreteStackBlock
的类型吗?
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp; //block内代码地址
Desc = desc; // 储存block描述
}
};
不管那么多写段代码打印一下
我们通过代码用class方法或者isa指针查看具体类型。
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int a = 10;
static int b = 11;
void(^block)(void) = ^{
NSLog(@"hello, a = %d, b = %d", a,b);
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
}
return 0;
}
从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象
通过代码查看一下block在什么情况下其类型会各不相同
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 内部没有调用外部变量的block
void (^block1)(void) = ^{
NSLog(@"Hello");
};
// 2. 内部调用外部变量的block
int a = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d",a);
};
// 3. 直接调用的block的class
NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
NSLog(@"%d",a);
} class]);
}
return 0;
}
上述代码转化为c++代码查看源码时却发现block的类型与打印出来的类型不一样,c++源码中三个block的isa指针全部都指向_NSConcreteStackBlock
类型地址。
我们可以猜测runtime运行时过程中也许对类型进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。
这里就涉及到了block在内存中的存储位置
这五个区存储的内容也各有划分:
- 栈区(stack):这一块区域系统会自己进行管理,我们不用干预,主要存一些局部变量,以及函数跳转时的现场保护。因此大量的局部变量、深递归、函数循环调用都可能耗尽内存而造成运行崩溃。
- 堆区(heap):与栈区相对,这一块一般由我们开发人员管理,比如一些alloc、free的操作,存储一些自己创建的对象。
- 全局区(静态区 static):全局变量和静态变量都存储在这里,已经初始化的和没有初始化的变量会分开存储在相邻的区域,程序结束后系统来释放。
- 常量区:存储常量字符串和const常量。
- 代码区:顾名思义,就是存我们写的代码。
-
_NSConcreteGlobalBlock
(全局) -
_NSConcreteStackBlock
(栈)
-_NSConcreteMallocBlock
(堆)
block是如何定义其类型
block是如何定义其类型,依据什么来为block定义不同的类型并分配在不同的空间呢?
接着我们使用代码验证上述问题,首先关闭ARC回到MRC环境下,因为ARC会帮助我们做很多事情,可能会影响我们的观察。
点击【Project】 - 点击【Target 】-点击 【build Setting】- 输入【“AutoMatic”】 -点击【Object-C Automatic Reference Count】改为【NO】
// MRC环境!!!
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Global:没有访问auto变量:__NSGlobalBlock__
void (^block1)(void) = ^{
NSLog(@"block1---------");
};
// Stack:访问了auto变量: __NSStackBlock__
int a = 10;
void (^block2)(void) = ^{
NSLog(@"block2---------%d", a);
};
NSLog(@"%@ %@", [block1 class], [block2 class]);
// __NSStackBlock__调用copy : __NSMallocBlock__
NSLog(@"%@", [[block2 copy] class]);
}
return 0;
}
通过打印的内容我们可以知道:
- 没有访问auto变量的block是
__NSGlobalBlock__
类型的,存放在数据段中。 - 访问了auto变量的block是
__NSStackBlock__
类型的,存放在栈中。 -
__NSStackBlock__
类型的block调用copy成为__NSMallocBlock__
类型并被复制存放在堆中。
再看看ARC中的打印
我了个去,发现访问了auto变量的block是
__NSMallocBlock__
类型了,可以猜想苹果在ARC环境下是不是帮我们做了一次copy操作呢?
__NSStackBlock__
访问了aotu变量,并且是存放在栈中的,上面提到过,栈中的代码在作用域结束之后内存就会被销毁,那么我们很有可能block内存销毁之后才去调用他,那样就会获取错误。所以ARC下直接帮我们自动进行了一次copy操作。
[__NSStackBlock __ copy]
操作就变成了__NSMallocBlock __
此时此刻,我们已经知道ARC进行了copy操作那它究竟针对不同类型的block做了什么操作呢?请看下图:
所以在平时开发过程中MRC环境下经常需要使用copy来保存block,将栈上的block拷贝到堆中,即使栈上的block被销毁,堆上的block也不会被销毁,需要我们自己调用release操作来销毁。而在ARC环境下回系统会自动copy,是block不会被销毁。
当然,你以为结束了吗?仔细看:
ARC帮我们做了什么
在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上。
什么情况下ARC会自动将block进行一次copy操作?
以下代码都在RAC环境下执行。
1. 将block赋值给__strong指针时
// ARC环境!!!
int main(int argc, const char * argv[]) {
@autoreleasepool {
// block内没有访问auto变量
Block block = ^{
NSLog(@"block---------");
};
NSLog(@"%@",[block class]);
int a = 10;
// block内访问了auto变量,但没有赋值给__strong指针
NSLog(@"%@",[^{
NSLog(@"block1---------%d", a);
} class]);
// block赋值给__strong指针
Block block2 = ^{
NSLog(@"block2---------%d", a);
};
NSLog(@"%@",[block2 class]);
}
return 0;
}
我们发现打印的第二个block内访问了auto变量,但没有赋值给__strong指针直接打印了
__NSStackBlock__
所以当block被赋值给__strong指针时,RAC才会自动进行一次copy操作。
此时此刻,我们定义block属性的时候是不是就可以用copy和strong呢?
block本身是像对象一样可以retain,和release。但是,block在创建的时候,它的内存是分配在栈上的,而不是在堆上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。因为栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区
.
使用retain也可以,但是block的retain行为默认是用copy的行为实现的,
因为block变量默认是声明为栈变量的,为了能够在block的声明域外使用,所以要把block拷贝(copy)到堆,所以说为了block属性声明和实际的操作一致,最好声明为copy。
2. 将block作为函数返回值时
// ARC环境!!!
typedef void (^Block)(void);
Block myblock()
{
int a = 10;
// block中访问了auto变量,此时block类型应为__NSStackBlock__
Block block = ^{
NSLog(@"---------%d", a);
};
return block;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block = myblock();
block();
// 打印block类型为 __NSMallocBlock__
NSLog(@"%@",[block class]);
}
return 0;
}
如果在block中访问了auto变量时,block的类型为
__NSStackBlock__
,上面打印内容发现blcok为__NSMallocBlock__
类型的,并且可以正常打印出a的值,说明block内存并没有被销毁。
block进行copy操作会转化为__NSMallocBlock__
类型,来讲block复制到堆中,那么说明RAC在 block作为函数返回值时会自动帮助我们对block进行copy操作,以保存block,并在适当的地方进行release操作。
3.block作为Cocoa API中方法名含有usingBlock的方法参数时
NSArray * arr = @[@1,@2,@3,@4];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//一系列装逼操作
}];
4.block作为GCD API的方法参数时
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//一系列装逼操作
});
注意
- 1,对于栈区block,在ARC情况下自动拷贝到堆区,MRC则放在栈区,所在函数执行完毕就会释放。那么要想在外面调用就要用copy指向它,这样
就copy到了堆区。 - 2,strong属性不会拷贝,会造成野指针错区从而crash。另外这里需要注意的是,ARC是编译器的一种特性,编译器在编译的时候会在合适的地方插入retain、release、autorelease,而不是iOS运行时的特性。
- 3,堆区是动态的,不像代码区是不变化的,堆区会不断地创建销毁,当没有强指针指向的时候就会被销毁,如果再去访问这段代码,程序就会崩溃。所以在堆区要用strong或者copy修饰。
- 4,block是一段代码块,即不可变,所以使用copy也不会深copy。
所以要用strong替代copy修饰block要满足两个条件:
用strong修饰且引用了外部变量
使用block的时候要注意循环引用导致内存泄露
一个block要使用self,会处理成在外部声明一个weak变量指向self,在block里又声明一个strong变量指向weakSelf?????
原因:block会把写在block里的变量copy一份,如果直接在block里使用self,(self对变量默认是强引用)self对block持有,block对self持有,导致循环引用,所以这里需要声明一个弱引用weakSelf,让block引用weakSelf,打破循环引用。
而这样会导致另外一个问题,因为weakSelf是对self的弱引用,如果这个时候控制器pop或者其他的方式引用计数为0,就会释放,如果这个block是异步调用而且调用的时候self已经释放了,这个时候weakSelf已就变成了nil。
当控制器(也可以是其他的控件)pop回来之后(或者一些其他的原因导致释放),网络请求完成,如果这个时候需要控制器做出反映,需要strongSelf再对weakSelf强引用一下。
但是,你可能会疑问,strongSelf对weakSelf强引用,weakSelf对self弱引用,最终不也是对self进行了强引用,会导致循环引用吗。不会的,因为strongSelf是在block里面声明的一个指针,当block执行完毕后,strongSelf会释放,这个时候将不再强引用weakSelf,所以self会正确的释放。