关于Block,我之前已经写了三篇文章了,按照时间顺序分别为:
- Block以及Block的循环引用
- Block
- Block 的高级使用
今天这篇文章也是在前面这三篇文章的基础上,再结合自己最近的一些工作感悟,进行内容的完善。
Block的分类
在Block中,我介绍过Block是分为StackBlock、MallockBlock以及GlobalBlock三种,实际上这种说法是不准确的,Block其实是有六种。
代码语言:javascript复制/********************
NSBlock support
We allocate space and export a symbol to be used as the Class for the on-stack and malloc'ed copies until ObjC arrives on the scene. These data areas are set up by Foundation to link in as real classes post facto.
We keep these in a separate file so that we can include the runtime code in test subprojects but not include the data so that compiled code that sees the data in libSystem doesn't get confused by a second copy. Somehow these don't get unified in a common block.
**********************/
void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };
我们看到,Block有StackBlock、MallocBlock、GlobalBlock、AutoBlock、FinalizingBlock、WeakBlockVariable六种。其中最常见的是StackBlock、MallocBlock、GlobalBlock这三种,我在Block中已经有过介绍;AutoBlock、FinalizingBlock、WeakBlockVariable这三种Block是系统级别的Block,我们程序员一般用不到。
函数式编程 & 链式编程
使用Block可以实现链式编程的写法,函数式编程也可以通过Block来实现,具体可以参考Block 的高级使用
关于函数式编程,我已经写过3篇文章,如下:
- Block 的高级使用
- Swift进阶六——函数和闭包
- KVO详解(二)
解决循环引用的三种方式
我们知道,解决循环引用的最常规思路就是通过weak弱引用的方式,第二种思路是通过在合适的时机手动将其中一个对象置空销毁,这两种思路我们已经很熟悉了。
这里我主要是介绍第三种思路。
先来看个例子:
这里self持有了block,而block又持有了self,明显循环引用了,解决的方式之一就是使用weak-strong dance。现在我们不使用weak ,看看还有没有其他的方式呢?
我们来分析一下这里的场景:我在block内部是使用外界的某个变量,并没有操作外界的变量,只是来获取其中的值,并且外界的变量不会改变。因此我就想,这种场景下其实我将外界的变量拷贝一份进来就可以了啊,我没必要非得获得最原始的变量啊。
而block的参数是随着block一起拷贝到堆区的,所以我就将block内部使用的这个vc通过参数传递进来不就好了嘛。代码如下:
Block的本质
现在有一个C文件:block.c,内容如下:
代码语言:javascript复制#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("Lavie NB!");
};
block();
return 0;
}
我进入block.c文件所在的文件夹,终端执行如下clang命令,将block.c编译成C 文件:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk block.c
之后就会在相同路径下多出一个名为block.cpp的文件,打开该文件之后,找到对应的源码如下:
代码语言:javascript复制int main(){
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);
return 0;
}
将一些类型强转代码移除掉之后,
void(^block)(void) = ^{
printf("Lavie NB!");
};
就可以翻译成下面?
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
而__main_block_impl_0的定义如下:
代码语言:javascript复制struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,__main_block_impl_0是一个结构体。因此__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)就是结构体的构造函数,所以block本质上就是__main_block_impl_0结构体的实例对象。
因此我们在外界可以使用%@来打印一个block:
代码语言:javascript复制void(^block)(void) = ^{
printf("Lavie NB!");
};
NSLog(@"%@", block);//<__NSGlobalBlock__: 0x100b4f070>
我们平常在开发中,经常会说block是一种匿名函数,其实这种说法也没有错。
根据__main_block_impl_0的结构体定义我们可以看到,其构造函数传入的第一个参数就是对应的函数,然后通过impl.FuncPtr变量进行保存:
还可以看一下外界调用传入的时候的情况:
block的调用block();编译成C 之后如下:
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
将类型强转相关代码移除之后如下:
(block)->FuncPtr((__block_impl *)block);
可以看到,block的调用本质上就是找到__main_block_impl_0结构体构造函数中作为第一个参数传入的那个函数,然后调用该函数。
因此,block是一个匿名函数,这句话没错,也正因为如此,我们才需要去手动调用block,进而执行block内部的代码。
Block对外界局部变量的捕获
我们现在修改一下block.c文件中的源码,如下:
代码语言:javascript复制#include "stdio.h"
int main(){
int a = 6;
void(^block)(void) = ^{
printf("Lavie NB! --- %d", a);
};
block();
return 0;
}
然后clang编译成C 源码:
代码语言:javascript复制int main(){
int a = 6;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
简化之后如下:
代码语言:javascript复制int main(){
int a = 6;
void(*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
((block)->FuncPtr)(block);
return 0;
}
可以看到,__main_block_impl_0构造函数中,多传入了一个参数a。
再来看一下此时的__main_block_impl_0结构体:
可以看到,__main_block_impl_0结构体中生成了一个变量a,然后它记录了__main_block_impl_0构造函数中传递进来的a变量的值。
需要注意的是,内部的这个自动生成的a和block外界的那个局部变量a已经是两个指向不同的内存区域的变量了。因此,在block对外界局部变量的值捕获完了之后,你再在外界修改局部变量的值,对block内部的值不会有任何影响:
代码语言:javascript复制int a = 8;
void(^block)(void) = ^{
printf("Lavie NB! -- %d", a);
};
a = 666;
block(); // Lavie NB! -- 8
那么现在我想,既然在block外界的修改不会影响block内部,那么block内部的修改是否可以影响到block外部呢?为了验证,我尝试在block内部修改a值:
竟然报错了!!!提示缺少__block。
接下来我就来分析一下为什么会报这个错误。
通过上面的分析我们现在知道了,正常情况下,对于外界局部变量,block会在自身的__main_block_impl_0类型的结构体中创建一个对应的成员变量,用于接收捕获的这个外界局部变量的值,因此在block内外,已经是两个不同的变量了,在block内部修改这个变量,修改的是__main_block_impl_0类型的结构体中创建的这个成员变量的值,它不会影响到外界的局部变量。但是呢,在写法上给人的感觉就是我在block内部修改了外界局部变量的值,此时如果外界局部变量的值没有改变的话,就会给人造成疑惑。苹果为了杜绝这种代码歧义,所以它禁止直接在block内部直接修改外界局部变量的值。所以上面?会报错。
然后我按照提示加上了__block:
代码语言:javascript复制__block int a = 8;
void(^block)(void) = ^{
a = 666;
};
block();
printf("Lavie NB! -- %d", a);
再运行,正常运行,且打印结果为:
Lavie NB! -- 666
此时我就想,为什么加上了__block就不会报错,并且可以在block内部修改外界局部变量的值了呢?
我将加上__block之后的block.c文件编译成C :
代码语言:javascript复制int main(){
__attribute__((__blocks__(byref))) __Block_byref_a_0 a =
{
(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0), 8
};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
&__main_block_desc_0_DATA,
(__Block_byref_a_0 *)&a,
570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
printf("Lavie NB! -- %d", (a.__forwarding->a));
return 0;
}
我发现,加上__block之后,变量会在编译的时候被转换成一个__Block_byref_a_0类型的结构体,__Block_byref_a_0类型的定义如下:
代码语言:javascript复制struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
其中的第二个变量__forwarding指针指向的是局部变量原来的内存地址。
因此,在局部变量前加上__block之后,会生成__Block_byref_a_0类型的结构体,这个结构体里面保存了原始变量的指针以及原始值,然后将这个__Block_byref_a_0类型的结构体传进block的__main_block_impl_0类型的结构体的构造函数,并且在__main_block_impl_0类型的结构体内部保存起来。这就是所谓的block捕获了外界局部变量的指针。
所以,这也就解释了为什么加上__block之后就可以在block内部修改block外部的局部变量了。
StackBlock是如何变换到MallocBlock的
我们从汇编入手,一步一步跟一下Block底层做的事情。
在正式分析之前,先来看一个结构Block_layout:
代码语言:javascript复制struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke; // block中调用的函数
struct Block_descriptor_1 *descriptor; //
// imported variables
};
Block_layout中的第二个参数flags中包含了很多记录各种状态的内容:
代码语言:javascript复制// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
解释说明如下:
其中的第四个标记位BLOCK_HAS_COPY_DISPOSE,表示的是是否拥有拷贝辅助函数。当满足的时候,会在Block_layout结构体中加入BLOCK_DESCRIPTOR_2和BLOCK_DESCRIPTOR_3:
那么如何来获取到BLOCK_DESCRIPTOR_2和BLOCK_DESCRIPTOR_3呢?看下图:
可以看到,通过内存偏移来获取descriptor_2和descriptor_3的。
接下来我们开始看案例。
执行到断电后打开汇编分析:
我把断点断到objc_retainBlock,然后就进入到的下层:
我一步一步往下走,最后会来到_Block_copy:
然后我在lldb中读取此处的x0并打印:
发现这里的block是一个GlobalBlock。其实这也印证了一个观点:如果一个Block中没有使用任何的变量,那么这个Block就是一个GlobalBlock。
接下来我对原始代码做如下改动:
然后我们接下来研究Block的变换过程。这里是本文的重点哦!
运行到断点处,然后转为汇编分析:
在跳转到objc_retainBlock的地方打个断点,然后进入objc_retainBlock的下层:
此时我读取寄存器中的x0并打印,结果表明这时的block是一个StackBlock。
接下来我一步步往下走,没走几步就来到_Block_copy跳转的地方:
然后我打一个_Block_copy的符号断点:
就来到_Block_copy函数里面:
总览一下_Block_copy的汇编代码,好多啊,百十行,接下来该怎么分析呢?
我们在分析的时候,一定要把中心思想时刻记在脑海中,我们现在做这些分析的目的是什么?目的就是分析StackBlock转为MallocBlock的过程。好,既然这样的话,那么我就看一看最后它返回的是一个类型的Block。我把断点打在return返回值的地方,返回值是存储在寄存区的x0中的,所以我读取x0:
结果表明,此时的block已经是一个MallockBlock了。
Block中的函数的调用验证
通过前面的分析,我们知道Block内部的代码在底层会被封装成一个函数,然后在block回调的时候,会调用该函数。接下来我们就验证这一点。
还是上面的源代码,打断点,然后汇编分析,如下:
我在objc_retainBlock和objc_release调用的地方分别打了断点,它俩分别意味着block的声明和释放,所以block中的函数的调用势必是在二者之间,那么我就在二者之间打一个断点,?下图标红处:
这里需要着重说明一点,[x0, #0x10] 表示的是从x0这个位置向后偏移16个字节。0x10是16进制,转成10进制就是16。
那么为什么要从x0这个位置向后偏移16个字节呢?我们来看下block结构体的定义:
代码语言:javascript复制struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke; // block中调用的函数
struct Block_descriptor_1 *descriptor; //
// imported variables
};
isa指针占8个字节,flags和reserved都是int类型,占4个字节,他们仨一共占16个字节。而就是block中的那个函数,所以需要从首地址向后偏移16个字节。
因此,[x0, #0x10] 表示的是从x0这个位置向后偏移16个字节,也就是block所对应的那个函数。
当然这只是我的猜想,接下来我通过读取寄存器来验证一下:
结果表明,x8就是_block_invoke,也就是block的函数调用,这就验证了我上面的猜想。
接下来我自上面的第30行断点处进去,就进入了_block_invoke的下层:
完美~
block对象的签名
现在我们已经知道了,block本质上就是一个结构体的实例对象,所以Block也是有签名的。
那么Block的签名存放在哪里呢?实际上上面我已经聊到了。
Block的结构是Block_layout:
代码语言:javascript复制struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor; //
// imported variables
};
Block_layout的后面会有一些可选变量BLOCK_DESCRIPTOR_2和BLOCK_DESCRIPTOR_3,而block的签名信息signature就存放在Block_descriptor_3结构体中:
代码语言:javascript复制struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
还是老套路,来到汇编代码中:
首先读取到寄存器的x0,然后使用x/4gx打印出block的内存结构,第一段是isa指针,第二段是flags和reserved,由于是小端模式,所以flags排在后面(上图标红部分)。由于invoke是函数指针,所以占8个字节,所以第三段是invoke。因此从第四段开始就是descriptor、descriptor2、descriptor3相关的内容了。
我上面说标红的为flags,那是我自己一厢情愿猜测的,为确保正确,我还是验证一下吧。
通过上面lldb打印我们也知道,此时的block是GlobalBlock,而flags中有一个标志位是记录是否是globalBlock的(BLOCK_IS_GLOBAL = (1 << 28)):
代码语言:javascript复制// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
我将1左移28位之后,和flags(即0x50000000)进行与操作,结果是0x10000000,结果不为空,这意味着是globalBlock。这跟上面的打印结果是一致的,所以也就验证了上图的标红就是flags。
我们知道,排在descriptor后面的是descriptor2,所以先来看下descriptor2是否存在。descriptor2的结构如下:
代码语言:javascript复制struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
可以看到,Block_descriptor_2里面包含了copy和dispose,而flags中有一个标志位是记录是否有COPY_DISPOSE的(BLOCK_HAS_COPY_DISPOSE = (1 << 25))。
我接下来将1左移25位之后,和flags(即0x50000000)进行与操作,结果是0x0,这意味着结果是0,也就是说没有COPY_DISPOSE,也就是说不存在descriptor2。
而排在descriptor2后面的是descriptor3,descriptor3是否存在呢?我们来验证下。
descriptor3的结构如下:
代码语言:javascript复制struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
可以看到,descriptor3中包括签名signature和layout。
而flags中有一个标志位是记录是否有签名SIGNATURE (BLOCK_HAS_SIGNATURE = (1 << 30))。
我接下来将1左移30位之后,和flags(即0x50000000)进行与操作,结果是0x40000000,这意味着有签名,进而意味着有descriptor3。
现在回到汇编中,在lldb中根据地址打印出当前block的签名信息:
可以看到,当前block的签名是“v8@?0”。
v表示返回值是void
8表示占8个字节
@?表示什么意思呢?我们在lldb中打印一下看看:
可以看到,block的签名本质上就是@?,@表示这是一个对象,?表示这是一个block。
以上。