ObjC block简析(二)

请注意,本文编写于 298 天前,最后修改于 191 天前,其中某些信息可能已经过时。

往深处看-block(一)

block的copy

往深处看-block(一)中我们已经探究过在MRC环境下Block的三种类型以及其关系。

我们发现有些情况下ARC和MRC环境下的block是不同的,这是因为ARC帮我们做了一些我们在OC层面看不见的事情。比如ARC下某些情况编译器会自动对block进行一次copy操作,将原本存在栈上的block拷贝到堆空间上。

block作为返回值

block作为函数返回值
block作为函数返回值

上图中执行的环境是ARC,我们可以看出本是一个__NSStackBlock__,当它作为一个函数的返回值的时候它变成__NSMallocBlock__类型,上篇中已经指出,__NSStackBlock__经过copy操作就会变成__NSMallocBlock__类型,而这里并没有显式的调用copy,这便是ARC为我们做的。

block被强指针引用

block被强指针引用
block被强指针引用

block作为强指针引用的时候也会自动调用copy,将栈区的block拷贝一份到堆区。

其他情况

当block作为Cocoa API中方法名含有usingBlock的方法参数和block作为GCD API的方法参数时ARC都会自动为block执行一次copy操作,比如:

其他情况
其他情况

所以,为了保证block的释放正和时宜,而不是像在栈空间上的变量似的随时可能被释放。在MRC的时候声明一个block的时候通常使用copy关键字修饰,而到了ARC虽然不用我们显式的执行copy,为了提醒我们block是通过copy操作存在于堆空间的我们还是会用copy关键修饰,这只是一个习惯,其实用strong也是可以的。

对象类型的捕获

往深处看-block(一)中简述了block对基本数据变量的捕获,那么对于auto对象block是怎么捕获的呢?会不会对auto对象产生相应的引用呢?

栈上的block捕获auto对象

在MRC下声明一个属性Person,并在Block中访问其属性age,观察Person什么时候被释放:

在栈上的block
在栈上的block

我们看到Person在block的大括号结束之后,还没有调用block0的时候Person对象就已经释放了,说明在栈上的block并没有对Person有强引用。

对block0的值执行一次copy操作,让block0持有该block,然后执行上述代码,发现在block没有release之前,person对象没有被释放,所以堆上的block强引用了额person对象,person执行一次release之后,person的引用计数依然没有成为0,因为,block还引用着它。这是因为当对block进行copy操作的时候,block会执行内部的__main_block_copy_0方法。__main_block_copy_0方法执行_Block_object_assign根据变量的修饰符判断对捕获的对象的引用情况(retain或者弱引用)。

而当block从堆中移除的时候,会调用与__main_block_copy_0对应的_Block_object_dispose函数,该函数会自动释放引用的auto变量。

捕获对象类型
捕获对象类型

关注上述源码中框出的部分,对比基本数据类型的变量捕获,我们发现在Desc结构体多了上面所说的copy和dispose函数指针,它们的作用和调用时机也在上面描述了。

__weak

由于MRC情况下并没有弱引用也就没有__weak这个关键字,所以在ARC下探究block对对象的引用情况。

依旧是上述代码,将他改成ARC环境:去掉block的copy,对象的release,以及Person的dealloc方法中对父类dealloc方法的调用。

而对对象的强弱引用是在runtime的时候进行判定的。所以在将.m转成.cpp的时候需要声明一下runtime环境,使用该命令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

block中的person实例被__strong修饰
block中的person实例被__strong修饰

上面图片中 block通过_Block_object_assign判断修饰person的关键字为__strong,对person进行强引用。

而使用__weak修饰person的时候会产生弱引用:

block对person弱引用
block对person弱引用

__Block

当我们在block内部尝试修改auto变量的时候会发现出现一个错误:Variable is not assignable (missing __block type specifier)

block内部更改auto变量
block内部更改auto变量

编译器提示我们使用__block修饰auto成员变量,才能进行修改。

__block的本质

利用上面提到的命令将代码转换成.cpp查看实现,发现出现了一个新的包含有isa指针和auto变量同名成员的结构体。它的每个成员的值如下图:

2018122115453787929122.png
2018122115453787929122.png

当我们修改a这个值得时候会通过结构体找到__forwarding这个指针,通过赋值我们发现这个指针指向其自身,然后在通过__forwarding指向的结构体查找到a这个成员,改掉a这个成员的值。那么auto变量a与生成的对象a以及对象的成员a之间是什么关系呢?

我们尝试模拟block底层的转换窥探其关系:

模拟block底层的转换
模拟block底层的转换

获取地址值:

打印结果
打印结果

__forwarding指向的是结构体本身,而结构体内存储的a的地址跟auto变量a的地址是一致的。block内部存储的为a重新生成的对象a与auto变量本身是不同的。

__block对象的内存管理

既然block为auto变量a生成了一个新的对象,那么这个对象在内存中是如何管理的呢?

它跟对象类型的变量一样,当block存在于栈上的时候都不会对他们产生强引用,而当block从栈上copy到堆上的时候,都会调用__main_block_copy_0,通过_Block_object_assign函数判断引用关系,而该函数的最后一个参数表明了是被__block修饰auto变量还是对象类型的auto变量。

判定引用关系
判定引用关系

当block从堆上移除的时候都会调用__main_block_dispose_0函数,并传入上述数字所代表的含义。

循环引用

循环引用
循环引用

上述代码person引用了block,而block内部又对person进行了引用,所以此时person的引用计数为2。当person的作用域结束的时候,person的引用计数减1,而block并没有被执行,所以person的引用计数依然是1,不能被回收。为了使person的内存能够正确的被回收,这个循环中必须有一个引用是弱引用才能保证对象被安全的释放。我们通常可以使用__weak或者__unsaft_unretained修饰被访问的对象。这两个关键字的含义都是声明一个不会被强引用的对象,所以block对person的引用是弱引用,所以当person的被release一次之后,就没有其他的引用了,person的引用计数就会为0,系统就会回收person的内存,完成内存释放。

使用__weak 或者 __unsaft_unretained解决循环引用
使用__weak 或者 __unsaft_unretained解决循环引用

除了上述的两种方法,还可以在block使用变量之后对变量进行置nil操作。但是这中做法必须对block进行一次调用,否则,block永远都不会执行置nil的操作,block对person的引用也会一直存在。由于在block内部对person进行了修改,所以使用__block关键字对person进行修饰。而使用__block修饰之后,block又会在内部生成一个person对象,这样又会多了一次引用,与上面的情况不同。

两种引起循环引用的情况
两种引起循环引用的情况

在block内部将person置为nil就是打破了__block引用person这条线,然后引用计数person的引用计数减1,知道person的引用计数为0的时候block释放,关于person的内存管理就完成了。

__block解决循环引用
__block解决循环引用

这种做法中重要的一点是必须调用一次block,才能使block中引用person被释放。

总结

1.block是一个封装了函数调用和函数调用环境的OC对象(因为其结构体的第一个成员是isa指针)。

2.block对auto变量的捕获是值传递,static变量是指针传递,全局变量不会进行捕获。

3.block有三种类型__NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__ 访问了外部变量的block存在于栈上,经过一次copy操作会将栈上的block拷贝到堆上,即:__NSStackBlock__变为__NSMallocBlock____NSGlobalBlock__copy什么也不做,__NSMallocBlock__引用计数加1。

4.mrc下不会自动进行copy,arc环境下在block作为返回值,被强指针引用,在cocoa api中作为usingblock的参数,作为gcd api的参数的时候回自动进行一次copy操作。

5.栈上block访问了对象类型的auto变量的时候不会对其发生强引用。

6.block从栈上copy到堆上的时候,block内部会执行copy操作,_Block_object_assign函数回通过auto变量的修饰符判断发生强弱引用。

7.block从堆中移除的时候,block内部会执行dispose,将引用的对象进行释放。

8.__block修饰的值,会在block内部生成一个对象,对象中存放了__block的地址,当修改这个值得时候block内部会找到这个对象(结构体,因为其第一个成员是isa)的__forwarding(指向其自身)然后在找到这个值得地址,直接修改地址中的存放值。

9.__block生成的对象的强弱引用也是通过_Block_object_assign函数对__block产生的变量产生强引用。从堆中移除的时候调用dispose释放对__block生成的变量的引用。

10.使用__weak修饰在block中访问变量解决循环引用的问题。

Comments

添加新评论