本文主要介绍:
1、block的本质
2、block捕获变量
3、block的类型
4、__block原理
本质
通过clang分析Block底层
step1:
定义block.c
文件
#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("lbh");
};
return 0;
}
step2:
通过xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c
,将block.c
编译成 block.cpp
,其中block在底层被编译成了以下的形式
int 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;
}
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("lbh");
}
//******简化******
//__main_block_impl_0 是构造函数,在结构体中
//参数__main_block_func_0 是闭包中具体实现
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));//构造函数
block->FuncPtr(block);//block调用执行
相当于block等于__main_block_impl_0
,是一个函数,第一个参数是__main_block_func_0
,它是个函数,代码块的实现函数。
step3:
查看__main_block_impl_0
,是一个结构体
//**block代码块的结构体类型**
struct __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;
}
};
//**block的结构体类型**
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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)};
struct __main_block_impl_0
包含一个struct __block_impl
类型的impl
和一个struct __main_block_desc_0*
类型的Desc
。
struct __block_impl
包含一个isa
指针,说明block
其实是一个对象。
static struct __main_block_desc_0
中包含一个Block_size
表示block占用内存空间
构造函数__main_block_impl_0
将第一个参数__main_block_func_0
传给了FuncPtr
,所以FuncPtr
指向block具体实现函数的地址。
block通过clang编译后的源码间的关系如下所示,以__block
修饰的变量为例
总结:block
本质上也是一个oc对象,他内部也有一个isa指针
。block
是封装了函数调用以及函数调用环境的OC对象。
捕获基本数据类型
捕获局部变量--auto
定义一个变量,并在block中调用
int main(){
int a = 10;
void(^block)(void) = ^{
printf("lbh--%d", a);
};
a = 20;
block();
return 0;
}
输出结果
lbh--10
底层编译成如下
struct __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) {
// : a(_a) c++语法 会自动将_a赋值给a a = _a
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
//代码块放在函数中
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy 值拷贝
printf("lbh--%d", a);
}
int main(){
int a = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
在构造函数创建block
时,传入的第三个参数是a
,然后将a
存入到结构体__main_block_impl_0
里的成员变量a
中,调用block
时,直接从结构体中取出变量a
的值 所以局部自动变量是值拷贝
, 如果此时在block内部实现中作 a++操作,是有问题的,会造成编译器的代码歧义,即此时的a是只读的
捕获局部变量--static
int main(int argc, char * argv[]) {
static int a = 10;
void(^block)(void) = ^{
printf("lbh--%d", a);
};
a = 20;
block();
return 0;
}
输出结果
lbh--20
底层编译如下
struct __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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *a = __cself->a; // bound by copy
printf("lbh--%d", (*a));
}
int main(){
static int a = 10;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
在通过构造函数创建block
时,第三个参数传的是&a
,即变量a的地址
,然后将&a
存入结构体__main_block_impl_0
中的成员变量*a
中,调用block
时,从结构体中取出a
,因为存入的是a的地址
所以是指针拷贝。
全局变量
static int a= 10;
int main(int argc, char * argv[]) {
void(^block)(void) = ^{
printf("lbh--%d", a);
};
a = 20;
block();
return 0;
}
输出结果
lbh--20
底层编译
struct __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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("lbh--%d", a);
}
int main(){
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
在通过构造函数创建block
时,并没有以参数的形式将a
传进去,在结构体__main_block_impl_0
中也没有生成新的成员变量,在调用时直接访问变量a
。
总结:
局部变量都会被block捕获,自动变量是值捕获,静态变量为地址捕获。全局变量则不会被block捕获
问题:
为什么全局变量不需要捕获,而局部变量需要捕获?解答: 因为全局变量都可以访问,捕获是多此一举,局部变量因为作用域的问题需要捕获,请看下面的例子
void (^block) (void);
void test()
{
auto int a = 10;
static int b = 20;
block = ^{
NSLog(@" a = %d b = %d", a,b);
};
}
//static int a= 10;
int main(int argc, char * argv[]) {
test();
block();
return 0;
}
a
、b
的作用域是在test
函数中,而block
代码块封装在另一个函数中,block调用时调用的是这个封装的函数,超出了a
、b
的作用域,如果不进行变量捕获,会出现访问异常。
问题
:下面代码中self
会不会被捕获? 为什么?
- (void)test
{
void(^block)(void) = ^{
NSLog(@"----%p",self);
};
block();
}
看底层编译
struct __LBHPerson__test_block_impl_0 {
struct __block_impl impl;
struct __LBHPerson__test_block_desc_0* Desc;
LBHPerson *self;
__LBHPerson__test_block_impl_0(void *fp, struct __LBHPerson__test_block_desc_0 *desc, LBHPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void _I_LBHPerson_test(LBHPerson * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__LBHPerson__test_block_impl_0((void *)__LBHPerson__test_block_func_0, &__LBHPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
__LBHPerson__test_block_impl_0
结构体中增加了一个成员变量LBHPerson *self;
,所以self
是会被捕获的,因为oc函数默认是会有两个参数(LBHPerson * self, SEL _cmd)
,而参数是局部变量
,局部变量是会被捕获的。
问题:
如果在block中使用成员变量或者调用实例的属性会有什么不同的结果
@interface LBHPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LBHPerson
- (void)test
{
void(^block)(void) = ^{
NSLog(@"%@",self.name);
NSLog(@"%@",_name);
};
block();
}
@end
通过xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc LBHPerson.m
编译成底层代码
struct __LBHPerson__test_block_impl_0 {
struct __block_impl impl;
struct __LBHPerson__test_block_desc_0* Desc;
LBHPerson *self;
__LBHPerson__test_block_impl_0(void *fp, struct __LBHPerson__test_block_desc_0 *desc, LBHPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __LBHPerson__test_block_func_0(struct __LBHPerson__test_block_impl_0 *__cself) {
LBHPerson *self = __cself->self; // bound by copy
//通过调用self的getter方法获取属性值
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f9___44hp612k7gxwp82fznnkpm0000gn_T_LBHPerson_03f602_mi_0,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
//通过self从其ivars中找到成员变量
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f9___44hp612k7gxwp82fznnkpm0000gn_T_LBHPerson_03f602_mi_1,(*(NSString * _Nonnull *)((char *)self + OBJC_IVAR_$_LBHPerson$_name)));
}
在__LBHPerson__test_block_impl_0
结构体中只捕获了LBHPerson *self;
,在由代码块中封装的函数中,是通过self
间接获取的。
block 类型
int main(int argc, const char * argv[]) {
@autoreleasepool {
// __NSGlobalBlock__ : NSBlock : NSObject
void (^block)(void) = ^{
NSLog(@"Hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
}
return 0;
}
注意
:在不同版本的xcode上可能会有差异,但最终都继承于NSObjcet
从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象
。
block的3种类型
通过代码查看一下block在什么情况下其类型会各不相同
int c = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 1. 内部没有调用外部变量的block
void (^block1)(void) = ^{
NSLog(@"Hello");
};
// 2. 内部调用auto变量的block
int a = 10;
void (^block2)(void) = ^{
NSLog(@"Hello - %d",a);
};
// 3. 内部调用局部static变量的block
//static int b = 10;
//void (^block3)(void) = ^{
// NSLog(@"Hello - %d",b);
//};
// 4. 内部调用全局变量的block
//void (^block4)(void) = ^{
// NSLog(@"Hello - %d",c);
//};
// 5. 直接调用的block的class
NSLog(@"%@ %@ %@ %@ %@", [block1 class], [block2 class],[^{
NSLog(@"%d",a);
} class]);
}
return 0;
}
输出结果
__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__
block可以分为三种各类型
__NSGlobalBlock__ ( _NSConcreteGlobalBlock ) //全局block
__NSStackBlock__ ( _NSConcreteStackBlock ) //栈区block
__NSMallocBlock__ ( _NSConcreteMallocBlock ) //堆区block
看下通过clang
编译结果
struct __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;
}
};
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
int a;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __main_block_impl_2 {
struct __block_impl impl;
struct __main_block_desc_2* Desc;
int a;
__main_block_impl_2(void *fp, struct __main_block_desc_2 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在底层C++代码中,block
的impl.isa = &_NSConcreteStackBlock;
,三个block的isa指针全部都指向_NSConcreteStackBlock类型地址。
问题:
为什么底层编译结果和打印结果不一致runtime运行时过程中也许对类型进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。
block在内存中的存储
通过下面一张图看一下不同block的存放区域
数据段中的NSGlobalBlock直到程序结束才会被回收,不过我们很少使用到NSGlobalBlock类型的block,因为这样使用block并没有什么意义。
NSStackBlock类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放,而在相同的作用域中定义block并且调用block似乎也多此一举。
NSMallocBlock是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。
block是如何定义其类型
block是如何定义其类型,依据什么来为block定义不同的类型并分配在不同的空间呢?首先看下面一张图
1、没有访问
auto
变量是全局block
2、访问了auto
变量是栈区block
3、栈区block
调用copy
变成堆区block
注意
:这是对MRC环境得出的结论,ARC打印结果会有差别,因为ARC内部会做很多事情
先关闭ARC环境,看下这个结论是否正确
// 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);
};
// Global:访问局部static变量:__NSGlobalBlock__
static int b = 10;
void (^block3)(void) = ^{
NSLog(@"block2---------%d", b);
};
NSLog(@"%@ %@ %@ %@", [block1 class], [block2 class],[block3 class],[[block2 copy] class]);
}
return 0;
}
输出结果
-
没有访问auto变量
的block是__NSGlobalBlock__
类型的,存放在数据段中。 -
访问了auto变量
的block是__NSStackBlock__
类型的,存放在栈中。 -
__NSStackBlock__
类型的block调用copy
成为__NSMallocBlock__
类型并被复制存放在堆中。
NSGlobalBlock类型的我们很少使用到,因为如果不需要访问外界的变量,直接通过函数实现就可以了,不需要使用block。
但是__NSStackBlock__访问了auto变量
,并且是存放在栈中
的,上面提到过,栈中的代码在作用域结束之后内存就会被销毁
,那么我们很有可能block内存销毁之后才去调用他,那样就会发生问题,通过下面代码可以证实这个问题。
//MRC
void (^block)(void);
void test()
{
// __NSStackBlock__
int a = 10;
block = ^{
NSLog(@"block---------%d", a);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
输出结果
问题:
可以发现a的值变为了不可控的一个数字。为什么会发生这种情况呢?因为上述代码中创建的
block是__NSStackBlock__类型
的,因此block是存储在栈中的,那么当test函数执行完毕之后,栈内存中block所占用的内存已经被系统回收
,因此就有可能出现乱得数据。查看其c++代码可以更清楚的理解。
为了避免这种情况发生,可以通过copy将NSStackBlock类型的block转化为NSMallocBlock类型的block,将block存储在堆中,以下是修改后的代码
//MRC
void (^block)(void);
void test()
{
// __NSStackBlock__ 调用copy 转化为__NSMallocBlock__
int age = 10;
block = [^{
NSLog(@"block---------%d", age);
} copy];
[block release];
}
看下此时的输出结果
那么其他类型的block调用copy会改变block类型吗?
所以在平时开发过程中MRC环境下经常需要使用copy来保存block,将栈上的block拷贝到堆中,即使栈上的block被销毁,堆上的block也不会被销毁
,需要我们自己调用release操作来销毁。而在ARC环境下系统会自动调用copy操作,使block不会被销毁。
ARC帮我们做了什么
在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy
操作,将block复制到堆上。
将上面的例子放在ARC环境中,看下是否会被copy到堆区
//ARC
void (^block)(void);
void test()
{
// __NSStackBlock__
int a = 10;
block = ^{
NSLog(@"block---------%d", a);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
NSLog(@"== %@",[block class]);
}
return 0;
}
输出结果
在ARC下block被copy到堆区
block对对象变量的捕获
block一般使用过程中都是对对象变量的捕获,那么对象变量的捕获同基本数据类型变量相同吗?
//ARC
@interface LBHPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LBHPerson
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
LBHPerson *person = [[LBHPerson alloc] init];
person.name = @"liu";
block = ^{
NSLog(@"------block内部%@",person.name);
};
} // 执行完毕,person没有被释放
NSLog(@"--------");
} // person 释放
return 0;
}
运行程序
person
是小括号(116-123行)中的一个auto变量
,按理说它的生命周期超出这个括号时就已经结束,但是在124行的断点处,person的dealloc
方法并没有执行,继续运行
运行到断点126行,发现person调用了dealloc
方法,此时person才被销毁
,由于person
是auto变量
,block代码块中有使用了person,所以block会捕获person
,即block对person有一个强引用
,所以block不被销毁的话,peroson也不会销毁。
查看源代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
LBHPerson *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LBHPerson *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
将上述代码转移到MRC环境下,在MRC环境下即使block还在,person却被释放掉了。因为MRC环境下block在栈空间,栈空间对外面的person不会进行强引用。
//MRC环境下代码
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
LBHPerson *person = [[LBHPerson alloc] init];
person.name = @"liu";
block = ^{
NSLog(@"------block内部%@",person.name);
};
[person release];
} // person被释放
NSLog(@"--------");
}
return 0;
}
运行结果
在126行的断点处person调用了dealloc
,被销毁了。
block调用copy操作之后,person不会被释放。
block = [^{
NSLog(@"------block内部%d",person.age);
} copy];
前面提到过,只需要对栈空间的block进行一次copy
操作,将栈空间的block拷贝到堆中
,person就不会被释放
,说明堆空间的block可能会对person进行一次retain操作,以保证person不会被销毁。堆空间的block自己销毁之后也会对持有的对象进行release操作。
栈空间上的block不会对对象强引用,堆空间的block有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操作
__weak
__weak添加之后,person在作用域执行完毕之后就被销毁了
typedef void (^Block)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Block block;
{
LBHPerson *person = [[LBHPerson alloc] init];
person.name = @"liu";
__weak LBHPerson *waekPerson = person;
block = ^{
NSLog(@"------block内部%@",waekPerson.name);
};
}
NSLog(@"--------");
}
return 0;
}
通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
编译成底层源码
注意
: __weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
LBHPerson *__weak waekPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, LBHPerson *__weak _waekPerson, int flags=0) : waekPerson(_waekPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在__main_block_impl_0
中也是用__weak
去修饰的
__main_block_copy_0 和 __main_block_dispose_0
当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0
的描述结构体__main_block_desc_0
中多了两个参数copy
和dispose
函数,查看源码
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->waekPerson, (void*)src->waekPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->waekPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}
copy
和dispose
函数中传入的都是__main_block_impl_0
结构体本身
copy本质就是__main_block_copy_0函数
,__main_block_copy_0
函数内部调用_Block_object_assign
函数
dispose本质就是__main_block_dispose_0函数
,__main_block_dispose_0
函数内部调用_Block_object_dispose
函数
_Block_object_assign函数调用时机及作用
当block进行copy操作
的时候就会自动调用__main_block_desc_0
内部的__main_block_copy_0
函数,__main_block_copy_0
函数内部会调用_Block_object_assign
函数。
_Block_object_assign
函数会自动根据__main_block_impl_0结构体内部的person是什么类型的指针
,对person对象产生强引用或者弱引用
。可以理解为_Block_object_assign函数内部会对person进行引用计数器的操作,如果__main_block_impl_0结构体内person指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内person指针是__weak类型,则为弱引用,引用计数不变。
_Block_object_dispose函数调用时机及作用
当block从堆中移除
时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数
,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。
_Block_object_dispose会对person对象做释放操作
,类似于release,也就是断开对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。
后续继续补充
Block循环引用
-
正常释放
:是指A持有B的引用,当A调用dealloc方法,给B发送release信号,B收到release信号,如果此时B的retainCount(即引用计数)为0时,则调用B的dealloc方法 -
循环引用
:A、B相互持有,所以导致A无法调用dealloc方法给B发送release信号,而B也无法接收到release信号。所以A、B此时都无法释放
如下图所示:
相关例子
例1
//代码一
NSString *name = @"LBH";
self.block = ^(void){
NSLog(@"%@",self.name);
};
self.block();
//代码二
UIView animateWithDuration:1 animations:^{
NSLog(@"%@",self.name);
};
问题:
上述两段代码是否出现循环引用?
解答:
代码一种发生了循环引用
,因为在block内部使用了外部变量name
,导致block持有了self
,而self原本是持有block的
,所以导致了self和block的相互持有
。
代码二中无循环引用,虽然也使用了外部变量,但是self并没有持有animation的block
,仅仅只有animation持有self,不构成相互持有
例2
新建一个页面B,它是从页面A push过来的
//类扩展
typedef void(^LBHBlock)(void);
@interface ViewController ()
@property (nonatomic, copy) LBHBlock lbhblock;
@property (nonatomic, copy) NSString *name;
@end
//实现
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 循环引用
self.name = @"lbh";
[self test1];
}
- (void)test1
{
// __weak typeof(self) weakSelf = self;
self.lbhblock = ^{
NSLog(@"%@",self.name);
};
}
- (void)dealloc{
NSLog(@"dealloc 来了");
}
@end
从当前的页面B 返回到页面A ,页面B的dealloc
并没有执行,由于循环引用导致页面无法释放
解决循环引用
解决循环引用常见的方式有以下几种:
1、weak-strong-dance
2、__block
修饰对象(需要注意的是在block内部需要置空
对象,而且block必须调用
)
3、传递对象self作为block的参数
,提供给block内部使用
4、使用NSProxy
weak-strong-dance
- 如果block内部并
未嵌套block
,直接使用__weak修饰self
即可
- (void)test1
{
__weak typeof(self) weakSelf = self;
self.lbhblock = ^{
NSLog(@"%@",weakSelf.name);
};
}
此时的weakSelf 和 self 指向同一片内存空间
,且使用__weak不会导致self的引用计数发生变化
,可以通过打印weakSelf和self的指针地址,以及self的引用计数来验证
- (void)test2
{
NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));
self.lbhblock = ^{
NSLog(@"%@",weakSelf.name);
};
NSLog(@"%p %p",weakSelf, self);
NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self.lbhblock)));
NSLog(@"%ld",(long)CFGetRetainCount((__bridge CFTypeRef)(self)));
}
- 如果block内部
嵌套block
,需要同时使用__weak 和 __strong
- (void)test3
{
__weak typeof(self) weakSelf = self;
self.lbhblock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%s %@",__func__,strongSelf.name);
});
};
self.lbhblock();
}
运行结果
【注意】
:实际上嵌套的block内部使用weakSelf
并不一定会出现问题,不过为了程序的严谨通常还是会使用strongSelf
__block修饰变量
这种方式同样依赖于中介者模式
,属于手动释放
,是通过__block修饰对象,主要是因为__block修饰的对象是可以改变的
- (void)test4
{
__block ViewController *vc = self;
self.lbhblock = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%s %@",__func__,vc.name);
vc = nil;
});
};
self.lbhblock();
}
运行结果
【注意】
这里的block必须调用
,如果不调用block,vc就不会置空,那么依旧是循环引用,self和block都不会被释放
对象self作为参数
主要是将对象self作为参数,提供给block内部使用,不会有引用计数问题
//声明一个新的block
typedef void(^LBH2Block)(ViewController *);
@property (nonatomic, copy) LBH2Block lbh2block;
- (void)test5
{
self.lbh2block = ^(ViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%s %@",__func__,vc.name);
});
};
self.lbh2block(self);
}
运行结果
NSProxy 虚拟类
-
OC是只能
单继承
的语言,但是它是基于运行时的机制
,所以可以通过NSProxy
来实现伪多继承
,填补了多继承的空白 -
NSProxy
和NSObject
是同级的一个类,也可以说是一个虚拟类
,只是实现了NSObject的协议 -
NSProxy
其实是一个消息重定向封装的一个抽象类,类似一个代理人,中间件,可以通过继承它,并重写下面两个方法来实现消息转发到另一个实例
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
使用场景
NSProxy的使用场景主要有两种:
- 实现多继承功能
- 解决了
NSTimer&CADisplayLink
创建时对self强引用问题,参考YYKit
的YYWeakProxy
。
循环引用解决原理
主要是通过自定义的NSProxy
类的对象来代替self
,并使用方法实现消息转发
下面是NSProxy子类的实现以及使用的场景
step1
自定义一个NSProxy
的子类LBHProxy
@interface LBHProxy : NSProxy
- (id)transformObjc:(NSObject *)objc;
+ (instancetype)proxyWithObjc:(id)objc;
@end
@interface LBHProxy ()
@property(nonatomic, weak, readonly) NSObject *objc;
@end
@implementation LBHProxy
- (id)transformObjc:(NSObject *)objc{
_objc = objc;
return self;
}
+ (instancetype)proxyWithObjc:(id)objc{
return [[self alloc] transformObjc:objc];
}
//2.有了方法签名之后就会调用方法实现
- (void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = [invocation selector];
if ([self.objc respondsToSelector:sel]) {
[invocation invokeWithTarget:self.objc];
}
}
//1、查询该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
NSMethodSignature *signature;
if (self.objc) {
signature = [self.objc methodSignatureForSelector:sel];
}else{
signature = [super methodSignatureForSelector:sel];
}
return signature;
}
- (BOOL)respondsToSelector:(SEL)aSelector{
return [self.objc respondsToSelector:aSelector];
}
@end
step2:
自定义Cat类和Dog类
//********Cat类********
@interface Cat : NSObject
@end
@implementation Cat
- (void)eat{
NSLog(@"猫吃鱼");
}
@end
//********Dog类********
@interface Dog : NSObject
@end
@implementation Dog
- (void)shut{
NSLog(@"狗叫");
}
@end
step3:
通过LBHProxy实现多继承功能
- (void)lbh_proxyTest{
Dog *dog = [[Dog alloc] init];
Cat *cat = [[Cat alloc] init];
LBHProxy *proxy = [LBHProxy alloc];
[proxy transformObjc:cat];
[proxy performSelector:@selector(eat)];
[proxy transformObjc:dog];
[proxy performSelector:@selector(shut)];
}
通过LBHProxy解决定时器中self的强引用问题
self.timer = [NSTimer timerWithTimeInterval:1 target:[LBHProxy proxyWithObjc:self] selector:@selector(print) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
总结
循环应用的解决方式从根本上来说就两种,以self -> block -> self
为例
-
打破
self 对 block
的强引用,可以block属性修饰符使用weak,但是这样会导致block还没创建完就释放了
,所以从这里打破强引用行不通 -
打破
block对self
的强引用,主要就是self的作用域和block作用域的通讯,通讯有代理、传值、通知、传参等几种方式,用于解决循环,常见的解决方式如下:-
weak-strong-dance
-
__block
(block内对象置空,且调用block) -
将对象
self
作为block的参数 -
通过
NSProxy
的子类代替self
-
问题:
block为什么需要调用?
解答:
block在底层是类型为__main_block_impl_0
结构体,通过其同名构造函数创建,第一个传入的block的内部实现代码块,即__main_block_func_0,用fp表示,然后赋值给impl的FuncPtr属性,然后在main中进行了调用,这也是block为什么需要调用的原因。如果不调用,block内部实现的代码块将无法执行,可以总结为以下两点
- 函数声明:即block内部实现声明成了一个函数
__main_block_func_0
- 执行具体的函数实现:通过调用block的FuncPtr指针,调用block执行
4.3 __block的原理
对a加一个__block
,然后在block中对a进行++操作
int main(){
__block int a = 11;
void(^block)(void) = ^{
a++;
printf("%d", a);
};
block();
return 0;
}
底层编译成如下
struct __Block_byref_a_0 {//__block修饰的外界变量的结构体
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {//block的结构体类型
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {//构造方法
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {//block内部实现
__Block_byref_a_0 *a = __cself->a; // bound by ref 指针拷贝,此时的对象a 与 __cself对象的a 指向同一片地址空间
//等同于 外界的 a++
(a->__forwarding->a)++;
printf("%d", (a->__forwarding->a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
int main(){
//__Block_byref_a_0 是结构体,a 等于 结构体的赋值,即将外界变量a 封装成对象
//&a 是外界变量a的地址
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 11};
//__main_block_impl_0中的第三个参数&a,是封装的对象a的地址
void(*block)(void) = __main_block_impl_0(__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;
}
-
main中的a是通过外界变量封装的对象
-
__main_block_impl_0
中,将对象a的地址&a
给构造函数 -
__main_block_func_0
内部对a的处理是指针拷贝
,此时创建的对象a与传入对象的a指向同一片内存空间
总结:
-
外界变量会生成
__Block_byref_a_0
结构体,结构体用来保存原始变量的指针和值 -
将变量生成的结构体对象的
指针地址传递给block
,然后在block内部就可以对外界变量进行操作了
两种拷贝对比如下
-
值拷贝 - 深拷贝,只是拷贝数值,且拷贝的值不可更改,指向不同的内存空间,案例中
普通变量a就是值拷贝
-
指针拷贝 - 浅拷贝,生成的对象指向同一片内存空间,案例中经过
__block修饰的变量a就是指针拷贝
网友评论