美文网首页
9 Block详解

9 Block详解

作者: 哈库呐玛塔塔__ | 来源:发表于2020-05-13 02:43 被阅读0次

    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,所以起到了一个延迟释放的作用。

    相关文章

      网友评论

          本文标题:9 Block详解

          本文链接:https://www.haomeiwen.com/subject/ouawnhtx.html