9 Block详解
1.明白如何定义block类型
定义Block类型:
typedef 返回值类型 Block名字 参数
block语法:
^(void){printf("Block %d \n",used);};
^ 返回值类型 参数列表 表达式
block语法是真正实现了匿名函数的地方,上边只是声明了一种block的类型。block的结构体的创建是在匿名函数被实现的时候创建的。
2.block所在内存区域(完)
block所在的内存区域为三种:栈,数据区,堆。
通过clang编译之后生成的【命名规则】结构体中impl结构体中的isa是由谁来生成,可以建立与所在内存区域的联系。
_NSConcreteStackBlock ———— 栈
_NSConcreteGlobalBlock ———— 数据区
_NSConcreteMallocBlock ———— 堆
具体哪种类型的block会在哪种内存区域中呢?
①.
记述全局变量的地方有block语法的时候;(实际上就是不需要截取外部变量。)
block语法的表达式不截获所在函数的局部变量的时候。
Block为_NSConcreteGlobalBlock类对象。虽然在第二种情况中,clang转换的源代码仍然是_NSConcreteStackBlock,但是具体实现上应该有所不同。具体的不太知道。
②.
超出变量作用域的Block,在ARC环境下有些会通过编译器进行适当的判断,然后通过内部调用objc_retainBlock,进行_block_copy的操作,复制到堆区,有些无法判断出来的就需要手动的调用copy方法了。比如向方法或者函数的参数中传递Block的时候。[[NSArray alloc] initWithObjects:[blcok copy],[block copy],nil];
3.block为什么使用copy修饰符 什么时候要对Block调用Copy实例方法。
arc下其实strong和copy是一样的,但是arc无效的时候就需要手动的通过copy方法来将其赋值到堆区。为什么?
对于内存的管理我们是通过retain和release,能够自动调用retain的地方有四种情况 alloc/new/copy/mutableCopy。他们都可以对一个对象进行持有。
当我们首次持有一个Block对象时,(实际上就是将其从栈中复制到堆上的时候)但是对于配置到栈上的Block调用retain并不起到任何作用。所以通过copy将其复制到堆区并且持有它。(对于栈中的Block,当超出其作用域的时候就会自动释放。)只要有一次复制并配置在了堆上,就可以通过retain调用了,正是因为第一次复制到堆上这种情况的缺陷,所以MRC下,使用了copy修饰符。
而arc下为什么也可以用strong修饰,因为ARC下对Block进行内存管理不是自动调用的retain方法,而是retainBlock方法,这个方法实际上调用的是_Block_Copy函数,他的效果和copy实例方法是一样的。所以没有问题。
除了以下三种情况外,需要对Block调用copy的实例方法:
Block作为函数返回值时。
将Block赋值给__strong修饰符修饰的id类型或者Block类型的成员变量时。
GCD或者含有usingBlock的Cocoa框架方法
因为Block需要从栈上复制到堆上的情况就四种除了上述三种就是调用copy实例方法的情况,所以除了上述3种情况,就都需要调用copy方法。而且上述3种情况最终都是通过调用_Block_Copy方法实现的从栈复制到堆。但是直接调用copy实例方法并不是调用了_Block_Copy,他们只是效果相同
4.如何将block从栈上移到堆上(完) 实际上并不是移动,而是复制。
__block修饰block内所捕获的变量
block赋值到了属性property中,arc下 strong和copy都可修饰,非arc只能用copy)
之前我们提到了用__block修饰的变量实际上是生成了一个新的结构体,当Block语法中使用了这个变量,这个变量会以这个结构体实例的方式成为block【命名规则】结构体的成员变量,当block被复制到堆上的时候,对这个变量也就是这个结构体实例,也就被移到堆上,并且仍然被这个block所持有。如果这个__block变量被多个Block使用,而这多个Block中其中也有移到堆上的话,那么就增加这个__block的引用计数。当配置在堆上的Block,被释放的时候,减少这个__block的引用计数,直到被释放。
之前说的__block生成的结构体中的_forwarding指针的作用实际上就是为了 【不管这个变量配置在栈上,还是堆上,都能正确的访问该变量】
如果对于某一个使用了__block变量block已经复制到堆区,那么这个__block变量同样是已经被复制到堆区的(这个变量是block结构体的成员变量),这个_forwarding指针指向的就是堆区的变量配置。如果对于一个未复制到堆区的blcok,那么栈中仍然存在那个被复制的__block变量的配置,这个_forwarding指针指向的就是栈区的变量配置
Block赋值到属性上分为两种情况:
MRC:属性用copy修饰,赋值的时候实际是将赋值的内容copy到堆区中。首次持有一个Block对象时,(实际上就是将其从栈中复制到堆上的时候)对于配置到栈上的Block调用retain并不起到任何作用。所以通过copy将其复制到堆区并且持有它
ARC:属性用strong,copy修饰都可以,copy同MRC,Strong修饰的时候,在内存上的retain处理,实际上调用的是objc_retainBlock,而不是像其他变量一样调用的objc_retain,而objc_retainBlock里边调用的是_Block_copy这个函数,_Block_copy与copy达到的效果是一样的,所以当你给以strong修饰的Block类型的属性赋值时,系统自动将其复制到了堆区。
5.没有加__block能否能在block中修改它的值。
先问,这里的它是什么?
①:全局变量 、 静态全局变量 和 静态的局部变量,不需要加__block就可以修改。
非静态的局部变量,在block修改的时候会编译报错。
通过clang转换后,全局变量和静态全局变量转换前后相同,仍然是全局变量和静态全局变量,全局变量和静态全局变量是全局的,作用域很广,Block结束之后,它们的值依旧可以得以保存下来。修改它的值自然不会有问题。
但是在block中对静态局部变量做修改的时候,block结构体如下
与非静态变量不同的是,是将静态局部变量的指针作为成员变量添加到【命名规则】结构体中,而非这个局部变量。在表达式中也是通过静态局部变量的指针对其进行访问。这里存储的是used指针(int*类型),而不是存储的*used 这个int类型的值,修改的时候修改的是*used 而不是used;
静态局部变量存放在内存的全局数据区。函数结束时,静态局部变量不会消失,每次该函数调用 时,也不会为其重新分配空间。它始终驻留在全局数据区,直到程序运行结束;
② 非静态局部变量
没有用__block修饰的变量,不可以在block中直接赋值,但是如果如果在block通过调用addObject,这类变更对象的方法却不会产生编译错误,并且能正确运行。
直接赋值的话会编译报错。
原因:该代码中截获的变量array是一个NSMutableArray类的对象,在C语言中就是一个NSMutableArray类对象用的结构体的实例指针,对这个实例指针进行赋值的时候会编译报错,但是对这个实例的值修改却不会有问题。和修改静态局部变量有些相似。
为什么不可以直接赋值?
从clang后的代码中可以看到注释bound by copy,虽然非静态局部变量/对象被捕获,但是使用的时候是用__cself->arr/__cself->used来访问的,访问的是block【命名规则】结构体中的值,并不是外部自动变量的值,而在复制的时候,我们并没有复制非静态局部变量/对象的地址指针,所以赋值的时候,因为找不到非静态局部变量/对象的内存地址,所以无法对其修改。
例如上边,对静态局部变量修改,是因为copy的是静态局部变量的指针地址,修改的时候用取内容符号去修改。但是普通的局部变量却不行。
再比如对象,【命名规则】结构体中的声明是NSMutableArray * arr 意思是copy的是一个指向NSMutableArray类型的指针,所以你对他里边的值可以进行修改,因为你能找到里边的值的内存,但是你直接对arr赋值不行,是因为不知道,这个arr(NSMutableArray类型的指针)的地址。
6.说说block的结构体
①:无参数的匿名函数结构 不截获局部变量(自动变量)
当使用Block语法时,会生成一个结构体,这个结构体的名称的命名规则为:_<Block语法所属的函数名>_block_impl_<该block语法在该函数出现的顺序值>,以此来给经clang变换的函数命名。
如下边例子中的__testBlock_block_impl_0就是如此。这个结果中包含两个成员变量分别是一个impl结构体实例和一个Desc结构体指针,以及一个构造函数。
impl是一个__block_impl类型的结构体,稍后着重说下isa和FuncPtr;
Desc 是一个__testBlock_block_desc_0类型的结构体指针。
首先说下__block_impl结构体,如下图所示,它有两个成员变量,在构造函数中我们发现,funcPtr是函数指针,就是那个匿名函数的指针。isa其实就是一个指向_NSConcreteStackBlock类的指针,这说明了block实质上就是一个OC类的对象。其他两个成员变量是某些标志和以后扩充升级的预留成员变量。
再说下__testBlock_block_desc_0结构体,他有两个成员变量,并且是以__testBlock_block_impl_0结构体实例的大小进行初始化的。
②:无参数的匿名函数结构 截获局部变量(自动变量)(int)
可以看到表达式中使用的自动变量被作为成员变量追加到了【 命名规则 】的结构体中,并且成员变量类型与自动变量类型一样,而未使用的自动变量并没有加入其中。
当调用【命名规则】的结构体的构造函数时,根据传给构造函数的参数对由自动变量追加的成员变量进行初始化。,在testBlock()函数中可以看到,在执行block语法的时候,使用了自动变量来调用了【命名规则】的结构体初始化方法,构造了实例。
匿名函数的实现中,可以看到,使用的同名变量实际上是被保存到【命名规则】结构体中的成员变量,而非外部的局部变量,所以当外部局部变量,在执行block语法的之后如果有所改变,然后在调用了这个block(),表达式里边的值并不会随之变化。
③:无参数的匿名函数结构 截获对象 (NSArray)与截获变量时有什么不同?
在block语法中使用了截获的被__strong修饰外部对象或者外部变量时(arc下不写等于默认用__strong修饰),会多出来两个函数__main_block_copy_0和__main_block_dispose_0,后边的0是跟【命名规则】结构体相对应的,1对1的关系,有多个在Block语法中使用到的变量或者对象,都在和Block对应两个函数中进行操作,__main_block_desc_0结构体中也会多出来指向这两个函数的函数指针,这也说明了上边的对应关系。
copy函数的作用相当于retain,调用时机是栈上的Block复制到堆时。
dispose函数的作用相当于release,调用时机是堆上的Block被废弃时。
8/*BLOCK_FIELD_IS_BYREF*/这个标志代表处理的是变量
3/*BLOCK_FIELD_IS_OBJECT*/这个标志代表处理的是对象
7.一个myblock() 执行的时候是如何调用了block中的函数指针的,或者说Block如何使用的。
这里实际上就是再问block的实质! 接着之前说的结构体往下说,这里的myBlock就是一个指向那个通过那个命名规则生成的结构体的实例的指针,取内容符号获取myblock实例,然后获取impl结构体中的函数指针 (*myblock->impl.FuncPtr)(myblock);通过指针调用的方式调用了这个匿名函数。
8.__block修饰的变量生成了一个怎样的结构体?__block到底做了什么?
__block 叫做存储域类说明符。
C语言的存储域类说明符有 :typedef、extern、static、auto、register。
当对一个局部变量用__block修饰符修饰后,在block表达式内对其进行修改,结构体有怎样的变化?
block内部没有用这个被__block修饰的变量也会生成那个结构体。
① . __block先生成了一个__Block_byref_used_0结构体,这个__Block_byref_used_0结构体中包含着一个同这个自动变量同名同类型的成员变量。并且这个成员变量的初始值和__block修饰的变量的值相同。等于是这个新的__Block_byref_used_0结构体中有一个相当于原自动变量的成员变量。
②.如果在表达式中使用了这个__block修饰的自动变量,那么再【命名规则】结构体中就会多一个上述新创建的__Block_byref_used_0结构体实例,并且与自动变量同名。不使用则没有。
③.在赋值的时候,used = 10,实际上是获取【命名规则】结构体中这个__block变量的__Block_byref_used_0结构体实例的指针。__Block_byref_used_0结构体实例的成员变量有一个__forwarding指针,持有的是指向该实例自身的指针(作用见上图)。通过成员变量__forwarding访问那个同名的成员变量进行赋值。
9.block的循环引用,举例说明下(完)
一个类对象有一个Block类型的成员变量的强引用,即类对象持有Block。
当给这个强引用的Block成员变量赋值Block语法时 _blk = ^(){ 使用了self };
Block语法中使用了init方法中生成的附有__strong修饰符的id类型的变量self。会造成循环引用。
或者block语法中使用了self持有的强引用的成员变量时其实根本上也是持有了self也会造成循环引用。
如何解决? 三种方法
__block __weak __unsafe_unretained
区别: 最常用的是__weak ,最少用的是__unsafe_unretained,一般用不到,主要是__block与__weak的区别。
优点:
__block可以控制对象的持有时间。
在执行block()的时候可以动态决定是否将nil或者其他对象赋值给__block修饰的变量。
缺点:
但是为了避免循环使用,必须执行block(),不执行的话仍然会造成循环引用。必须执行的原因是block的表达式中必须执行对__block变量的赋值操作。不赋值,执行了也不解决问题。
在ARC下,__block 修饰的变量,这个变量前边一般不加修饰符,其实自动加的就是strong修饰符,对于Block结构体来说,就是持有了这个__block修饰的变量。所以不将其置为nil是不行的。(有人说直接不用修饰符直接置nil,那就是忘了,不能直接赋值的事儿了。)
在MRC的情况,因为Block不会持有__block修饰的变量或者id类型的对象。那么再Block中是否对__block赋值也就无所谓了。
block的一个面试题
block中直接对testObjc1赋值的话是会报错的,但是对他里边的内容进行赋值是没有问题的。
block 为什么能够捕获外界变量? __block做了什么事?
研究Block的捕获外部变量就要除去函数参数这一项,下面一一根据这4种变量类型的捕获情况进行分析。
自动变量
静态变量
静态全局变量
全局变量
首先全局变量global_i和静态全局变量static_global_j的值增加,以及它们被Block捕获进去,这一点很好理解,因为是全局的,作用域很广,所以Block捕获了它们进去之后,在Block里面进行++操作,Block结束之后,它们的值依旧可以得以保存下来。
在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。
这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量,等等,这些变量在Block里面并不会被使用到。那么这些变量并不会被Block捕获进来,也就是说并不会在构造函数里面传入它们的值。
Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。
__weak为什么能解决循环引用?为什么block里有的地方又要用__strong?
先简单说下block的循环引用产生原因
block被self持有,当block中捕获到self的时候,block内部实际上是在生成的一个定义block的结构体中强引用了self。从而造成了循环引用。
当通过__weak typeof(self) weakSelf = self; 在block使用weakSelf来试图解决循环引用的时候,block结构体中实际上是引用了__weak 标示的weakself
说白了,block的结构体中,捕获self 和 weakSelf,而生成的成员变量一个是 __strong self,一个是__weak weakSelf。接下来其实就和代理那个weak的使用一样了。当weakSelf指向的内存被释放的时候,weakSelf被自动置为了nil,从而解决了循环引用。
那么__strong typeof(weakSelf) strongSelf = weakSelf; 是解决什么问题的呢?
有人看到这里,肯定会有疑惑,这里strongSelf的引用不又像前面的self一样导致了循环引用了吗?这里需要好好解释一下:
self是一个指向实例对象的指针,它的生命周期至少是伴随着当前的实例对象的,所以一旦它和对象之间有循环引用是无法被自动打破的;strongSelf是block内部的一个局部变量,变量的作用域仅限于局部代码,而程序一旦跳出作用域,strongSelf就会被释放,这个临时产生的“循环引用”就会被自动打破,代码的执行事实上也是这样子的。
strongSelf主要解决的问题,是在block中,使用延迟调用,多线程的时候,如果还使用weakSelf,会被释放。而使用stringSelf的时候,它的作用域是整个的block,所以起到了一个延迟释放的作用。