Think in Block(上)

Block 简介

这不是一篇介绍如何使用Block的文章,主要想要探究一下系统背后Block发生了什么,主要参考的是《Objective-C高级编程》这本书,不过这本书本身也有些年代了,苹果实现的机制也有了一些改变。实践出真知,不清楚的地方自己动手试一下,会有意外地收获。

Block是什么

苹果官网说法:
A block is an anonymous inline collection of code, and sometimes also called a “closure”.

通俗的讲:
闭包就是能够读取其它函数内部变量的匿名函数

与普通函数的区别:
普通函数能使用局部变量,参数,静态变量,静态全局,全局变量,并且有自己的函数名。
block除了能使用这些外还能使用其他函数内的变量,而且没有函数名。

BLock的用法和功能

语法

void (^)(void){
};
与C语言函数相比没有函数名,带有^;可省略很多部分。

变量

void (^var)(void);
与C语言函数指针相比只是把*改成了^,Block变量和普通变量作用完全相同。

  1. 通过typedef可以简化声明
  2. 调用和C语言函数调用一样,变量名+小括号
截获变量
  1. Block可以使用在它之前声明的局部变量,因此在执行Block时,即使已经改变了值,也不影响Block中截获的值。
  2. 一般情况,Block只能保存声明时的瞬间值,保存后就不能修改了;如果需要改变则要附加__block.
  3. 如果截获的是oc对象,使用是没有问题的,但是如果要重新赋值,则也必须加__block。

Block的实现

Block的本质

先看下源代码:

int main() {

    void (^blk)(void) = ^{
        printf("Block\n");
    };

    blk();
    return 0;
}

很简单,简单地定义一个Block,赋给blk对象,然后调用这个Block,那么这段代码经过编译器编译过后,发生了什么呢?我们用Clang 命令 -rewirte-objc转换成c++看下编译器搞得什么鬼!

首先是这一段,这段代码是编译器插入的代码,主要作用就是声明了一个结构体,这可以看成是我们的Block类的数据部分

#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

这一段则是系统给我们声明的真正的Block类,其中包括了数据部分和一些描述性内容,以及一个初始化函数。

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

这一段则是赋值给Block变量中的那个函数的声明,其中会把self传入,这个很多语言都会这么处理,在内部使用self的时候能够找到真实地那个对象。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        printf("Block\n");
    }

这一段则是前面所说的描述性的内容,主要就是Block对象的大小,因为这个例子比较简单,复杂的情况这里还会生成其他的一些函数,后面会讲到。
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)};

最后则是这一段我们写得main函数的代码,通过调用__main_block_impl_0函数来初始化一个Block对象,最后调用也就是简单地用c语言函数指针的方式调用。
int main() {

    void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

从isa的存在我们也大概能够看出来Block其实就是Objc的对象,只不过这是一个为了性能考虑而可能在栈上生成的一个对象。

如何截获自动变量

这一次尝试一下使用Block来截获外部变量,看一下,Block如何能够使用外部变量,原函数:

int main() {

    int dmy = 256;
    int var = 10;
    const char *fmt = "var = %d\n";

    void (^blk)(void) = ^{
        printf(fmt, var);
    };

    var = 2;
    fmt = "Real var = %d\n";

    blk();
    return 0;
}

重写以后发现,其实没有多大改变,只是多了一些内容:

  • __main_block_impl_0中多了一个指针fmt,和int类型的变量,其实也很容易理解,就是用来存放外部变量的。
  • __main_block_impl_0初始化的时候会同时对成员变量赋值。
  • __main_block_func_0中会在开始的地方帮你声明两个同名的变量,赋值的值则是从传入的self通过指针取值。

那么其实从这些我们也不难看出,Block其实只截获了内部用到对象,以及为什么Block中普通截获的值不能改变,当然不能改写只是编译器检查,因为改了也没用。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int var;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _var, int flags=0) : fmt(_fmt), var(_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int var = __cself->var; // bound by copy

        printf(fmt, var);
    }

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)};
int main() {

    int dmy = 256;
    int var = 10;
    const char *fmt = "var = %d\n";

    void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, var);

    var = 2;
    fmt = "Real var = %d\n";

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

__block的作用

首先来看下,有哪些变量是就算不在函数内声明,函数中也能使用和改变值的:静态变量,静态全局,全局变量。那这些变量是通过什么方式来实现的呢?
对于全部变量来说,其实本身它的作用域就已经包括了函数所在区域,能使用并不奇怪,普通的静态变量其实是通过指针来做到的。
那其实也不难推出,__block基本上来说也是通过指针(因为这也是最简单地一种方法)。
废话不多说,上代码:

int main() {

    __block int var = 10;
    void (^blk)(void) = ^{
        var = 1;
    };
    return 0;
}

经过重写后发现,增加了狂多代码,不过不用慌,都是可读性很强的代码,慢慢分析。

这一段就是block说明符修饰过后的变量最终声明的结构体,其中 forwarding很重要(这个是不同存储区域能访问到同一个变量的关键)

struct __Block_byref_var_0 {
  void *__isa;
__Block_byref_var_0 *__forwarding;
 int __flags;
 int __size;
 int var;
};

在这里大概也看出了一点端倪,其实传给var指针的是_var->forwarding,后面会给出 forwarding指向的是谁。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_var_0 *var; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

看到这,基本就知道为什么用__block修饰的变量,在Block中也能改变了,因为内部声明时是用了指针嘛。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_var_0 *var = __cself->var; // bound by ref

        (var->__forwarding->var) = 1;
    }

这里的copy,和dispose先略过,在后续的Objc对象那部分会详解。
static void main_block_copy_0(struct main_block_impl_0dst, struct __main_block_impl_0src) {_Block_object_assign((void)&dst->var, (void)src->var, 8/BLOCK_FIELD_IS_BYREF/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->var, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

通过这一段,可以看出其实var已经不是简单地Int类型了,变得老长老长了,编译器总是在默默中给我们加了点料。同时也知道了__forwarding指向的是var自身。
int main() {

    __attribute__((__blocks__(byref))) __Block_byref_var_0 var = {(void*)0,(__Block_byref_var_0 *)&var, 0, sizeof(__Block_byref_var_0), 10};
    void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_var_0 *)&var, 570425344);
    return 0;
}

Block,__block存储域

Q:Block超出变量作用域可存在?

A:

  1. 配置在全局变量中的Block,在变量作用域之外也可以通过指针安全的使用
  2. 在栈上的Block则是通过复制到堆上来解决的
  3. block变量用 forward可以实现无论配置在哪都可以正确地访问 __block变量
  4. 在ARC下,大多数情况下编译器会恰当的判断,自动将Block从栈上复制到堆上。(autorelease)

补充:

  • 自动copy:Block作为函数的返回值时
  • 手动copy:向方法或函数的参数中传递Block(但是可以在函数中适当地copy就不必在传递前copy)(例如GCD,Animation等)

Q:调用Copy时不同的Block对象究竟发生了什么:

  1. _NSConcreteStackBlock :从栈复制到堆
  2. _NSConcreteGlobalBlock :什么也不做
  3. _NSConcreteMallocBlock :引用计数增加

Q:调用Copy时不同的__block对象究竟发生了什么:

  1. 栈 :从栈复制到堆,并且被Block持有
  2. 堆 :被Block持有

新的开始

这一篇,主要把Block变量和__block变量分析了一下,对于Objc中得对象,Block如何处理放到下一篇详细的讲述,同时下一篇也会详细的讲解一下避免Block循环引用的N种姿势。以及之前没有讲得copy和dispose是干嘛的,顺带着还会说一下mrc下地Block。

当然本人才疏学浅,上面只是个人的一些理解,可能会有偏差,如果有误,欢迎吐槽纠正。