用了block很长时间,也能避免相关的使用问题,想研究下大体底层实现,看了很多的优秀博客,这里写一下自己的理解。
首先block用Apple文档的话来说,“A block is an anonymous inline collection of code, and sometimes also called a "closure".
个人理解:block就是一个匿名内联的代码集合,有时也叫他"closure"<闭包>
闭包是啥:看到网上一句经典的描述,闭包就是能够读取其他函数内部的函数。
通常来说,block都是一些简短代码片块的封装,适用作工作单元,通常用来做并发任务、遍历、以及回调。
比如:
多线程的相关的操作,GCD苹果都是以block的形式,等等
数组,字典的相关的遍历,等等
网络请求的相关回调之类的,界面跳转传值,等等
当前也有很多的三方框架也应用很多的block,通过block进行异步传值,进行事件响应回调。
使用 clang -rewrite-objc 来深入了解一下block吧。
clang提供的中间代码可以带我们简单的了解一下block。clang可以将我们写完的OC代码编译成C++代码。
1、首先进入程序的目录
2、执行clang
3、生成相关的cpp文件
但是带有
#import <UIKit/UIKit.h>
的类好像是转不不成cpp文件的。
编译的结果内容比较多,一个简单的main.m文件简单的几句代码,编译之后就大概有10万多行。
很多语言都可以只实现编译器前端,生成C中间代码,然后利用现有的很多C编译器后端。
通过代码进行编译查看
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^myBlock1)() = ^{
printf("hello world1111");
};
myBlock1();
void (^myBlock2)() = ^{
printf("hello world2222");
};
myBlock2();
}
return 0;
}
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("hello world1111");
}
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_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_1(struct __main_block_impl_1 *__cself) {
printf("hello world2222");
}
static struct __main_block_desc_1 {
size_t reserved;
size_t Block_size;
} __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
void (*myBlock2)() = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock2)->FuncPtr)((__block_impl *)myBlock2);
}
return 0;
}
<1>、首先出现的结构体就是__main_block_impl_0,可以看出是根据所在函数(main函数)以及出现序列(第0个)进行命名的。
下面还有一个__main_block_impl_1,应该是按照出现序列的(第1个)进行命名的。
<2>、__main_block_impl_0中包含了两个成员变量和一个构造函数,成员变量分别是__block_impl结构体和描述信息Desc,之后在构造函数中初始化block的类型信息和函数指针等信息。从impl.isa = &_NSConcreteStackBlock;
<3>、接着出现的是 __main_block_func_0 函数,即block对应的函数体。该函数接受一个__cself参数,即对应的block自身。
再下面__main_block_desc_0描述的是block的相关信息,大小。
<4>、最下面展示的就是block的相关的调用和实现了。
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
void (*myBlock2)() = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock2)->FuncPtr)((__block_impl *)myBlock2);
执行的时候实际上就是把block的相关信息传了进去,也就是上面介绍的
__main_block_impl_0
__main_block_func_0
__main_block_desc_0
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
&__main_block_desc_0_DATA));
以上是一个block的基本。
block可以访问局部变量,甚至可以修改局部变量,那么我们来看一下是怎么实现的。
先看一个直接访问局部变量的示例
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int val = 0;
void (^myBlock1)() = ^{
printf("val === %d",val);
};
myBlock1();
}
return 0;
}
clang -rewrite-objc main.m 之后得到的转化代码
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
printf("val === %d",val);
}
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, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int val = 0;
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
}
return 0;
}
可以看到__main_block_impl_0中多了一个变量,val。
在实现中__main_block_func_0多了一个
int val = __cself->val; // bound by copy
__cself->val,应该是类似于self.val。直接把val作为一个属性来进行操作。 bound by copy这个注释标识代表了,val是被拷贝过来的,内存地址和上面的val 是不一样的。
int val = 0;
printf(" \nblock外面 %p",&val);
void (^myBlock1)() = ^{
printf("\nval === %d",val);
printf("\nblock里面 %p",&val);
};
myBlock1();
printf("\nblock外面 %p\n",&val);
打印的结果
block外面 0x7fff5fbff78c
val === 0
block里面 0x1005000f0
block外面 0x7fff5fbff78c
里外的val的对应的内存地址已经发生变化了,但是当block调用完毕之后,val的地址没有发生变化,也就是说,在block里面使用val的时候复制了一个新的地址进行使用了。也就能理解在block内部修改val是修改不了的,因为val在block的地址和外面的不一样,里面是一个临时copy的一个全新地址的参数。
但是想直接修改局部变量的时候报错了,提示要用__block来进行修饰val。
那么,为什么这个时候不能给val进行赋值呢?
摘自网上的一段解释,理解一下。
因为main函数中的局部变量val和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量val还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针访问就……
当时看完了有几个比较疑惑的点。
1、"main函数中的局部变量val和函数__main_block_func_0不在同一个作用域中"。为什么不在一个作用域。
val 的作用域是main函数,__main_block_func_0是全局的
static void __main_block_func_0
通过这个可以看出。
val只是一个局部变量
2、"main函数栈还没展开完成",这句话啥意思?展开什么意思?
展开就是释放的意思,就是说,main函数被释放了。
3、"当然,在上面代码中,我们可以通过指针来实现局部变量的修改",这个怎么实现
int main(int argc, const char * argv[]) {
@autoreleasepool {
int val = 0;
int *bbb = &val;
printf(" \nblock外面 %p val = %d",&val,val);
void (^myBlock1)() = ^{
*bbb = 30;
printf("\nblock里面 %p val = %d",&val,val);
};
printf("\nblock外面111 %p val = %d",&val,val);
myBlock1();
printf("\nblock外面222 %p val = %d\n",&val,val);
}
return 0;
}
打印的结果
block外面 0x7fff5fbff76c val = 0
block外面111 0x7fff5fbff76c val = 0
block里面 0x100107508 val = 0
block外面222 0x7fff5fbff76c val = 30
最终的结果可以看到 val的地址没有发生变化,除了在block里面<block里面 0x100107508 val = 0>,这个打印的其实不是真正的val,是block copy的val,所以还是0。
修改了val的值,并且没有修改val的地址。
来看一下我们熟悉的解决方法
添加__block修饰val对象。并且探索一下,__block怎么实现的。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block int val = 0;
printf(" \nblock外面 %p val = %d",&val,val);
void (^myBlock1)() = ^{
printf("\nblock里面 %p val = %d",&val,val);
};
printf("\nblock外面111 %p val = %d",&val,val);
myBlock1();
printf("\nblock外面222 %p val = %d\n",&val,val);
}
return 0;
}
打印结果,内存地址彻底的被修改了。执行void (^myBlock1)()的时候就被修改了
block外面 0x7fff5fbff738 val = 0
block外面111 0x100102088 val = 0
block里面 0x100102088 val = 0
block外面222 0x100102088 val = 0
通过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
printf("\nblock里面 %p val = %d",&(val->__forwarding->val),(val->__forwarding->val));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};
printf(" \nblock外面 %p val = %d",&(val.__forwarding->val),(val.__forwarding->val));
void (*myBlock1)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
printf("\nblock外面111 %p val = %d",&(val.__forwarding->val),(val.__forwarding->val));
((void (*)(__block_impl *))((__block_impl *)myBlock1)->FuncPtr)((__block_impl *)myBlock1);
printf("\nblock外面222 %p val = %d\n",&(val.__forwarding->val),(val.__forwarding->val));
}
return 0;
}
来看一下和val在没有添加__block修饰的时候的clang出来的代码的主要区别
1、由第一个成员__isa指针也可以知道** __Block_byref_val_0也可以是NSObject。
第二个成员__forwarding指向自己,为什么要指向自己?指向自己是没有意义的,只能说有时候需要指向另一个__Block_byref_val_0结构。后面我们揭晓__forwarding**。
最后一个成员是目标存储变量val。
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
2、__main_block_impl_0中也发生了变化
int val;变成了现在的*__Block_byref_val_0 val; // by ref
3、** __main_block_func_0中和之前也不一样了
int val = __cself->val; // bound by copy
变成了现在的
__Block_byref_val_0 val = __cself->val; // bound by ref
__Block_byref_val_0指针类型变量val通过其成员变量__forwarding*指针来操作另一个成员变量。
4、** __main_block_desc_0**中多了两个东西
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
从上面的clang编译出来的代码可以看到,block被转化成了__main_block_impl_0结构体实例,该实例持有__Block_byref_val_0结构体实例的指针。
看一下 val 在block中的调用的情况
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
printf("\nblock里面 %p val = %d",&(val->__forwarding->val),(val->__forwarding->val));
}
通过__cself找到结构体__Block_byref_val_0,然后通过__forwarding找到结构体的成员val。成员变量val是该实例自身持有的变量,指向的是原来的局部变量。如图所示:
<a href="http://www.cocoachina.com/ios/20150106/10850.html">图片来自</a>
但是还是存在问题
<1>、__Block_byref_val_0类型的变量对应的val仍然在栈上,当block执行回调的时候,val所对用的栈被释放了怎么办?
<2>、为什么访问val还要通过__forwarding?不直接修修改或者访问val呢?
存储域
上面的clang出的代码可以看出。 isa指向的是 _NSConcreteStackBlock,还有另外的两个类似的
_NSConcreteStackBlock 保存在栈中的block,出栈时会被销毁
_NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量
_NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁
上面我们的代码,blcok是在栈上生成的,现在创建一个_NSConcreteGlobalBlock类型的block
#import <Foundation/Foundation.h>
void (^myBlock)()=^{
printf("block");
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
myBlock();
}
return 0;
}
clang -rewrite-objc main.m之后的代码是
struct __myBlock_block_impl_0 {
struct __block_impl impl;
struct __myBlock_block_desc_0* Desc;
__myBlock_block_impl_0(void *fp, struct __myBlock_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __myBlock_block_func_0(struct __myBlock_block_impl_0 *__cself) {
printf("block");
}
static struct __myBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
} __myBlock_block_desc_0_DATA = { 0, sizeof(struct __myBlock_block_impl_0)};
static __myBlock_block_impl_0 __global_myBlock_block_impl_0((void *)__myBlock_block_func_0, &__myBlock_block_desc_0_DATA);
void (*myBlock)()=((void (*)())&__global_myBlock_block_impl_0);
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
}
return 0;
}
可以看到
impl.isa = &_NSConcreteGlobalBlock;
分配在全局的block,当超出变量的作用域的时候,依然可以通过指针进行安全的访问。但是在栈上面的block,如果他所属于的变量的作用域结束,那么该block的作用域结束也就结束了。同样的__block修饰的变量也分配在栈上,当超过该变量的作用域时,该__block修饰的变量也会被废弃。
这个时候就需要一个在堆上面block,生命周期由我们自己控制。
_NSConcreteMallocBlock登场。
这个时候就会将block和__block修饰的变量,从栈上复制到堆上面。那么,栈上的block就可以超出所属的变量的作用域了,那么复制到堆上面的block以及__block所修饰的变量仍然可以进行操作。
复制到堆上面的block也就变成
impl.isa = &_NSConcreteMallocBlock;
而此时 结构体中的__forwarding就发挥了作用,保证能访问从栈上拷贝到堆上的__block修饰的变量。
我们一般可以使用copy方法手动将 Block 或者 __block变量从栈复制到堆上。比如我们把Block做为类的属性访问时,我们一般把该属性设为copy。有些情况下我们可以不用手动复制,比如Cocoa框架中使用含有usingBlock方法名的方法时,或者GCD的API中传递Block时。
当一个Block被复制到堆上时,与之相关的__block变量也会被复制到堆上,此时堆上的Block持有相应堆上的__block变量。当堆上的__block变量没有持有者时,它才会被废弃。(这里的思考方式和objc引用计数内存管理完全相同。)
当栈上的__block修饰的变量被复制到了堆上之后,那么之后访问堆上的变量就通过val->__forwarding->val了。
让我们来看一下上面的第4点不同,多了的那两句话,此时main_block_desc_0多了两个成员函数,分别是** copy和 dispose分别指向__main_block_copy__0和__main_block_dispose__0**
当block从栈上被拷贝到堆上的时候,会调用__main_block_copy_0将__block类型的成员变量val从栈上复制到堆上;而当block被释放时,相应地会调用__main_block_dispose_0来释放__block类型的成员变量val。
这时候,__forwarding的作用就体现出来了:当一个__block变量从栈上被复制到堆上时,栈上的那个__Block_byref_val_0结构体中的__forwarding指针也会指向堆上的结构。
<a href = "http://www.cocoachina.com/ios/20150106/10850.html">图片来自</a>
__block可以指定任何的局部变量,上面的代码有如下代码
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
当block从栈上复制到堆上面的时候,会使用__main_block_copy该方法持有变量(相当于retain),当堆上面的block被废弃的时候,就会使用__main_block_dispose释放__block修饰的变量(相当于release)。
我们一开始用的demo,block一直实在栈上的,声明的局部变量也是在栈上的,他们都在一个方法中,生命周期的话都依据所持有的方法。方法释放了,局部变量和block也玩儿完,但是为什么这个时候局部变量想在block中修改还是必须得添加__block呢?
猜想,可能这是苹果指定的规则,block是作为参数传递以供后续回调执行的,block被传递出去了,局部变量持有者可能因为没啥用了就被释放了,那么局部变量也就被释放了,再在block中修改局部变量就危险了。所以,不管在什么时候,blcok中修改局部变量都得添加__block来进行修饰。
理论部分说完了,来玩一下断点看一下情况吧。
不对啊,和之前说好的不一样啊,不应该是在栈上的吗?
不应该是clang以后的_NSConcreteStackBlock吗?难道上面的理论都不成立?网上肯定不只有我瞎扯。
看到大神的博客,安心了。
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/#NSConcreteGlobalBlock__u7C7B_u578B_u7684_block__u7684_u5B9E_u73B0
在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中,会输出 <NSMallocBlock: 0x100109960>
。在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。
由于 ARC 已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。
如有失误请各位路过大神即时指点,或有更好的做法,也请指点一二,在下感激不尽。
参考的网址:
http://www.cocoachina.com/ios/20150106/10850.html
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/
http://blog.csdn.net/jasonblog/article/details/7756763
http://blog.csdn.net/hherima/article/details/38620175
http://www.dreamingwish.com/articlelist/category/toturial