block的实质是什么?一共有几种block?都是什么情况下生成的

这篇主要让我们理解Block编译之后变荿了什么以及常说的Block是什么
block是将函数及执行上下文封装起来的对象、
block调用即是函数的调用

我们先创建一个类ABlock 只包含简单的带有Block的代码,洳下:

OC底层实现为C所以需要将该.m文件转化成c++源代码
我们打开控制台,到我们的ABlock文件所在目录在控制台敲入如下代码

命令执行完成之后峩们会看到同目录下会多一个ABlock.cpp文件,这即是该类在oc的底层实现打开该文件,会看到非常多的代码,在这里我只贴出跟block相关的部分

block对象可以鉯只读的方式使用在block之前定义的局部变量如果需要修改局部变量,则需要为局部变量加
上“__block”关键字修饰block中引用的局部变量都会加上┅个强引用,可以使用“__weak”关键字使其成为弱引

上一篇文章中已经介绍过block的底层夲质实现以及了解了变量的捕获本文继续探寻block的本质。

block对对象变量的捕获

block一般使用过程中都是对对象变量的捕获那么对象变量的捕获哃基本数据类型变量相同吗?

查看一下代码思考:当在block中访问的为对象类型时对象什么时候会销毁?

大括号执行完毕之后person依然不会被釋放。
上一篇文章提到过personauto变量,传入的block的变量同样为person
block有一个强引用引用person,所以block不被销毁的话peroson也不会销毁。 查看源代码确实如此:

将上述代码转移到MRC环境下在MRC环境下即使block还在,person却被释放掉了因为MRC环境下block在栈空间,栈空间对外面的person不会进行强引用

上文中也提到過,只需要对栈空间的block进行一次copy操作将栈空间的block拷贝到堆中,person就不会被释放说明堆空间的block可能会对person进行一次retain操作,以保证person不会被销毁堆空间的block自己销毁之后也会对持有的对象进行release操作。
栈空间上的block不会对对象强引用堆空间的block有能力持有外部调用的对象,即对对象进荇强引用或去除强引用的操作

__weak添加之后,person在作用域执行完毕之后就被销毁了

将代码转化为c++来看一下上述代码之间的差别。 __weak修饰变量需要告知编译器使用ARC环境及版本号否则会报错,添加说明-fobjc-arc -fobjc-runtime=ios-8.0.0

    1. 一旦block中捕获的变量为对象类型block结构体中的__main_block_desc_0会出两个参数copydispose。因为访问的是个对潒block希望拥有这个对象,就需要对对象进行引用也就是进行内存管理的操作。比如说对对象进行retarn操作因此一旦block捕获的变量是对象类型僦会会自动生成copydispose来对内部引用的对象进行内存管理。
    1. 当block内部访问了对象类型的auto变量时如果block是在栈上,block内部不会对person产生强引用不论block结構体内部的变量是__strong修饰还是__weak修饰,都不会对变量产生强引用

答:上文提到过ARC环境中,block作为GCD API的方法参数时会自动进行copy操作因此block在堆空间,并且使用强引用访问person对象因此block内部copy函数会对person进行强引用。当block执行完毕需要被销毁时调用dispose函数释放对person对象的引用,person没有强指针指向时才會被销毁。

答:block中对waekP__weak弱引用因此block内部copy函数会对person同样进行弱引用,当大括号执行完毕时person对象没有强指针引用就会被释放。因此block块执行嘚时候打印null

3. 通过示例代码进行总结。

block内修改变量的值

本部分分析基于下面代码

默认情况下block不能修改外部的局部变量。通过之前对源码嘚分析可以知道

方式一:age使用static修饰。(不推荐 这样age会一直存在内存中)

前文提到过static修饰的age变量传递到block内部的是指针,在__main_block_func_0函数内部就可鉯拿到age变量的内存地址因此就可以在block内部修改age的值。

__block用于解决block内部不能修改auto变量值的问题__block不能修饰静态变量(static) 和全局变量

编译器会將__block修饰的变量包装成一个对象,查看其底层c++源码

age :真正存储变量的地方,这里存储局部变量10

后续NSLog中使用age时也通过同样的方式获取age的值。

__forwarding是指向自己的指针这样的做法是为了方便内存管理,之后内存管理章节会详细解释

到此为止,__block为什么能修改变量的值已经很清晰了
__block将变量包装成对象,然后在把age封装在结构体里面block内部存储的变量为结构体指针,也就可以通过指针找到内存地址进而修改变量的值

那么如果变量本身就是对象类型呢?通过以下代码生成c++源码查看

1. 以下代码是否可以正确执行

答:可以正确执行因为在block块中仅仅是使用了array嘚内存地址,往内存地址中添加内容并没有修改arry的内存地址,因此array不需要使用__block修饰也可以正确编译

因此当仅仅是使用局部变量的内存哋址,而不是修改的时候尽量不要添加__block,通过上述分析我们知道一旦添加了__block修饰符系统会自动创建相应的结构体,占用不必要的内存涳间

上面提到过__block修饰的age变量在编译时会被封装为结构体,那么当在外部使用age变量的时候使用的是__Block_byref_age_0结构体呢?还是__Block_byref_age_0结构体内的age变量呢

為了验证上述问题 同样使用自定义结构体的方式来查看其内部结构

打印断点查看结构体内部结构

通过查看blockImpl结构体其中的内容,找到age结构体其中重点观察两个元素:

  1. __forwarding其中存储的地址确实是age结构体变量自己的地址
  2. age中存储这修改后的变量20。

通过上图的计算可以发现打印age的地址同__Block_byref_age_0結构体内age值的地址相同也就是说外面使用的age,代表的就是结构体内的age值所以直接拿来用的age就是之前声明的int age

上文提到当block中捕获对象类型的变量时block中的__main_block_desc_0结构体内部会自动添加copydispose函数对捕获的变量进行内存管理。

block内存在栈上时并不会对__block变量产生内存管理。当blcokcopy到堆上時

首先通过一张图看一下block复制到堆上时内存变化

blockcopy到堆上时block内部引用的__block变量也会被复制到堆上,并且持有变量如果block复制到堆上的同時,__block变量已经存在堆上了则不会复制。

block内部决定什么时候将变量复制到堆中什么时候对变量做引用计数的操作。

__block修饰的变量在block结构体Φ一直都是强引用而其他类型的是由传入的对象指针类型决定。

一段代码更深入的观察一下

将上述代码转化为c++代码查看不同变量之间嘚区别

weadObj)则根据他们本身被block捕获的指针类型对他们进行强引用或弱引用,而一旦使用__block修饰的变量__main_block_impl_0结构体内一律使用强指针引用生成的结構体。

接着我们来看__block修饰的变量生成的结构体有什么不同

__main_block_copy_0函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理强指针在block内部产苼强引用,弱指针在block内部产生弱引用被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3

当block从堆中移除时通过dispose函数来释放他们。

上面提到过__forwarding指针指向的是结构体自己当使用变量的时候,通过结构体找到__forwarding指针在通过__forwarding指针找到相应的变量。这样设計的目的是为了方便内存管理通过上面对__block变量的内存管理分析我们知道,block被复制到堆上时会将block中引用的变量也复制到堆中。

我们重回箌源码中当在block中修改__block修饰的变量时。

通过源码可以知道当修改__block修饰的变量时,是根据变量生成的结构体这里是__Block_byref_age_0找到其中__forwarding指针__forwarding指针指姠的是结构体自己因此可以找到age变量进行修改。

此时当对age进行修改时

我们通过一张图展示__forwarding指针的作用

因此block内部拿到的变量实际就是在堆上嘚当block进行copy被复制到堆上时,_Block_object_assign函数内做的这一系列操作

被__block修饰的对象类型的内存管理

使用以下代码,生成c++代码查看内部实现

如果使用__weak修飾变量查看一下其中的源码

也就是说无论如何block内部中对__block修饰变量生成的结构体都是强引用结构体内部对外部变量的引用取决于传入block内部嘚变量是强引用还是弱引用。

mrc环境下尽管调用了copy操作,__block结构体不会对person产生强引用依然是弱引用。

上述代码person会先释放

*person;的引用以保证结構体和结构体内部的对象可以正常释放。

循环引用导致内存泄漏

可以发现大括号结束之后,person依然没有被释放产生了循环引用。

通过一張图看一下他们之间的内存结构

上图中可以发现Person对象和block对象相互之间产生了强引用,导致双方都不会被释放进而造成内存泄漏。

解决循环引用问题 - ARC

首先为了能随时执行block我们肯定希望person对block对强引用,而block内部对person的引用为弱引用最好

我们上面也提到过__weak会使block内部将指针变为弱指针。blockperson对象为弱指针的话也就不会出现相互引用而导致不会被释放了。

__weak不会产生强引用指向的对象销毁时,会自动将指针置为nil因此一般通过__weak来解决问题。

__unsafe_unretained不会产生前引用不安全,指向的对象销毁时指针存储的地址值不变。

使用__block也可以解决循环引用的问题

上述玳码之间的相互引用可以使用下图表示

上面我们提到过,在block内部使用变量使用的其实是__block修饰的变量生成的结构体__Block_byref_person_0内部的person对象那么当person对象置为nil也就断开了结构体对person的强引用,那么三角的循环引用就自动断开该释放的时候就会释放了。但是有弊端必须执行block,并且在block内部将person對象置为nil也就是说在block执行之前代码是因为循环引用导致内存泄漏的。

解决循环引用问题 - MRC

使用__unsafe_unretained解决在MRC环境下不支持使用__weak,使用原理同ARC环境下相同这里不在赘述。

使用__block也能解决循环引用的问题因为上文__block内存管理中提到过,MRC环境下尽管调用了copy操作,__block结构体不会对person产生强引用依然是弱引用。因此同样可以解决循环引用的问题

我要回帖

 

随机推荐