再探函数与Block

函数与block的区别

函数是我们在C语言中就接触到的概念,是指一段执行特定任务的代码,结构上包括返回值,函数名,参数,方法体等,在OC或C语言中它的定义形式为

void functionName(int param){
    
    return;
}

函数的指针的声明形式为

void (*functionName)(int)

而OC中还定义了一种封装了函数的对象类型,即Block 它的定义形式为:

    void (^blockName)(int) = ^void (int param){
        return;
    };

观察到block左侧的声明形式与函数十分相似,唯一的区别在于前者在变量名前用的是*指针字符,而后者使用特指block的 ^ 字符(脱字符)

而右侧的定义部分则有两处不同,一是block在返回值类型前使用了 ^ 字符,其次是block在定义中没有指定名称,因此它也被称为匿名函数

说了这么多废话,也只是在形式上区分了函数和block,那么究竟为什么要设计出这么一个东西呢,接下来就说说block的关键特性。

block是一种OC对象

如上所述,正因为block是一种对象,因此它可以在函数的内部定义,而block本身又是一个函数的封装,因此block中的函数是可以在函数内部嵌套实现的,这是一般函数做不到的,像这样:

void (^blk)(void);

void functionName(int param){
    int a = 1;
    blk = ^ (void){
        NSLog(a);
        return;
    };
    return;
}

我们在一个函数的内部实现了一个作为全局变量的block,并且在block的内部使用了其上下文中的一个变量a,函数则不能像这样实现。

聪明的你看到这里一定发现了一个问题,a是一个局部变量,而blk是一个全局变量,假如在函数的生命周期结束之后,也就是a变量被释放掉之后,在其他的地方再调用blk时,还能将a打印出来吗?答案当然是肯定的,不然block这种对象设计出来就没有意义了,因此这便是block的关键特性,**在block中使用的上下文中的局部变量,会被它捕获,以至于即使局部变量被释放,block中仍然可以使用当初捕获到的变量。 **

那么大家应该猜想到了,block很可能是将这些局部变量复制了一份,与原本的局部变量相独立。如此一来便又引发了两个疑问:

  • 既然是两个独立的数据,那么在blk外部对a的值进行修改,会影响到blk里面的值吗?
  • 在blk里面对a进行修改,又会影响到外面吗?

首先来验证第一个问题

void (^blk)(void);

void functionName(int param){
    int a = 1;
    blk = ^ (void){
        NSLog(@"%d", a);
        return;
    };
    a = 2;
    return;
}

functionName(0);
blk();

打印出来的值为1,因此对于第一问题的答案是,blk在捕获了局部变量后,这些捕获的值就不再受外部影响了。这个也比较符合我们的直觉,毕竟已经是两个独立的元素了嘛。

而尝试在blk内部修改a时,编译器则会提示一个error: “Variable is not assignable (missing __block type specifier)”。说变量缺少一个__block修饰符,也就是说默认情况下是不允许我们修改被捕获元素的,总之我们先将__block添加到a的声明前。

void (^blk)(void);

void functionName(int param){
    __block int a = 1;
    blk = ^ (void){
        a = 2;
        NSLog(@"%d", a);
        return;
    };
    blk();
    NSLog(@"%d", a);
    return;
}

functionName(0);

此次我们将blk放入函数中执行,发现打印出来的结果是2 2,即外部的a也随着内部的a一起改变了。

那这种情况下外部修改的变量值是否会影响到内部呢?也来验证看看

void (^blk)(void);

void functionName(int param){
    __block int a = 1;
    blk = ^ (void){
        NSLog(@"%d", a);
        return;
    };
    a = 2;
    NSLog(@"%d", a);
    return;
}

functionName(0);
blk();

发现结果依旧是2 2,证明在添加了__block修饰之后被block捕获的变量与外部的变量是互相影响的,那么这又是如何做到的呢,明明是两个独立的元素才对啊?

block的底层实现

实际上__block修饰符对block及它捕获的局部变量的影响远比表面上看起来大,在llvm中使用Clang -rewirte-objc 命令将.m文件转译为c++文件,可一窥block的真身。

将如下的main.m通过 Clang -rewirte-objc main.m转换成main.cpp

int main(int argc, char * argv[]) {
    void (^blockName)(int) = ^void (int param){
        return;
    };
    return 0;
}

得到了一个130多行的cpp文件,先找到最后的main函数部分:

int main(int argc, char * argv[]) {
    void (*blockName)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    return 0;
}

可以发现原本的block对象变成了一个函数指针类型,并且这个函数指针的内容是通过调用一个名为__main_block_impl_0的函数返回而来,它传递了两个参数,分别为__main_block_func_0和__main_block_desc_0_DATA。

我们一步一步看,首先是__main_block_impl_0函数,就在上方

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;
  }
};

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

发现其实是一个结构体的构造函数,也就说block的对象引用实质上是block结构体构造函数的一个指针

而构造函数的第一个参数__main_block_func_0则是

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int param) {

        return;
}

不难猜测这就是block封装的函数,只是多了一个__main_block_impl_0结构体类型的参数__cself,这个可以稍后再提。

这个函数被赋给了__block_impl结构体中的函数指针成员FuncPtr。

__block_impl中还有一个isa指针成员,其值为&_NSConcreteStackBlock,代表当前block的类,block还有另外两种类型,这个也是稍后再提。

而__main_block_desc_0则是一个保存了__main_block_impl_0所占空间的结构体。

接着我们在oc代码中添加一行block的执行

blockName(0);

转换后发现它变成了

((void (*)(__block_impl *, int))((__block_impl *)blockName)->FuncPtr)((__block_impl *)blockName, 0);

去除掉类型转换的部分,简化就是

(blockName->FuncPtr)(blockName, 0);

那么其实就是调用了block结构体中的FuncPtr所指的函数。

至此实际上没有什么复杂的点,那么再看看它对捕获的局部变量的处理,在main.m中这样写:

int main(int argc, char * argv[]) {
    int a = 1;
    void (^blockName)(int) = ^void (int param){
        printf("%d", a);
        printf("%d", param);
        return;
    };
    blockName(0);
    return 0;
}

这里还需要导入一个包含printf函数的头文件,例如stdio.h或者foundation.h,转译后的代码会变得很长,但依旧是找到最后面的main函数部分,发现block的构造函数多了一个参数:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

新增的成员即为被捕获的变量a,作为了__main_block_impl_0的成员,且在初始化时就将a的值赋给了结构体内的成员a,这与我们之前想象的一致,即外部的局部变量与block内部使用的局部变量实际上是两个元素。

而block函数部分变为:

static void __main_block_func_0(struct __main_block_impl_0 *__cself, int param) {
    int a = __cself->a; // bound by copy
    
    printf("%d", a);
    printf("%d", param);
    return;
}

系统自动生成了一句注释bound by copy,提醒我们在block函数中使用的“局部变量”仅仅只是一个复制的值,而没有获取它的地址,因此无法改变原本的局部变量值。这里大家也可以尝试一下其他类型的变量,比如oc对象类型,本质上都是生成了一个与局部变量类型完全相同的拷贝作为block结构体的新成员。

至此应该没有任何问题,那么为了解答我们之前的疑问,在为局部变量添加了__block修饰符后,block是如何联系内外的局部变量的。

我们在a前加上__block修饰符

int main(int argc, char * argv[]) {
    __block int a = 1;
    void (^blockName)(int) = ^void (int param){
        a = 2;
        printf("%d", a);
        return;
    };
    blockName(0);
    return 0;
}

然后转译得到:

int main(int argc, char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
    void (*blockName)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    ((void (*)(__block_impl *, int))((__block_impl *)blockName)->FuncPtr)((__block_impl *)blockName, 0);
    return 0;
}

此时发现,加上了__block的int型变量a,在转译后竟然变成了一个结构体类型__Block_byref_a_0。

其声明为:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

种一棵树最好的时间是在十年前,而后是现在。

Loading Disqus comments...
Table of Contents