在日常开发中,我们常常会定义block将一段代码保存起来等待合适的时机调用来完成一系列的操作(hehe...出bug了吧)。我们知道block中无法修改引用的外部变量除非使用 __block 修饰外部变量,使用block时需要格外注意循环引用。在文章中我们将会详细探讨究竟是什么原因导致了上述问题。
从hello word开始
编写hello word是一位程序员必备的编程技能,为了提升我们的逼格与社会接轨,我们也来写一段hello word。
int main() {
void(^block)(void) = ^{
printf("hello word");
};
block();
return 0;
}
很简单是不是? 然而事情并没有那么简单,此hello word非彼hello word。
接下来进入烧脑环节 ^_^ ,有条件的读者可以先喝罐一二三四五六七八个核桃...
接下来我们借助clang编译器,将这段block转换为C++代码。在控制台输入 clang -rewrite-objc 文件路径,我们会得到一个.cpp文件,打开文件找到 main 函数。
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;
}
Excuse me? 这一坨是什么鬼?懵逼吗?懵逼就对了因为代码没贴全😊😊😊。
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("hello word");
}
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() {
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;
}
好了,贴全了,各位是不是有种恍然大悟茅塞顿开的感觉。
路人甲乙丙丁戊己庚辛壬癸:你TM在逗我...
哎哎哎,别急我话还没说完呢麻烦各位刀先收一收,大家都是文明人。
那什么... 二营长,你他娘的意大利炮能不能先抬回去!
#@#¥¥#...
咳咳... 好了我们先切入正题。
接下来我们逐行分析。
void(^block)(void) = ^{
printf("hello word");
};
//对应C++代码
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
void(*block)(void):定义一个名为block的函数指针指向类型为 void(*)(void) /无返回值无参 的函数。而从代码中可以看出实际上block指向的并非一个函数而是一个结构体对象。
__main_block_impl_0:block结构体 __main_block_impl_0表示这个block是名为mian的函数中定义的第0个block。(第0个?emm... 有毛病?没毛病!)
struct __main_block_impl_0 {
//可以理解为block的基类,所有block结构体都包含__block_impl中定义的成员变量
struct __block_impl impl;
//block描述信息
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
//block存储区域 _NSConcreteStackBlock栈 _NSConcretGlobalBlock全局 _NSConcretMallocBlock堆
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
//函数指针 指向代码块所对应的函数
impl.FuncPtr = fp;
//block描述信息
Desc = desc;
}
__main_block_func_0:block对应的函数,函数的实现由block中包含的代码转换而来。这个函数接收__main_block_impl_0类型的结构体对象做为入参。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
//__cself的作用在此我们先不做解释后文中讲解__block时在详细说明。
//我们的block中输出hello word的代码
printf("hello word");
}
__main_block_desc_0_DATA:包含block的描述信息。
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)};
block();
//对应C++代码
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
上文中我们提到block指针指向的并非是一个函数,而是一个block结构体对象,所以在这里我们通过block指针找到结构体对象中的FuncPtr指针,FuncPtr指针指向的才是我们需要调用的函数,并将block结构体对象做为入参。(千万不要看着一堆圆括号就感觉头大,不妨将圆括号中的内容拆解一下可能会比较好理解)
(
//类型转换 将FuncPtr强转为void (*)(__block_impl *)类型的函数指针
(void (*)(__block_impl *))
//取FuncPtr
((__block_impl *)block)->FuncPtr
)
//将block转为__block_impl * 类型做为入参 调用FuncPtr
((__block_impl *)block);
到这里大家是不是对block有了一些不一样的理解呢?
如果没有请把上面的内容再认真阅读一遍。
我们知道在block中是不能修改引用的外部变量的,如果想要修改那么我们需要使用__block来修饰外部变量,接下来我们就来看一看__block到底对我们的代码做了什么。
咦? 二营长人呢?
路人甲:他去看上面的内容了。
emm...
__block? 放开那行代码,让我来!
在讲__block之前我们不妨先看看下面的代码
int main () {
int a = 10;
void(^block)(void) = ^{
printf("%d",a);
};
block();
return 0;
}
打开生成的C++文件我们可以发现__main_block_impl_0的成员变量中多了一个 int a;
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;
}
};
__main_block_impl_0结构体的构造函数中多了一个int类型的参数_a,_a最终会被赋值给结构体中的成员变量a。而结构体中的成员变量a、函数参数_a、外部变量a他们的内存地址都不相同。当执行过block的语法后,即使外部对a重新赋值也不会改变block结构体中a的值(修改的不是同一块内存)。
讲道理我们是可以修改结构体中成员变量a的值的,但是当我们修改a的值时编译器会无情的给我们一个大大的报错。
xCode:兄dei,这里的a和外面的a不是同一个a,你改这里的a外面的a不会变的,哈哈哈...
程序员0:哦,__block。
xCode:咳咳... 老夫纵横江湖这么多年,见过的程序员连起来能绕地球两圈,你是他们中最优秀的,没有之一!
程序员0:哦。
为了能够在block内部修改引用的外部变量的值,我们需要用__block修饰外部变量。
int main () {
__block int a = 10;
void(^block)(void) = ^{
a = 20;
printf("%d",a);
};
block();
return 0;
}
同样转为C++代码
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
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_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 20;
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*/);}
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};
int 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;
}
说实话,确实有点辣眼睛... 不过别担心,同样的我们逐行分析。
__block int a = 10;
//您的__block请查收
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
我们的int a变成了__Block_byref_a_0 a,what? __Block_byref_a_0又是什么鬼。
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
__Block_byref_a_0结构体中包含一个本身类型的指针 __forwarding 和一个 int 类型的变量a 。当我们使用__block修饰变量a时变量被转换成了上述结构体对象a,而这个结构体中的__forwarding会被赋值为本身的地址,__forwarding的作用会在后续讲解block在内存中的存储区域时讲到,这里我们先不管他。我们对a赋值会转换为对结构体中的成员变量a赋值。
void(^block)(void) = ^{
a = 20;
printf("%d",a);
};
//C++
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
block:函数指针,指向的实际是一个block结构体。
__main_block_impl_0:我们的block被转换为结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
//指向外部变量a
__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;
}
};
__main_block_func_0:其中包含了block中的代码。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
//通过__forwarding来修改结构体中a的值确保修改的正确性
(a->__forwarding->a) = 20;
printf("%d",(a->__forwarding->a));
}
前面我们提到过__forwarding指向的是自己的地址,为什么这里不直接对成员变量a赋值而是通过__forwarding找到自己然后在对自己的成员变量a赋值呢?可能大家会感觉这样做是多此一举,但是现实很残酷。我们后面会详细介绍。
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
至于这一行我们就不讲了,不懂请翻前面的内容。
好吧实际上到这里为止好像我们只知道__block把我们的 int a 转换成了 __Block_byref_a_0 a ,还挖了一堆坑。
其实__block只做了一件事情,那就是把我们的变量转换为结构体,至于为什么这样做我们还需要先了解一下block的存储区域。
block存储区域
_NSConcreteStackBlock 保存在栈中的block,出栈时会被销毁
_NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量
_NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁
当我们把block作为全局变量使用时,生成的block将被存储在全局区,对应的impl.isa会被设置为 &_NSConcreteGlobalBlock。除此之外block创建时内存分配在栈上impl.isa会被设置为 &_NSConcreteStackBlock,作用域结束block会在出栈时被销毁。那么问题来了,如果我们需要block脱离当前作用域的限制该怎么办呢?这时候_NSConcreteMallocBlock就粉墨登场了。
将栈上的block复制到堆上,这样当栈上的block超过他的作用域时,堆上的block还可以继续存在,被复制到堆上的block结构体成员变量isa将变为 &_NSConcreteMallocBlock。
impl.isa = &_NSConcreteMallocBlock;
__forwarding
当我们将一个栈上的block复制到堆上时,与其相对应的__block变量同样会被复制到堆上,此时我们拥有两份block以及__block变量,并且他们的内存地址不同,此时问题就出现了。
栈上的block结构体存储的是栈上的__block变量而堆上的block结构体存储的是堆上的__block变量,当我们在其中一个block中修改__block变量时,另一个block结构体中的__block变量如何进行同步?
此时我们的__forwarding终于派上了大用处,当我们将栈上的__block变量拷贝到堆上时,将栈上的变量的__forwarding指向堆上的变量,堆上的变量的__forwarding指向自己,这样无论是在栈上还是堆上__forwarding始终指向堆上的变量,当我们通过__forwarding修改变量时修改的都是堆上的变量。
因此就有了我们上面见到的代码
(a->__forwarding->a) = 20;
为什么block属性需要使用copy修饰
在ARC下,使用strong和copy都是一样的,因为在访问/修改外部变量的时候,block都是在堆区,苹果官方建议使用copy
在MRC下,单纯的Block是存放在全局/常量区的,如果Block访问/修改外部变量后,block存放在了栈区,在栈区是不可以全局共享的,只有堆区的对象,变量才会被全局共享,所以使用copy拷贝一份Block到堆区中,这样Block才会全局共享
关于循环引用
程序员1:呦,哥们,撸代码呢。
程序员0:嗯(self.xxx = xxx)。
程序员1:哎哎哎,哥们,你这里循环引用了,block里面怎么能直接用self,快weak weak。
程序员0:滚犊子!
block中使用self就一定会循环引用吗? 呵呵...
造成循环引用的条件是出现引用环,而解决的方案也很简单,打破引用环。
我们先看看下面的代码
@implementation RetainCircle {
void(^_block)(void);
}
- (instancetype)init
{
self = [super init];
if (self) {
void(^block)(void) = ^{
NSLog(@"%@",self);
};
block();
_block = ^{
NSLog(@"%@",self);
};
_block();
}
return self;
}
- (void)dealloc {
NSLog(@"GG思密达");
}
@end
问:有循环引用吗?
这两个block都引用了self,block和self的关系无非就是被self持有和不被self持有,上面的代码所有的组合都占了,那必须有循环引用啊,这还用问吗?
程序员嘛,就这一点好,逻辑推理能力6的不要不要的,来来来,双击666,小礼物走一波~
咳咳...其实我想问的是哪一个block有循环引用。
上面两个block中都持有了self,而_block被self持有,这样就造成了_block持有self,self持有_block。
_block:哥,要不你先挂?
self:凭什么我先挂,我是你哥,要挂也轮不到我。
_block:hehe...
block:两位哥哥,小弟先走一步,勿念...
所以当我们在block中使用self时并不一定会造成循环引用。
如果造成了循环引用也不要担心,我们是有解决方案的,先平复一下心情,下面我们看一看解决方案。
解决方案:
略。
啊,不好意思各位,翻错页码了,见谅见谅。
MRC环境中:
新建一个__block的局部变量,并把self赋值给它,而在block内部则使用这个局部变量来进行取值。因为__block标记的变量是不会被自动retain的。
__block typeof(self) mSelf = self;
_block = ^{
NSLog(@"%@",mSelf);
};
ARC环境下:
__block要换成__weak,因为ARC环境下自动释放池会自动做引用计数的增减。此处我们需要一个弱引用对象controller指向self对象,这样即便在block中使用了controller,由于它是一个弱引用,可以使用self的地址空间但是并不会造成引用计数加1
__weak typeof(self) weakSelf = self;
_block = ^{
NSLog(@"%@",weakSelf);
};
补充 2018-5-18
关于解决block的循环引用还需要了解的一件事情
如果我们的block中引用了成员变量该怎么解决循环引用呢?
当然是weak啦
Excuse me?
那你让我strong可别怪我不客气了。
哎呦喂!好了。开心,撸斤小龙虾先。
为什么在block里面需要使用strong
是为了保证block执行完毕之前self不会被释放,执行完毕的时候再释放。这时候会发现为什么在block外边使用了__weak修饰self,里面使用__strong修饰weakSelf的时候不会发生循环引用?!
__strong修饰的self只是为了保证在block内部执行的时候不会释放,但存在执行前self就已经被释放的情况,导致self=nil。注意判空处理。
不会引起循环引用的原因
因为block截获self之后self属于block结构体中的一个由__strong修饰的属性,会强引用self, 所以需要使用__weak修饰的weakSelf防止循环引用。
block使用的__strong修饰的self是为了在block生命周期中self不会提前释放。self实质是一个局部变量(在block这个“函数”里面的局部变量),当block执行完毕就会释放自动变量self,不会对self进行一直进行强引用。
参考
iOS内存管理---block机制详解
iOS 关于 __block 底层实现机制的疑问?
blocksruntime
iOS Block底层实现原理详解
你真的理解__block修饰符的原理么?