Block的解析
来源:互联网 发布:three.js skybox 编辑:程序博客网 时间:2024/06/03 20:26
一,block是什么?
*带有自动变量的匿名函数。
————引自《iOS与OS X多线程和内存管理》*
1.匿名函数
首先blocks是c语言的扩充功能
c语言中函数是这个样子的:
void func() { printf("hello world");}
那么,block是一个什么样子的呢?
^void () { printf("hello world");}
这种不带名的函数就是所谓的匿名函数
2.带自动变量
还是要从c语言函数说起。
int a = 10;void func (int b) { printf("%d + %d = %d",a,b,a+b);}int main(int argc, char * argv[]) { func(5); return 0;}
上述的代码主要想说明一件事,C语言函数中,函数体中使用的函数外部变量只有两种:函数参数即全局变量。
那么block是怎么使用的呢?
int main(int argc, char * argv[]) { int a = 10; void(^block)(int) = ^(int b) { printf("%d + %d = %d\n",a,b,a+b); }; block(5); return 0;}
我们看到,在此例中a已经不是全局变量了,而是一个局部变量,也就是自动变量。然而block却可以正常使用,为什么呢?因为block内部维护了一个变量a的值,所以执行正确。这里你先不用纠结,下面会有源码。由上我们就知道了什么叫做带自动变量了。
二,block的实质
想要看Block的实质我们还是Block的实现过程。我们还是要借助clang。
#include <stdio.h>int main(int argc, char * argv[]) { int a = 10; void(^block)(int) = ^(int b) { printf("%d + %d = %d\n",a,b,a+b); }; block(5); return 0;}
还是这个简单的函数,我们借助clang来转换一下。这里为了稍后方便,我们尽量删除其他无用头文件,引入必要头文件。
clang -rewrite-objc main.m
转换完成后我们会发现main.m同文件夹下多了一个main.cpp的文件。打开这个文件,现在command + L跳转到第62行,复制62行至67行,command + 下调至文件底部粘贴,再跳至510行,command + shift + 上选中上面所有代码,delete删除后就剩下干货了,大概是这个样子的:
在block结构体中,三个成员变量,一个构造函数。
第一个成员变量是__block_impl的结构体,其中有block实现函数的函数指针。第二个成员变量是__main_block_desc_0,用来负责block的内存管理。第三个int型成员变量是a。
int a这个成员变量就是上面提到的带有的自动变量。因为block内部引用了外部的自动变量,所以在block结构体中多了一个同类型同名的成员变量。同样,如果没有引入外部的自动变量的话此处block结构体中也不会有这第三个成员变量。
现在将目光集中到main函数中。可以看到,第一行声明了一个局部变量,第二行调用了block的构造函数,将block对应的函数指针和Desc以及局部变量传给了函数指针指向的函数
然后我们看到,第三行调用block结构体的函数指针指向的函数,并把block自身及参数传给了函数指针指向的函数。
转过来看block指向的函数,函数首先从block自身中提取捕获的自动变量a赋值给一个临时变量,同时执行原本block的函数体。
至此就完成了一次block函数的调用过程。
这里我们要注意下捕获的自动变量:
所谓捕获的自动变量我们可以从两方面来理解:
1.我们看到在生成block的瞬间就将自动变量的值赋给了block。所以此时外界计时修改局部变量的值并不影响block中的值。
2.block中我们是不能对捕获的变量进行赋值操作的,只要这么做编译器就会警告。为什么苹果会做出这样的限制呢?因为在block里对捕获的自动变量复制其实是有歧义的。因为通过看__main_block_func_0内部的实现我们知道,block内部使用的都是block捕获到自动变量,当然这个自动变量是我们转换代码之前完全不知道的一个概念。也就是在编码过程中我们在block中使用的变量与实际代码运行过程中block内部操作的变量本就是两个变量,所以在这里修改block捕获的自动变量的值事实上跟开发者预期的结果完全是两个结果。所以苹果干脆在此就给出个警告来避免未知的错误。
3.虽说不能对捕获的自动变量进行赋值操作,但这并不影响我们使用他,否则的话这个自动变量捕获到也没有什么用了。这点很好理解,没什么好解释的。
三,关于Block对外部的赋值操作
上文说过,Block不能对其捕获的局部(非静态)变量的值进行赋值操作。既然有这些限制,那么一定有可以Block中可以做赋值操作的变量,他们都有谁呢?
- 静态变量
- 全局变量
- __block说明符修饰的变量
还是针对带有自动变量的匿名函数这句话来讲。这一节我们来探讨一下Block是如何使用外部变量的。我们知道Block截获变量的意义在于想要使用Block作用于内无法使用的变量,所以他要截获变量。接下来围绕着这句话从各种变量类型做深入的展开。
1.仅使用参数的Block
int main(int argc, char * argv[]) { void(^block)(int) = ^(int a) { a = 10; printf("block : a = %d\n",a); }; int a = 5; block(a); printf("a = %d",a); 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, int a) { a = 10; printf("block : a = %d\n",a);}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[]) { void(*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); int a = 5; ((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, a); printf("a = %d",a); return 0;}
由于使用的是函数的参数,是在Block作用域内可以使用的,所以Block没有对变量进行截获。这个Block基本就是最简单的函数。
2.使用局部变量(非静态)
int main(int argc, char * argv[]) { int a = 10; void(^block)() = ^() { printf("n = %d",a); }; block(); return 0;}///clang转换后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("n = %d",a);}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[]) { int a = 10; void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); return 0;}
我们看到,这里截获了这个局部变量,具体原因在上述内容中有讲到过,此处不再赘述。
3.局部静态变量
我们知道,静态变量存储在静态区,只创建一次,随后使用的同名变量均应指向同一地址。由静态变量的特性我们应该知道,如果Block截获了一个静态局域变量,并在Block中对其值进行了更改,这个操作应该是有效的,他应该改变该变量的值。我们看下他是如何实现的?
int main(int argc, char * argv[]) { static int a = 10; void(^block)() = ^() { a = 20; }; block(); printf("a = %d",a); return 0;}///clang转换后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 (*a) = 20;}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[]) { static int a = 10; void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); printf("a = %d",a); return 0;}
我们看到了,Block截获的是局部静态变量的指针。这个思路跟C语言中函数一样。C语言中我们想更改实参的值时也是通过传址的方式实现的。形如:
void mySwap(int * a,int * b);int main(int argc, char * argv[]) { int a = 1; int b = 2; mySwap(&a, &b); printf("a = %d",a); return 0;}void mySwap(int * a,int * b) { int temp = *a; *a = *b; *b = temp;}
4.全局变量(静态与非静态)
上面说过,Block捕获变量是为了在Block中使用其作用域外的变量,那么全局变量本身作用在区域,Block可以使用,故不需要对全局变量进行捕获。以下以全局静态变量为例。
static int a = 10;int main(int argc, char * argv[]) { void(^block)() = ^{ a = 20; }; block(); printf("a = %d",a); return 0;}///clang 转换后static int a = 10;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) { a = 20;}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[]) { void(*block)() = ((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); printf("a = %d",a); return 0;}
5.__block修饰的变量
我们知道,被__block修饰的局部变量,在Block内部对其进行赋值操作是可以的,那么他是如何实现的呢?
int main(int argc, char * argv[]) { __block int a = 10; void(^block)() = ^{ a = 20; }; block(); printf("%d",a); return 0;}///clang 转换后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;}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(int argc, char * argv[]) { __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 (*)())&__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); printf("%d",(a.__forwarding->a)); return 0;}
我们看到__block修饰的变量会自动为其生成一个结构体,并在之后对变量的操作使用的都是结构体中持有的变量a。而后Block捕获了结构体,Block中对变量的复制也映射成了对结构体内部变量的赋值。我们可以发现,在上面的例子中,不仅生成了一个__block变量的结构体,还多了__main_block_copy_0和__main_block_dispose_0两个函数,的具体作用我们稍后再表。
四,Block类型及其存储域
首先应该了解一下Block的三种类型:
- _NSConcreteStackBlock///栈区Block
- _NSConcreteMallocBlock///堆区Block
- _NSConcreteGlobalBlock///全局Block
我们设想这样一种情况,上述的例子中,我们看到我们都是在main函数中声明的Block,也就是说他其实block对象其实是一个局域变量,那么他一定会被存储在栈上。也就是说当出了变量的作用于,也就是main函数结束,block对象就会被销毁。这时我们的Block即为_NSConcreteStackBlock。我们可以从__block_impl结构体中的isa指针看到上述例子中的Block均为_NSConcreteStackBlock类型。
但是平时我们使用block的时候还有这么一种情况,即并不是在声明的地方立即使用,而是在等待某个时机从而进行回调。而此时一般是已经出了block对象的作用域,如果跟之前一样是栈区block的话显然block已经被销毁,此时进行回调只会引起crash。这时我们就需要_NSConcreteMallocBlock区的block了,即堆区Block。回想我们持有block的时候使用什么修饰符呢?copy对吧,而block对象执行copy操作就是将其按需复制到堆区。
#import <Foundation/Foundation.h>int globalVar = 100;void(^globalBlock)() = ^{ NSLog(@"global block beyond main");};int main(int argc, char * argv[]) { int a = 10; NSLog(@"stack block: %@",^{NSLog(@"here a = %d",a);}); NSLog(@"malloc block: %@",[^{NSLog(@"here a = %d",a);} copy]); NSLog(@"global block: %@",^{NSLog(@"I use nothing");}); NSLog(@"global block beyong main: %@",globalBlock); NSLog(@"global block use globalVar: %@",^{NSLog(@"%d",globalVar);}); return 0;}///输出:stack block: <__NSStackBlock__: 0x7fff5d7cd578>malloc block: <__NSMallocBlock__: 0x60000004ef70>global block: <__NSGlobalBlock__: 0x1024320d0>global block beyong main: <__NSGlobalBlock__: 0x102432050>global block use globalVar: <__NSGlobalBlock__: 0x102432110>
此处我们可以看到三种block类型。从源码我们可以知道默认生成的block均为_NSConcreteStackBlock类型,而后执行了copy操作的block为_NSConcreteMallocBlock类型,后面三个均为_NSConcreteGlobalBlock类型。
这里我们先说_NSConcreteGlobalBlock类型的Block。在全局范围内声明的Block即为全局Block,并且没有引入自动变量的也为全局Block。
现在我们知道了,调用过copy方法的block会被复制到堆区,堆区的Block均为_NSConcreteMallocBlock类型。那么什么情况下block会执行copy方法呢?
其实我们可以从上述的分析中猜到,当block需要在其作用域外使用的我们应该将其复制到堆区。例如block作为函数返回值的时候,这时候编译器会按需调用copy方法:
typedef void(^voidBlock)();voidBlock func();int main(int argc, char * argv[]) { NSLog(@"the return value of func:%@",func()); return 0;}voidBlock func() { int a = 10; NSLog(@"the block in func:%@",^{NSLog(@"block in func : a = %d",a);}); return ^{ NSLog(@"block in func : a = %d",a); };}///输出:the block in func:<__NSStackBlock__: 0x7fff56877558>the return value of func:<__NSMallocBlock__: 0x6080000486a0>
此例中我们看到函数体中,输出了一个Block其为栈区Block,但是当将同样的Block作为返回值返回到main函数中的时候,他变成了堆区Block。
同学们不要说我这不是一个Block,我应该生成一个Block将其赋值,Log一下,在返回出去。这个真不是我不赋值,我不能啊,因为在ARC中赋值的时候如果不附加修饰符的话默认认为生成的变量是以__strong修饰符修饰的,而编译器遇到__strong修饰符会自动copy。。。我怎么给你做例子啊。。。反正这么写虽然不是同一个block,但是应该是同一类型block,足以说明问题。另外说过,编译器会按需调用copy方法。也就是说栈区block会出作用域销毁,全局block并不会,所以如果返回值是一个全局block的话,则不会调用copy方法。
此外以下两种情况也会由系统为我们调用copy方法:
- Cocoa框架的方法且方法名中含有usingBlock等时
- GCD的API
还有就是显示调用copy方法的时候,另外如果将其赋值给有copy修饰符修饰的属性的话也会调用copy方法。
然而什么时候应该调用copy方法呢?我们先来看下不同类型block调用copy方法会有什么行为。
Block类型 副本源的配置存储域 复制效果
_NSConcreteStackBlock 栈 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 堆 引用计数增加 不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。————引自《iOS与OS X多线程和内存管理》
但是在我们确定的时候,还是要根据需要调用copy方法,不要盲目调用copy方法,毕竟这个方法是十分占用CPU资源的。
五, __block说明符
上文中,已经讲述了block对象在调用copy方法时候的行为。然而__block说明符修饰的变量与block对象基本一致。
正如在上文中提到的,被__block说明符的变量会自动生成一个结构体。
值得一提的是三个地方:
只有被block说明符修饰的变量,今后使用的均为其结构体中维护的同名成员变量,不过从源码中我们看到,并不是简单地使用了成员变量,而是a.forwarding->a
这样一个引用方式,这是因为什么呢?
首先从__Block_byref_a_0中我们可以看到forwarding是一个Block_byref_a_0
类型的结构体指针。
从main函数中第一行block变量生成的代码我们看出,在本例中生成__block变量a的同时将a的__forwarding指向了a自身。这样a.forwarding->a
最终还是指向了__block变量a结构体中的成员变量a。
既然这样,就一定存在__forwarding并不指向block变量自身的情况,故此才需要__forwarding存在来保证时刻能取到一个正确的值。而上文中提到的调用copy方法的时候,就会对__forwarding指针进行操作。
由上图我们可以看到,当调用copy方法后,__forwarding指针指向堆中的__block变量。而堆中的__block变量的__forwarding指针则指向自身。
同时我们知道,block其实是对c语言的扩充,然而OC中我们使用的是引用计数来管理对象生命周期,而不是GC。所以事实上Block需要自行管理内存。那么当我们的Block捕获了一个对象时,他又是如何管理其引用计数的呢?
上文中有提到过__main_block_copy和__main_block_dispose两个函数。当Block结构体中捕获到的对象需要retain的时候则调用__main_block_copy方法增加引用计数,当其需要释放的时候则调用__main_block_dispose释放对象。所以当block从栈上复制到堆的时候会调用copy函数,而对上的block被释放时调用dispose函数。
六,关于block的引起的循环引用
一直以来,Block引起的循环引用都让不少初级工程师,甚至包括一些中级工程师(索性就叫他中级吧。。。)谈虎色变。他们不知道Block是如何引起循环引用的,只知道__weak可以避免循环引用。知其然不知其所以然,闹出一些笑话也是让人无语。
首先说一下什么是循环引用?
引用计数机制不做展开,我们只需要知道,在OC中对象是在引用计数为0的时候进行销毁的。一个对对象的强引用会造成一次引用计数的加一。释放一个强引用会造成引用计数的减一。
Block对内部使用的自动变量造成一个强引用,而如果这个自动变量恰好对Block也有强引用的话就会造成循环引用。
既然知道了循环引用的起因,那么我们只要打破引用的闭环就可以轻松解决。两个思路,一个是从最开始就不让强引用成为闭环,使用弱引用。另一个思路是找到一个合适的时机主动释放一个强引用,打破闭环。
1.弱引用
__weak typeof(self)weakSelf = self;self.block = ^{ NSLog(@"%@",weakSelf);};self.block();
上述代码中,使用__weak生成一个弱引用变量weakSelf,保持对self的弱引用。然后Block捕获到weakSelf,对weakSelf也是弱引用,然而却没有造成闭环。故避免了循环引用。
主动释放
__block id blockSelf = self;self.block = ^{ NSLog(@"%@",blockSelf); blockSelf = nil;};NSLog(@"%@",self.block);self.block();
上述代码中,使用__block生成一个block对象blockSelf,保持对self的强引用。然后Block捕获到blockSelf,强引用blockSelf,由于self对block还有一个强引用,此时形成了一个闭环。但当block调用的时候,内部最后将blockSelf对象置为nil。由于blockSelf置为nil,__block对象失去强引用被销毁,同时释放对self的强引用,从而打破闭环。
不过两种避免循环引用的方式都有各自的缺点。
__weak 的弱引用形式的缺点在于,当block执行的时候,由于对self是弱引用,不能保证self对象是否已经被销毁。事实上block执行前self被销毁还好,顶多是不执行。但是如果在block执行过程中,self被销毁就会造成不可预估的后果。所以当使用__weak的时候我们通常会看到如下结构:
__weak typeof(self)weakSelf = self;self.block = ^{ __strong typeof(weakSelf)strongSelf = weakSelf; NSLog(@"%@",strongSelf);};self.block();
这样的结构可以保证在block执行过程中,不会因为self释放引起问题,然而如果block执行前self被释放后block也就没有机会执行了,也算是对代码的保护。
__weak有这样的缺点,为什么不适用__block等方式呢?
事实上__block同样有着自己的烦恼,就是一定要在block体中对__block对象置为nil,且block一定要执行才可以解决循环引用。所以开发者要根据具体情况合理的选择解决循环引用的方式。
- Block的解析
- block 解析
- block 解析
- DATA BLOCK内部结构的解析续二
- Block 4:Block实质解析
- Block 4:Block实质解析
- ie6 ie7 的div 对 display:inline-block的解析
- ARC下的block导致的循环引用问题解析
- ios 全面解析block
- ios 全面解析block
- iOS block 陷阱解析
- Block解析图
- Block-levelDB源码解析
- oc block解析
- Block面试题解析
- Block面试题解析
- iOS block 陷阱解析
- ios block例子解析
- Session总结
- Android常用控件八之使用RadioButton切换图片,代码实例
- 2017年河南省ACM省赛 Problem H: Intelligent Parking Building
- mysql my.ini配置文件中的 max_allowed_packet
- maven打包jar,排除不需要jar
- Block的解析
- 内存四区分配图和指针
- combo_box SetCurSel(i)设置显示的内容
- 多重背包(二进制优化)
- 路由器工作在哪一层
- Intellij IDEA配置PHP开发环境
- git学习------>在CenterOS系统上安装GitLab并自定义域名访问GitLab管理页面
- xcode duplicate symbols lipo
- Eclipse快捷键