本文不介绍block基本概念,而主要介绍block捕获自动变量原理和__block原理以及循环引用问题
开始!
1.Block 捕获自动变量
自动变量,即是局部变量,C语言中变量一般可以分为一下5种:
自动变量
函数参数
静态变量
静态全局变量
全局变量
好的,我们直接来看一下这段代码:
int val = 10;
MyBlock block = ^ {
NSLog(@"%d", val);
};
val = 20;
block();
打印结果如下:
2017-04-04 15:13:36.206 Block[850:99999] 10
可以看到,block保存了之前的val变量值,执行 ^之后的代码对其进行了捕获
那么,block里可以修改自动变量吗
int val = 10;
MyBlock block = ^ {
NSLog(@"%d", val);
val = 20;
};
val = 20;
block();
答案是不可以的,这里编译器便会报错。
我们把最上面的代码用clang命令转化为C++代码,代码很长,我们直接看关键部分
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;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_nj_bxfgkgws0dbcjwm4psdxt2qm0000gn_T_main_63c9d1_mi_0, val);
}
好了,主要看关键两句
第一句,
第11行的 int val;
看起来很熟悉,就是我们定义的自动变量,block 在自己的结构体里面也声明了一个一样的变量来记录上文中的自动变量。
第二句
^ { NSLog(@"%d", val); };
转换之后的 NSLog 代码,我们在一个静态函数里面看到了:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_nj_bxfgkgws0dbcjwm4psdxt2qm0000gn_T_main_63c9d1_mi_0, val);
}
cself是什么,我们可以猜猜,这个是不是相当于 OC 中的 self 呢?实际上就是这样的,这个参数的作用于 OC 中的 self 关键字功能差不多,用来访问到block里出现的变量。
我们可以看到,将捕获的变量赋值给了一个int型的变量,所以,我们在block里改变的只是这个int型的变量,并不是原数据,而且,// bound by copy这一句也提醒了我们只是简单的赋值。这里编译器就不让我们改变,因此直接报错。
那我们再看看其他几种变量的情况,对于静态变量和静态全局变量传入的都是指针,因此可以改变,不会被捕获,全局变量分配在全局区,因此也可以直接访问到,函数参数和自动变量同理都会被捕获。
那么,我们真的没有办法在block去改变自动变量了吗
答案是错误的,通过__block来修饰的自动变量,便可以在block中修改(具体在下面)
2.__block原理
还是先看代码
__block int val = 10;
MyBlock block = ^ {
val = 20;
NSLog(@"val = %d", val);
};
block();
此时打印结果为
val=20
再次使用clang命令进行转化,可以看到如下代码:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_nj_bxfgkgws0dbcjwm4psdxt2qm0000gn_T_main_c4a31c_mi_0, (val->__forwarding->val));
}
好了,这时候我们看关键三句
第一句
struct __Block_byref_val_0
定义了一个结构体,里面东西还挺多的,可又有一个很熟悉的东西,就是
int val;
和我们外部的自动变量一摸一样的变量
第二句
在static void __main_block_func_0(struct __main_block_impl_0 *__cself)函里面有一句
__Block_byref_val_0 *val = __cself->val; // bound by ref
由第一句的结构体生成了一个指针变量指向了 cself 中的 val 变量,后面还有句注释,bound by ref,意思是受ref约束,即是受这个结构体指针约束,我们立刻感受到与没有加__block的不同,之前是
int val = __cself->val; // bound by copy
下来,还有重要的一句,我们执行了
val=20;
转化过来的代码是
(val->__forwarding->val) = 20;
看不懂了,好了,现在可以说说第一句struct __Block_byref_val_0这个结构体了,结构体里有5个成员变量。
第一个是isa指针,
第二个是指向自身类型的*__forwarding指针,
第三个是一个标记flag,
第四个是它的大小,
第五个是变量值,名字和变量名同名。
先说isa指针,一个实例对象的 isa 指针指向的是其类对应的类对象,这个类对象存储着对象所需要的实例对象和实例方法,类对象的 isa 指针则指向的是元类,元类中存储的是类方法,也就是说,所谓的类对象和元类,都存储着构造一个对象所需要的信息。
一般来说,__forwarding指针都指向本block
在ARC下,一般只要对block赋值,block便会从栈复制到堆,好了又得说说block的三种类型
_NSConcreteStackBlock:栈区block
只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了;不持有对象。
_NSConcreteMallocBlock:堆区block
有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制;持有对象。
_NSConcreteGlobalBlock:全局区block
没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束;不持有对象。
当__block才会被复制到堆上,__block也会被复制到堆上,并且该Block会持有该__block,对于栈里的__forwarding指针,把原来__forwarding指针指向自己,换成指向_NSConcreteMallocBlock上复制之后的__block自己。这样不管__block怎么复制到堆上,还是在栈上,都可以通过(val->__forwarding->val)来访问到变量值,由于是通过指针访问,因此则可以改变变量的值了。
3. block循环引用
看这段代码(ARC环境下)
self.block = ^{
[self doSomething];
};
在ARC下,由于对block赋值,并且引用了外界变量self,因此会触发block的copy,就从栈区复制到堆区,而堆区的block是会持有block里的对象的。在MRC下,只要主动调用copy,block也会从栈区复制到堆区,像这样
self.block = [^{
[self doSomething];
}copy];
好了回到原来的问题,
我们知道block持有了self,但本来self就持有着block,这时就造成了循环引用
我们如何解决呢?
可以使用一个__weal修饰的weakSelf变量指向self对象,在block中使用weakSelf;
__weak typeof(self) weakself = self;
self.block = ^{
[weakself doSomething];
};
block在从栈上复制到堆上的时候,会对使用strong修饰的对象引用计数加1,而对weak修饰的不作变化,因此不会增加self的引用计数
这样就解决了相互增加引用计数的问题
所以这样就完美了吗,并不是
如果当self被销毁,weakself就会被置为nil(不知道了解一下weak原理),这样再用调用其方法就会crash的。
因此,我们需要这样做
__weak typeof(self) weakself = self;
self.block = ^{
__strong typeof(weakself) strongself = weakself;
[strongself doSomething];
};
这是一种非常巧妙的做法
由于在block里定义的对象不会被堆区block持有,因此在block里访问strongself则不会增加self的引用计数
使用了__strong在strongSelf变量作用域结束之前,对weakSelf有一个引用,防止对象(self)提前被释放。而作用域一过,strongSelf不存在了,对象(self)也会被释放。
可是这样写代码有些多,想简单一点,可以用@strongify和@weakify来替代上面代码
@weakify(self);
self.block = ^{
@strongify(self);
[self doSomething];
};
@weakify(self)执行之后self就是具有__weak修饰的self,同理@strongify就是具有__strong修饰的self