ios - Blocks - 底层原理

本文将通过 Blocks 由 OC 转变的 C++ 源码来一步步解析 Blocks 的底层原理。
通过本文您将了解到:
Blocks 的实质是什么?
Block 截获局部变量和特殊区域变量
Block 的存储区域
Block 的循环引用

Blocks的本质

Blocks 是带有局部变量的匿名函数,但要想了解 Block 的本质,就需要从 Block 对应的 C++ 源码来入手。

Blocks 源码概览

  • 转换前 OC 代码:
int main () {
    void (^myBlock)(void) = ^{
        printf("myBlock\n");
    };

    myBlock();

    return 0;
}
  • 转换后 C++ 源码:
/* 包含 Block 实际函数指针的结构体 */
struct __block_impl {
    void *isa;
    int Flags;               
    int Reserved;        // 今后版本升级所需的区域大小
    void *FuncPtr;      // 函数指针
};

/* Block 结构体 */
struct __main_block_impl_0 {
    // impl:Block 的实际函数指针,指向包含 Block 主体部分的 __main_block_func_0 结构体
    struct __block_impl impl;
    // Desc:Desc 指针,指向包含 Block 附加信息的 __main_block_desc_0() 结构体
    struct __main_block_desc_0* Desc;
    // __main_block_impl_0:Block 构造函数
    __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 主体部分结构体 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("myBlock\n");
}

/* Block 附加信息结构体:包含今后版本升级所需区域大小,Block 的大小*/
static struct __main_block_desc_0 {
    size_t reserved;        // 今后版本升级所需区域大小
    size_t Block_size;    // Block 大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

/* main 函数 */
int main () {
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}

下面我们一步步来拆解转换后的源码。

Block 结构体

我们先来看看 __main_block_impl_0结构体( Block 结构体)

/* Block 结构体 */
struct __main_block_impl_0 {
    // impl:Block 的实际函数指针,指向包含 Block 主体部分的 __main_block_func_0 结构体
    struct __block_impl impl;
    // Desc:Desc 指针,指向包含 Block 附加信息的 __main_block_desc_0() 结构体
    struct __main_block_desc_0* Desc;
    // __main_block_impl_0:Block 构造函数
    __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结构体(Block 结构体)包含了三个部分:
1.成员变量 impl;
2.成员变量 Desc 指针;
3.__main_block_impl_0构造函数。

我们先来把这几个部分剖析一下。

struct __block_impl impl 说明
第一部分 impl__block_impl结构体类型的成员变量。__block_impl 包含了 Block 实际函数指针FuncPtrFuncPtr指针指向 Block 的主体部分,也就是 Block 对应 OC 代码中的 ^{ printf("myBlock\n"); }; 部分。还包含了标志位Flags,今后版本升级所需的区域大小Reserved__block_impl结构体的实例指针isa

  1. 对象的首地址是isa变量,isa指向了其类对象。
    通过ISA,可以在运行时找到一个对象的所有信息,如继承层次结构中的位置,它的实例变量的大小和结构,以及可以相应消息的方法所实现的位置。
    2.标志位简介:
    标志寄存器,又称程序状态寄存器(它的内容是Program Status Word,PSW).这是一个存放条件码标志,控制标志和系统标志的寄存器.
/* 包含 Block 实际函数指针的结构体 */
struct __block_impl {
    void *isa;               // 用于保存 Block 结构体的实例指针
    int Flags;               // 标志位
    int Reserved;        // 今后版本升级所需的区域大小
    void *FuncPtr;      // 函数指针
};

struct __main_block_desc_0* Desc说明
第二部分 Desc 是指向的是__main_block_desc_0类型的结构体的指针型成员变量,__main_block_desc_0 结构体用来描述该 Block 的相关附加信息:
1.今后版本升级所需区域大小: reserved 变量。
2.Block 大小:Block_size 变量。

/* Block 附加信息结构体:包含今后版本升级所需区域大小,Block 的大小*/
static struct __main_block_desc_0 {
    size_t reserved;      // 今后版本升级所需区域大小
    size_t Block_size;  // Block 大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

__main_block_impl_0 构造函数说明
第三部分是 __main_block_impl_0 结构体(Block 结构体) 的构造函数,负责初始化__main_block_impl_0 结构体(Block 结构体) 的成员变量。

__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() 函数中,对该构造函数的调用。

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

我们可以把上面的代码稍微转换一下,去掉不同类型之间的转换,使之简洁一点:

struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *myBlock = &temp;

这样,就容易看懂了。该代码将通过 __main_block_impl_0 构造函数,生成的 __main_block_impl_0 结构体(Block 结构体)类型实例的指针,赋值给 __main_block_impl_0 结构体(Block 结构体)类型的指针变量 myBlock。

可以看到, 调用 __main_block_impl_0构造函数的时候,传入了两个参数。
1.第一个参数:__main_block_func_0

  • 其实就是 Block 对应的主体部分,可以看到下面关于__main_block_func_0结构体的定义 ,和 OC 代码中^{ printf("myBlock\n"); }; 部分具有相同的表达式。
  • 这里参数中的 __cself是指向 Block 的值的指针变量,相当于 OC 中的 self。
/* Block 主体部分结构体 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("myBlock\n");
}

2.第二个参数:__main_block_desc_0_DATA__main_block_desc_0_DATA 包含该 Block 的相关信息。
我们再来结合之前的 __main_block_impl_0结构体定义。

  • __main_block_impl_0 结构体(Block 结构体)可以表述为:
struct __main_block_impl_0 {
    void *isa;               // 用于保存 Block 结构体的实例指针
    int Flags;               // 标志位
    int Reserved;        // 今后版本升级所需的区域大小
    void *FuncPtr;      // 函数指针
    struct __main_block_desc_0* Desc;      // Desc:Desc 指针
};
  • __main_block_impl_0构造函数可以表述为
impl.isa = &_NSConcreteStackBlock;    // isa 保存 Block 结构体实例
impl.Flags = 0;        // 标志位赋值
impl.FuncPtr = __main_block_func_0;    // FuncPtr 保存 Block 结构体的主体部分
Desc = &__main_block_desc_0_DATA;    // Desc 保存 Block 结构体的附加信息

Block 实质总结

__main_block_impl_0 结构体(Block 结构体)相当于 Objective-C 类对象的结构体,isa 指针保存的是所属类的结构体的实例的指针。_NSConcreteStackBlock相当于 Block 的结构体实例。对象impl.isa = &_NSConcreteStackBlock; 语句中,将 Block 结构体的指针赋值给其成员变量 isa,相当于 Block 结构体的成员变量 保存了 Block 结构体的指针,这里和 Objective-C 中的对象处理方式是一致的。

也就是说明: Block 的实质就是对象。Block 跟其他所有的 NSObject 一样,都是对象。

Block 截获局部变量和特殊区域变量

Blcok 截获局部变量的实质

回顾一下上篇文章讲解的例子:

// 使用 Blocks 截获局部变量值
- (void)useBlockInterceptLocalVariables {
    int a = 10, b = 20;

    void (^myLocalBlock)(void) = ^{
        printf("a = %d, b = %d\n",a, b);
    };

    myLocalBlock();    // 输出结果:a = 10, b = 20

    a = 20;
    b = 30;

    myLocalBlock();    // 输出结果:a = 10, b = 20
}

从中可以看到,我们在第一次调用 myLocalBlock(); 之后已经重新给变量 a、变量 b 赋值了,但是第二次调用 myLocalBlock(); 的时候,使用的还是之前对应变量的值。

这是因为 Block 语法的表达式使用的是它之前声明的局部变量 a、变量 b。Blocks 中,Block 表达式截获所使用的局部变量的值,保存了该变量的瞬时值。所以在第二次执行 Block 表达式时,即使已经改变了局部变量 a 和 b 的值,也不会影响 Block 表达式在执行时所保存的局部变量的瞬时值。
这就是 Blocks 变量截获局部变量值的特性。

为什么 Blocks 变量使用的是局部变量的瞬时值,而不是局部变量的当前值呢?
我们来看一下对应的 C++ 代码:

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    int b = __cself->b; // bound by copy

    printf("a = %d, b = %d\n",a, b);
}

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 a = 10, b = 20;

    void (*myLocalBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, b));
    ((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);

    a = 20;
    b = 30;

    ((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);
}

1.可以看到 __main_block_impl_0 结构体(Block 结构体)中多了两个成员变量ab,这两个变量就是 Block 截获的局部变量。 a 和 b 的值来自与__main_block_impl_0 构造函数中传入的值。

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int a;    // 增加的成员变量 a
    int b;    // 增加的成员变量 b
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) {    
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

2.还可以看出__main_block_func_0(保存 Block 主体部分的结构体)中,变量 a、b 的值使用的__cself 获取的值。
__cself->a__cself->b 是通过值传递的方式传入进来的,而不是通过指针传递。这也就说明了 a、b 只是 Block 内部的变量,改变 Block 外部的局部变量值,并不能改变 Block 内部的变量值。

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

那么来总结一下:
在定义 Block 表达式的时候,局部变量使用『值传递』的方式传入 Block 结构体中,并保存为 Block 的成员变量。
而当外部局部变量发生变化的时候,Block 结构体内部对应的的成员变量的值并没有发生改变,所以无论调用几次,Block 表达式结果都没有发生改变。

如果在 Block 主体部分对外部局部变量进行修改呢?类似下面这样,是不是就可以将截获的外部局部变量修改了?

int a = 10, b = 20;

void (^myLocalBlock)(void) = ^{
    a = 20;
    b = 30;

    printf("a = %d, b = %d\n",a, b);
};

myLocalBlock();   

很遗憾,编译直接报错了。

由此我们暂时可以得出一个结论:
被截获的自动变量的值是无法直接修改的。
有一个办法,可以通过__block 说明符修饰局部变量。

使用 __block 说明符更改局部变量值

// 使用 __block 说明符修饰,更改局部变量值
- (void)useBlockQualifierChangeLocalVariables {
    __block int a = 10, b = 20;

    void (^myLocalBlock)(void) = ^{
        a = 20;
        b = 30;

        printf("a = %d, b = %d\n",a, b);    // 输出结果:a = 20, b = 30
    };

    myLocalBlock();
}

从中我们可以发现:通过 __block 修饰的局部变量,可以在 Block 的主体部分中改变值。
我们来转换下源码,分析一下:

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

struct __Block_byref_b_1 {
    void *__isa;
    __Block_byref_b_1 *__forwarding;
    int __flags;
    int __size;
    int b;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_a_0 *a; // by ref
    __Block_byref_b_1 *b; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_a_0 *a = __cself->a; // bound by ref
    __Block_byref_b_1 *b = __cself->b; // bound by ref

    (a->__forwarding->a) = 20;
    (b->__forwarding->b) = 30;

    printf("a = %d, b = %d\n",(a->__forwarding->a), (b->__forwarding->b));
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->b, 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};

int main() {
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
    __Block_byref_b_1 b = {(void*)0,(__Block_byref_b_1 *)&b, 0, sizeof(__Block_byref_b_1), 20};

    void (*myLocalBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, (__Block_byref_b_1 *)&b, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);

    return 0;
}

可以看到,只是加上了一个__block,代码量就增加了很多。

我们从 __main_block_impl_0开始说起

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_a_0 *a; // by ref
    __Block_byref_b_1 *b; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

我们在 __main_block_impl_0 结构体中可以看到: 原 OC 代码中,被 __block 修饰的局部变量__block int a__block int b 分别变成了 __Block_byref_a_0__Block_byref_b_1类型的结构体指针 a、结构体指针 b。这里使用结构体指针 a 、结构体指针 b 说明_Block_byref_a_0__Block_byref_b_1 类型的结构体并不在 __main_block_impl_0 结构体中,而只是通过指针的形式引用,这是为了可以在多个不同的 Block 中使用 __block 修饰的变量。

__Block_byref_a_0__Block_byref_b_1类型的结构体声明如下:


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

struct __Block_byref_b_1 {
    void *__isa;
    __Block_byref_b_1 *__forwarding;
    int __flags;
    int __size;
    int b;
};

拿第一个 __Block_byref_a_0 结构体定义来说明,__Block_byref_a_0 有 5 个部分:
1.__isa:标识对象类的 isa 实例变量
2.__forwarding:传入变量的地址
3.__flags:标志位
4.__size:结构体大小
5.a:存放实变量 a 实际的值,相当于原局部变量的成员变量(和之前不加__block修饰符的时候一致)。

再来看一下main()函数中,__block int a__block int b的赋值情况。

顺便把代码整理一下,使之简易一点:

__Block_byref_a_0 a = {
    (void*)0,
    (__Block_byref_a_0 *)&a, 
    0, 
    sizeof(__Block_byref_a_0), 
    10
};

__Block_byref_b_1 b = {
    0,
    &b, 
    0, 
    sizeof(__Block_byref_b_1), 
    20
};

还是拿第一个__Block_byref_a_0 a的赋值来说明。

可以看到 __isa 指针值传空,__forwarding指向了局部变量 a 本身的地址,__flags 分配了 0,__size为结构体的大小,a 赋值为 10。下图用来说明__forwarding 指针的指向情况。

image.png

这下,我们知道 __forwarding 其实就是局部变量 a 本身的地址,那么我们就可以通过 __forwarding 指针来访问局部变量,同时也能对其进行修改了。

来看一下 Block 主体部分对应的 __main_block_func_0结构体来验证一下。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_a_0 *a = __cself->a; // bound by ref
    __Block_byref_b_1 *b = __cself->b; // bound by ref

    (a->__forwarding->a) = 20;
    (b->__forwarding->b) = 30;

    printf("a = %d, b = %d\n",(a->__forwarding->a), (b->__forwarding->b));
}

可以看到 (a->__forwarding->a) = 20;(b->__forwarding->b) = 30; 是通过指针取值的方式来改变了局部变量的值。这也就解释了通过 __block 来修饰的变量,在 Block 的主体部分中改变值的原理其实是:通过『指针传递』的方式。

更改特殊区域变量值

除了通过 __block说明符修饰的这种方式修改局部变量的值之外,还有一些特殊区域的变量,我们也可以在 Block 的内部将其修改。

这些特殊区域的变量包括:静态局部变量、静态全局变量、全局变量。

我们还是通过 OC 代码和 C++ 源码来说明一下:

  • OC 代码:
int global_val = 10; // 全局变量
static int static_global_val = 20; // 静态全局变量

int main() {
    static int static_val = 30; // 静态局部变量

    void (^myLocalBlock)(void) = ^{
        global_val *= 1;
        static_global_val *= 2;
        static_val *= 3;

        printf("static_val = %d, static_global_val = %d, global_val = %d\n",static_val, static_global_val, static_val);
    };

    myLocalBlock();

    return 0;
}
  • C++ 代码:
int global_val = 10;
static int static_global_val = 20;

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *static_val = __cself->static_val; // bound by copy
    global_val *= 1;
    static_global_val *= 2;
    (*static_val) *= 3;

    printf("static_val = %d, static_global_val = %d, global_val = %d\n",(*static_val), static_global_val, (*static_val));
}

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() {
    static int static_val = 30;

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

    return 0;

}

从中可以看到:

__main_block_impl_0 结构体中,将静态局部变量static_val以指针的形式添加为成员变量,而静态全局变量static_global_val、全局变量 global_val 并没有添加为成员变量。

int global_val = 10;
static int static_global_val = 20;

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

再来看一下 Block 主体部分对应的__main_block_func_0 结构体部分。静态全局变量 static_global_val、全局变量 global_val是直接访问的,而静态局部变量 static_val则是通过『指针传递』的方式进行访问和赋值。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *static_val = __cself->static_val; // bound by copy
    global_val *= 1;
    static_global_val *= 2;
    (*static_val) *= 3;

    printf("static_val = %d, static_global_val = %d, global_val = %d\n",(*static_val), static_global_val, (*static_val));
}

Block 的存储区域

通过之前对 Block 本质的探索,我们知道了 Block 的本质是 Objective-C 对象。通过上述代码中impl.isa = &_NSConcreteStackBlock;,可以知道该 Block 的类名为 NSConcreteStackBlock,根据名称可以看出,该 Block 是存于栈区中的。而与之相关的,还有 _NSConcreteGlobalBlock_NSConcreteMallocBlock

_NSConcreteGlobalBlock

在以下两种情况下使用 Block 的时候,Block 为 NSConcreteGlobalBlock类对象。

1.记述全局变量的地方,使用 Block 语法时;
2.Block 语法的表达式中没有截获的自动变量时。
NSConcreteGlobalBlock 类的 Block 存储在『程序的数据区域』。因为存放在程序的数据区域,所以即使在变量的作用域外,也可以通过指针安全的使用。

  • 记述全局变量的地方,使用 Block 语法示例代码:
void (^myGlobalBlock)(void) = ^{
    printf("GlobalBlock\n");
};

int main() {
    myGlobalBlock();

    return 0;
}

通过对应 C++ 源码,我们可以发现:Block 结构体的成员变量 isa 赋值为:impl.isa = &_NSConcreteGlobalBlock;,说明该 Block 为 NSConcreteGlobalBlock 类对象。

_NSConcreteStackBlock

除了 3.1_NSConcreteGlobalBlock中提到的两种情形,其他情形下创建的 Block 都是 NSConcreteStackBlock 对象,平常接触的 Block 大多属于 NSConcreteStackBlock 对象。

NSConcreteStackBlock类的 Block 存储在『栈区』的。如果其所属的变量作用域结束,则该 Block 就会被废弃。如果 Block 使用了__block变量,则当 __block 变量的作用域结束,则__block变量同样被废弃。

image.png

_NSConcreteMallocBlock

为了解决栈区上的 Block 在变量作用域结束被废弃这一问题,Block 提供了 『复制』 功能。可以将 Block 对象和 __block 变量从栈区复制到堆区上。当 Block 从栈区复制到堆区后,即使栈区上的变量作用域结束时,堆区上的 Block 和 __block变量仍然可以继续存在,也可以继续使用。

image.png

此时,『堆区』上的 Block 为NSConcreteMallocBlock对象,Block 结构体的成员变量 isa 赋值为:impl.isa = &_NSConcreteMallocBlock;

那么,什么时候才会将 Block 从栈区复制到堆区呢?

这就涉及到了 Block 的自动拷贝和手动拷贝。

Block 的自动拷贝和手动拷贝

Block 的自动拷贝

在使用 ARC 时,大多数情形下编译器会自动进行判断,自动生成将 Block 从栈上复制到堆上的代码:
1,将 Block 作为函数返回值返回时,会自动拷贝;
2.向方法或函数的参数中传递 Block

Block 的手动拷贝

我们可以通过『copy 实例方法(即 alloc / new / copy / mutableCopy)』来对 Block 进行手动拷贝。当我们不确定 Block 是否会被遗弃,需不需要拷贝的时候,直接使用 copy 实例方法即可,不会引起任何的问题。

关于 Block 不同类的拷贝效果总结如下:


image.png

Block 的循环引用

从上文 2. Block 截获局部变量和特殊区域变量 中我们知道 Block 会对引用的局部变量进行持有。同样,如果 Block 也会对引用的对象进行持有(引用计数 + 1),从而会导致相互持有,引起循环引用。

/* —————— retainCycleBlcok.m —————— */   
#import <Foundation/Foundation.h>
#import "Person.h"

int main() {
    Person *person = [[Person alloc] init];
    person.blk = ^{
        NSLog(@"%@",person);
    };

    return 0;
}


/* —————— Person.h —————— */ 
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

@interface Person : NSObject
@property (nonatomic, copy) myBlock blk;
@end


/* —————— Person.m —————— */ 
#import "Person.h"

@implementation Person    

@end

我们将 retainCycleBlcok.m 转换为 C++ 代码来看一下:

节选部分 C++ 代码:

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *person = __cself->person; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_8957e0_mi_0,person);
}

可以看到 __main_block_impl_0 结构体中增加了成员变量 person,同时 __main_block_func_0结构体中也使用了 __cself->person

这样就导致了:person持有成员变量myBlock blk,而 blk 也同时持有成员变量 person,就造成了循环引用问题。

那么,如何来解决这个问题呢?

ARC 下,通过 __weak 修饰符来消除循环引用

在 ARC 下,可声明附有 __weak 修饰符的变量,并将对象赋值使用。

int main() {
    Person *person = [[Person alloc] init];
    __weak typeof(person) weakPerson = person;

    person.blk = ^{
        NSLog(@"%@",weakPerson);
    };

    return 0;
}

这样就可以解决循环引用的问题。我们再来转换为 C++ 代码来看看。

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    Person *__weak weakPerson = __cself->weakPerson; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_447367_mi_0,weakPerson);
}

可以看到,__main_block_impl_0 使用过了 __weak 对成员变量 person 进行弱引用。

这样,person 持有成员变量 myBlock blk,而 blkperson 进行弱引用,从而就消除了循环引用。

MRC 下,通过 __block 修饰符来消除循环引用

MRC 下,是不支持 __weak 修饰符的。我们可以通过 __block 来消除循环引用。

int main() {
    Person *person = [[Person alloc] init];
    __block typeof(person) blockPerson = person;

    person.blk = ^{
        NSLog(@"%@", blockPerson);
    };

    return 0;
}

使用__block 修饰后的 Block 示例代码中,节选的部分 C++ 代码:

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

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

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_536cd4_mi_0,(blockPerson->__forwarding->blockPerson));
}

可以看到,通过__block引用的blockPerson,生成了 __Block_byref_blockPerson_0结构体指针。这里通过指针的方式来访问 person,而没有对 person进行强引用,所以不会造成循环引用。

参考资料

https://www.jianshu.com/p/ba7ab9522cbc
《Objective-C 高级编程》干货三部曲(二):Blocks篇
https://blog.csdn.net/stephenbruce/article/details/51148652
https://www.cnblogs.com/-yfan/p/4600775.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345