iOS进阶将来跳槽用

iOS底层原理探索— block的本质(一)

2019-07-26  本文已影响16人  劳模007_Mars

探索底层原理,积累从点滴做起。大家好,我是Mars。

往期回顾

iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
iOS底层原理探索— 关联对象的本质

今天继续带领大家探索iOS之block的本质。

一、block底层结构分析

本文我们研究block的底层原理,如果你对block的基础掌握的不是很透彻的话,建议大家先去自行学习一些block的基础,然后再来阅读本文。

首先我们在main函数中声明一个block并完成调用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
           NSLog(@"Hello, World!");
        };
        block();
    }
    return 0;
}

然后我们通过clang命令将main.m转为c++文件,来帮助我们查看block内部结构:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

我们将生成的.cpp文件拖入工程中并取消编译,将OC代码跟转化完的c++代码做对比:

代码对比.png

通过对比我们发现,block的声明和调用都在c++文件一一对应显示。在c++代码中,(void (*)()表示强制转换,那么我们分析源码的时候为了便捷查看,我们将强制转换的代码暂时删掉:

简化后的c++代码.png

下面我们根据上图简化后的c++代码分别查看一下block的定义和调用的内部源码实现。

1.定义block

在定义block时调用了__main_block_impl_0函数,并将__main_block_impl_0函数的地址赋值给block。我们来看__main_block_impl_0函数内部结构:

__main_block_impl_0.png

我们看到,绿框标注的__main_block_imp_0结构体内有一个同名函数__main_block_imp_0,已用红框标注。这个同名的函数__main_block_imp_0构造函数构造函数中对一些变量进行了赋值,类似于OCinit函数,构造函数最终会返回一个结构体(此处对构造函数不做过多阐述,有疑问者可自行百度)。此处返回的结构体就是__main_block_imp_0

也就是说定义block时调用&__main_block_imp_0,最终会将__main_block_imp_0结构体的地址赋值给了block

我们继续分析红框标注的__main_block_imp_0函数。发现这个函数传入了三个参数,但是从上面简化后的c++代码中可以看到只传入了两个参数而已,这是为什么呢?

这就涉及到c++的语法,我们在上图中用黄色框标注的int flags=0,此处flags是直接赋值了,在c++语法中如果flage参数在调用的时候可以省略不传,也就是可以忽略这个参数。

接下来我们分析一下剩下的两个参数分别代表什么。

第一个参数:__main_block_func_0

我们直接查看__main_block_func_0源码:

参数__main_block_func_0.png
从源码中可以看到NSLog,那么我们可以分析出这个参数中封装的就是我们定义的block内部执行的逻辑。

也就是说把我们写在block中的代码封装成__main_block_func_0函数,并将__main_block_func_0函数的地址作为参数传到__main_block_impl_0的构造函数中保存在结构体内。

第二个参数:&__main_block_desc_0_DATA

参数__main_block_desc_0_DATA.png
从源码中看到__main_block_desc_0中存储着两个参数,reservedBlock_size,并且将0赋值给reservedBlock_size则存储着__main_block_impl_0的占用空间大小。

最终再把__main_block_desc_0结构体的地址作为第二个参数传入__main_block_func_0中。

我们在整体分析一下这段代码:

整体分析.png
图中,红框标注的为第一个参数及代表的内容,黄框标注的为第二个参数及代表的内容。我们看到,在构造函数__main_block_imp_0中,把第一个参数以fp赋值给impl.FuncPtr,把第二个参数以desc赋值给Desc

在构造函数__main_block_imp_0中还有两句赋值代码,其中impl.isa = &_NSConcreteStackBlock;这段代码含义是我们定义的block类型为_NSConcreteStackBlock。另外一句impl.Flags = flags;赋值代码,由于上文我们解释flags可忽略,所以这句代码我们也暂时忽略。

构造函数__main_block_imp_0赋值完毕之后,返回一个结构体,即__main_block_imp_0结构体,里面包含struct __block_impl impl;struct __main_block_desc_0* Desc;经过分析,impl中包含block的类型和封装的内部执行逻辑,Desc则包含block所占用内存空间大小。

我们在进入struct __block_impl impl;内部查看源码:

__block_impl内部结构.png
我们发现__block_impl结构体内部有一个isa指针。这一点说明

block本质上是一个oc对象

用一张图来总结以上分析:


关系逻辑.png

2.执行block

我们先看一下简化后的执行block的代码:

简化后的c++代码.png
调用block就是通过block找到FuncPtr直接调用。上文我们知道,FuncPtr是封装在__block_impl结构体中,而block指向的是__main_block_impl_0类型结构体,那它是如何找到FuncPtr的呢?我们来看一下没有简化的调用block的代码:
//调用block
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

可以看出,(__block_impl *)blockblock强制转换为__block_impl类型的,然后拿到FuncPtr。因为__block_impl__main_block_impl_0结构体的第一个成员,相当于将__block_impl结构体的成员直接拿出来放在__main_block_impl_0中,那么也就说明__block_impl的内存地址就是__main_block_impl_0结构体的内存地址开头。所以可以强制转换成功,也可以找到FunPtr

ok,至此我们理清了block的底层结构,也可以总结出:

block本质上是一个OC对象,其内部也有一个isa指针。
block就是封装了函数调用以及函数调用环境的OC对象

二、block的变量捕获机制

分析完block的底层结构,我们继续探索block的其他内容。
为了保证block内部能够正常访问外部的变量,block有一个变量捕获机制。下面我们定义block,通过其源码来分析block的变量捕获机制。

带参数的block.png
代码中,声明了局部变量a = 10b = 20,声明带两个int类型参数的blockblock内执行打印传入的参数和局部变量ab的值,然后调用block

我们发现在c++代码中,定义block时,后面多了ab两个参数。
然后我们进入__main_block_impl_0结构体查看具体内容:

捕获变量的__main_block_impl_0结构体.png
我们看到,__main_block_impl_0结构体中多了两个变量,即我们之前声明的ab两个变量,同时我们还看到在__main_block_impl_0构造函数中红色标注的代码a(_a), b(_b)。这也是c++的语法,代表了将传入的参数_a赋值给a,将传入的参数_b赋值给b。然后保存在__main_block_impl_0结构体中

然后我们进入__main_block_func_0函数中查看:

捕获变量的__main_block_func_0函数.png
通过分析代码我们可以得知,__main_block_func_0函数会从__main_block_impl_0结构体取出ab的值,完成NSLog打印输出。

通过以上分析,我们可以总结出:在block的底层结构体中,会将捕获的变量存放起来,保存变量的值。当block内部封装的执行函数需要调用变量时,会从结构体中取出变量的值,完成调用。

我们再用一张图片简单展示一下block的底层结构:

block内部结构示意图.png

下面我们继续研究如果block捕获不同变量会产生什么效果

变量捕获机制.png
代码中我们分别声明了局部变量ab以及全局变量cd,其中a为自动局部变量(auto修饰,int a就是声明了自动局部变量,auto可不写),b为静态局部变量。在定义完block后分别修改四个变量的值,然后调用block。通过打印发现,只有自动局部变量a的值没有变,其他三个变量的值均发生了改变,我们进入底层查看block对这四个变量都做了什么:
首先是main函数的代码:
变量捕获main代码.png
我们发现,在c++的代码中,定义block时自动局部变量a依旧是捕获其值作为参数,静态局部变量b则是捕获其地址作为参数传入,在看__main_block_impl_0结构体中:
变量捕获block结构体.png
自动局部变量a依旧是值存储,而静态局部变量b则是以指针形式存储。
在看一下调用执行封装函数的代码:
变量捕获block封装执行函数代码.png
自动局部变量a依旧是传递其值进行调用,而静态局部变量b则是传递其指针进行调用。

我们可以得出结论:

block对自动局部变量会进行捕获,访问形式是值传递
block对静态局部变量会进行捕获,访问形式是指针传递

原因很简单,因为自动变量随时可能会被销毁,block在执行的时候有可能自动变量已经被销毁了,那么此时如果再去访问被销毁的地址肯定会发生坏内存访问,因此对于自动变量一定是值传递而不可能是指针传递了。而静态局部变量不会被销毁,所以完全可以传递地址。
block调用之前静态局部变量的值发生改变,但静态局部变量的地址没有发生改变,对应block中的捕获的地址不变,所以静态局部变量会随之改变。对应b打印出的值就发生了改变。

同时我们从代码中发现,每段代码中都没有全局变量cd,也就是说block不会捕获全局变量,因为全局变量无论在哪里都可以访问。
我们可以得出结论:

block对全局变量不会进行捕获,访问形式是直接访问

捕获变量示意图.png

三、block的类型

1.block的三种类型

block有三种类型,都继承自NSBlock类型。我们可以通过class方法或者isa指针查看其具体类型。
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
下面通过代码来展示三种类型的block的具体使用场景以及block的继承关系:

block的三种类型.png
block的父类继承自NSBlock,而NSBlock继承自NSObject。这恰好也证明了我们之前说block是一个OC对象的结论。

block在内存中的存放区域

不同类型的block在内存中存放的区域也不同。
__NSGlobalBlock__类型的block存放在数据段中,程序结束就会被回收。不过我们很少使用到__NSGlobalBlock__类型的block,因为这样使用block并没有什么意义。
__NSStackBlock__类型的block存放在栈区中,栈区由系统自动分配和释放,作用域执行完毕之后就会被立即释放。
__NSMallocBlock__类型的block存放在堆区中,堆区需要我们自己进行内存管理。__NSMallocBlock__类型的block是我们平时经常使用的。

下面通过示意图总结一下:

block在内存中的区域.png
那么系统是如何定义这三种类型呢?我们继续分析:
系统如何定义block类型.png
我们看到,存放在栈区的__NSStackBlock__类型的block调用copy就会成为__NSMallocBlock__类型,并且被复制存放在堆中。但是我们上文在测试代码中block2并没有调用copy,那它为什么会是__NSMallocBlock__类型呢?

原因就是在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上。
我们从上面的图片中可以看到,访问了外部变量的block__NSStackBlock__类型,存储在栈中。在实际编码过程中,存在这一种情况就是我们声明一个全局的block,在某个函数中全局的block访问了局部变量,此时block存储在栈中。当函数执行完毕后,栈内存中block所占用的内存已经被系统回收,这时我们在函数外调用block时就会出现问题。
下面我们用代码来验证一下,首先要关闭ARC,回到MRC环境中:

关闭ARC环境.png
然后是代码验证:
没有调用copy的block.png
我们发现,打印的值是-272632616,说明就已经出现了问题。

那么其他类型的block调用copy操作会有什么效果呢:

不同类型的block调用copy.png

ARC在一下四种情况下会对block进行copy操作:

  1. block作为函数返回值时
  2. block赋值给__strong指针时
  3. Cocoa API中方法名含有usingBlock的方法参数时,例如遍历数组的enumerateObjectsUsingBlock方法
  4. GCD APIblock为方法参数时

至此我们掌握了block的底层结构、block的变量捕获机制以及block的类型,那么block中对象类型变量的捕获是什么样的呢?当在block中访问的变量为对象类型时,该变量什么时候才会被销毁?我们继续分析。

四、block中对象类型变量的捕获

首先我们写一段测试代码,实际查看一下。声明一个Person类,内部声明age属性,并且实现dealloc方法,方法内打印Person -- dealloc。然后我们创建一个block,并在block内部捕获personage属性,查看Person对象的dealloc方法调用时机。

对象类型变量ARC捕获.png
通过打印我们看到,大括号内创建对象和block内调用对象的代码执行完毕之后,person没有被释放。而是在block被销毁时,peroson才被释放。
这是因为personaotu变量,传入block后,ARC自动对block调用copy操作,将block放入堆内存中,block内部会有一个强引用引用person对象,所以block不被销毁的话,peroson对象也不会销毁。
我们可以通过MRC环境来验证一下这一点。我们关掉ARC环境测试一下: 对象变量捕获MRC.png
可以看到,在MRC环境下,即使block没有被释放,当Person对象调用release操作后,就会被释放。因为MRC环境下block在栈空间,栈空间对外面的person不会进行强引用。
如果我们对block进行copy操作后,Person对象依然是不会被释放的。

也就是堆空间的blockperson对象进行了强引用,以保证person对象不会被销毁。当block自己释放之后也会对持有的person对象进行release释放操作。

对象类型变量的捕获底层.png
通过源码确实可以证明堆空间的block对对象类型的变量进行强引用。
同时我们还发现block结构体中__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数:copy函数和dispose函数:
对象变量捕获__main_block_desc_0.png
经过分析,copy函数和dispose函数中传入的都是__main_block_impl_0结构体本身。其中:

copy函数

copy函数就是__main_block_copy_0函数,__main_block_copy_0函数内部调用_Block_object_assign函数,并传入是person对象的地址、person对象以及8三个参数。
block进行copy操作时,内部就会自动调用__main_block_desc_0内部的__main_block_copy_0函数,__main_block_copy_0函数内部又会调用_Block_object_assign函数。
_Block_object_assign函数会自动根据__main_block_impl_0结构体内部的person是什么类型的指针,对person对象产生强引用或者弱引用。可以理解为_Block_object_assign函数内部会对person进行引用计数器的操作,如果__main_block_impl_0结构体内person指针是__strong类型,则为强引用,引用计数+1,如果__main_block_impl_0结构体内person指针是__weak类型,则为弱引用,引用计数不变。

dispose函数

dispose函数就是__main_block_dispose_0函数,__main_block_dispose_0函数内部调用_Block_object_dispose函数,传入person对象以及8两个参数。
block从堆中移除时就会自动调用__main_block_desc_0中的__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数。
_Block_object_dispose会对person对象进行释放,相当于release操作,也就是放弃对person对象的引用,而person究竟是否被释放还是取决于person对象自己的引用计数。

当我们用__weak修饰block捕获的对象类型变量时,内部结构唯一的变化就是__main_block_impl_0结构体中对Person对象引用是__weak修饰的,也就是弱引用。其他代码没有变化。

那么我们就可以得出总结:

1.当block中捕获的变量为对象类型时,block底层结构体中的__main_block_desc_0会出两个参数copy函数和dispose函数,从而对内部引用的对象进行内存管理。

2.当block进行copy操作,拷贝到堆区后,copy函数会调用_Block_object_assign函数,根据变量的修饰符(__strong__weakunsafe_unretained)做出相应的操作,形成强引用或者弱引用

3.当block从堆中移除,dispose函数会调用_Block_object_dispose函数,自动释放引用的变量。

block的底层探索暂时告一段落,下篇文章会继续为大家分析block在使用过程中需要注意的问题。

更多技术知识请关注公众号
iOS进阶


iOS进阶.jpg
上一篇下一篇

猜你喜欢

热点阅读