iOS底层分析-block(二)

645 阅读4分钟

这是我参与8月更文挑战的第29天,活动详情查看: 8月更文挑战

在之前的文章中,我们分析了block的内存管理以及堆栈情况;今天我们来分析一下block究竟是一个什么样的结构呢;

clang分析block底层结构

没有__block修饰变量

说到分析结构,那么我们第一反应就是使用clang或者xcrun命令来分析cpp文件;

为了避免其他函数的干扰,我们创建一个block.c的文件,代码如下:

进入文件目录,使用xcrun命令生成cpp文件:

xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc block.c -o block.cpp

接下来,打开cpp文件,查看main函数,其实现如下:

我们将代码中强转类型相关的代码去掉,对cpp文件代码进行优化,如下:

那么__main_block_impl_0是什么呢?其实现也可以在cpp文件中找到:

可以看到__main_block_impl_0是一个结构体;根据代码我们可以分析出是将block指向了__main_block_impl_0结构体的构造函数;

a(_a)是一个c++的语法,我们传入的参数_a会对成员变量a进行赋值,将_a传给a

这时我们发现,这个结构体中有一个a,这个a是否是我们外部的参数a呢?我们将main函数中的代码修改如下:

然后再次生成cpp文件,比较前后两个cpp文件中__main_block_impl_0有什么区别:

这也就说明,我们传进来的参数a,被block捕获之后,会在block的底层生成一个成员变量;对象类型同样适用,也会生成成员变量;

再分析__main_block_impl_0我们发现这样一个细节impl.isa = &_NSConcreteStackBlock;implisa指向了一个_NSConcreteStackBlock;

按照我们之前讲的,如果一个block捕获到外界变量,并且没有被弱引用,那么他应该是一个堆block;而此时编译器生成的却是一个栈block

那么是不是编译时生成的是栈block,到运行时才会变为堆block呢?

我们来看一下__main_block_impl_0的构造函数,它的第一个参数fp,那么fp是什么时候被调用的呢?

首先我们要确定fp是什么?我们可以从cpp文件的main函数实现中,可以分析出fp指的就是__main_block_func_0,那么__main_block_func_0又是什么呢?

__main_block_func_0也是一个函数,也就是fp传递的是一个函数;而impl.FuncPtr = fp;fp又被赋值给了FuncPtr;而FuncPtr是在main函数中被调用的;

block在底层是一个fp的函数式保存;所以一个block被定义之后,如果没有被调用,那么功能逻辑代码永远不会被执行;

我们在函数__main_block_func_0中发现a来自于__cself->a(值拷贝),那么传进来的__cself又是是什么呢?

根据上下文调用关系我们可以分析出__cself就是block自身;__main_block_impl_0中的a__main_block_func_0中的a值相同,地址不同;

__block修饰变量

我们在开发过程中,经常会有这样的场景,我们需要在block中修改a的值,那么我们就需要使用__block来修饰a,这样我们才能在block内部修改a,那么__block做了什么事情导致我们可以对外部变量进行修改了呢?

接下来,我们将main函数修改如下:

然后生成其cpp文件;我们先来看一下__main_block_impl_0结构体有什么变化:

可以看到我们之前的int a,因为使用了__block之后,变成了__Block_byref_a_0 *a;而在main函数中多了一行代码:__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 5};,很明显,这是一个结构体的初始化操作;其代码优化之后如下:

接下来我们来看一下__Block_byref_a_0:

  • __forwarding就是a;

然后在__main_block_impl_0的构造函数中将结构体__Block_byref_a_0进入进去

然后a(_a->__forwarding)__Block_byref_a_0结构体中的__forwarding传给了__main_block_impl_0的成员变量__Block_byref_a_0 *a;

那么此时函数__main_block_func_0中的a和我们定义的a = 5a 是相同的,他们都指向了同一片内存空间(传递的是地址,指针赋值);

正因为都指向了同一片内存空间,所以在block内部才能修改外部变量;

__block生成了一个结构体__Block_byref_a_0,并且传给block其指针地址,从而达到能够修改同一片内存空间的目的;

分析到这里,我们基本上了解了block在底层的数据结构,但是我们中间产生的疑问:是不是编译时生成的是栈block,到运行时才会变为堆block呢?依然没有解决;请听下回讲解......