ObjC block简析(一)

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

探究block的本质

Block
Block

在main.m的main函数中声明一个block并执行block()通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp命令将main.m转为main.cpp。

cpp文件中的main函数
cpp文件中的main函数

在main.cpp中可以发现上图中的代码。其中在main函数中的block和block(),被转化为

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

其中不乏强制转换类型的代码,将强制类型转换的代码去掉我们可以明确的看出:

void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
        block->FuncPtr(block);

block存储了__main_block_impl_0函数的地址,而此函数中传递的第一个值是一个函数指针,函数的作用就是block中存储的代码的作用,即打印hello world。第二个参数是一个结构体指针,其中包含有对block的描述。

__main_block_impl_0是一个结构体,__main_block_impl_0函数是结构体__main_block_impl_0的构造函数,在此结构中就可以看到这个与结构体名字相同的没有返回值的函数。

该函数将类型地址赋值给isa,将存放block中代码的函数的指针赋值给funcptr,将block的描述赋值给desc。

__main_block_impl_0结构体中第一个成员就是:

__block_impl结构体
__block_impl结构体

我们可以看到这里并不是结构体指针,而是结构体本身,所以__main_block_impl_0的第一个成员其实是isa.

而我们的block()转换成了block->FuncPtr(block);就是通过block查找到FuncPtr然后调用,执行相应的代码。至于block明明是__main_block_impl_0类型的为什么却能直接查找到FuncPtr成员,上面已经说明这里存放的并不是结构体指针,也不是地址,而是结构体impl本身,所以block是可以经过类型强制转换转换成__block_impl类型进而通过FuncPtr指针调用相应的函数的。

我们都知道,ObjC中的对象都有一个isa指针,而block中同样存在isa指针。所以可以说block是一个ObjC指针。它封装了block函数的调用。

Block的变量捕获

像上述例子对我们来说通常没有什么实际作用,而我们通常要在block中处理一下外部变量的逻辑,那么block是怎么处理这些变量的呢?

auto局部变量的捕获

访问auto变量
访问auto变量

出现上述情况的原因是什么呢?依然将main.m转成main.cpp。

main.cpp
main.cpp

上面图片中我们可以发现block的结构体中出现了一个age变量,并且其构造函数表明将_age赋值给age成员。而在FuncPtr存储的函数__main_block_func_0的地址中,我们发现调用此函数是从block的结构体中取出age成员的值进行打印的。所以当定义block的时候age的值已经被block用一个同样名字的成员捕获了。而且捕获的仅仅是age的值。

static局部变量的捕获

main.cpp
main.cpp

通过上图我们可以看出block中存储的是a的地址,所以当FuncPtr指向的函数调用的时候会通过取a地址中存储的值,所就出现下面这种情况了。

static类型的变量捕获
static类型的变量捕获

全局变量的捕获

全局变量
全局变量

我们可以看出全局变量并没有被block所捕获,因为全局变量存放在全局区,随时都可以访问,所以当FuncPtr指向的函数调用的时候就会直接取a和b的值用。而局部变量超过作用域就会自动回收所以block需要在自身存放一份,以保证其能准确访问。

总结

局部变量在block中使用的时候会被block捕获,auto变量是值捕获,而static变量是地址捕获。全部变量不会被捕获。

block类型

既然上面说block是一个oc对象,那么他也应该是有类型的。那么block的类型有什么有趣的事呢?

由于在ARC环境下编译器默默对block做了一些我们看不见的工作,所以我们将xcode的arc模式关掉,以便于窥探到本质。

关闭arc
关闭arc

类型
类型

结果
结果

通过上述结果我们可以看出当block访问了auto变量的时候会变成__NSStackBlock__类型。而其他情况下是__NSGlobalBlock__类型。

__NSGlobalBlock__类型存在于数据区,__NSStackBlock__存在于栈区。

在ARC环境下打印的结果:

ARC下的block类型
ARC下的block类型

而在ARC环境下原本__NSGlobalBlock__的block依然是__NSGlobalBlock__类型,而原本是__NSStackBlock__却变成了__NSMallocBlock__存放在堆区。这是因为当我们定义block的时候ARC默认为我们做了一次copy操作。

下面是block的类型以及在内存中存放的区域:

block的类型
block的类型

我们尝试在MRC下对__NSGlobalBlock____NSMallocBlock__类型的block进行copy操作并打印结果:

copy操作
copy操作

block的类型(MRC环境)

访问了auto变量的block是__NSStackBlock__类型,没有访问auto变量的block是__NSGlobalBlock__类型。而对__NSStackBlock__类型进行copy操作就会变为__NSMallocBlock__类型。

对三种类型的block分别进行copy操作结果如下:

对三种类型的block进行copy操作
对三种类型的block进行copy操作

[往深处看-Block(二)](

Comments

添加新评论