前言

iOS与OS X多线程和内存管理 读书笔记(二) Blocks

笔记

Blocks概要

  1. Blocks:带有自动变量(局部变量)的匿名函数

  2. 完整的Block语法与一般的C语言函数定义相比只有2点不同:

    1. 开头多了^符号
    2. 没有函数名
  3. 看看如何声明Block类型变量:int (^blk)(int);声明了一个参数类型为int,返回类型为int的Block变量blk。

  4. typedef int (^blk_t)(int); 那么blk_t代表一个Block变量

  5. Block会自动截获和保存变量值。但是不能更改保存的变量值,除非你使用__block。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    int val = 0;
    void (^blk)(void) = ^{val = 1}; // 更改自动保存的变量值 报错
    blk();

    /* 使用__block之后 */
    __block int val = 0;
    void (^blk)(void) = ^{val = 1}; // 不报错
    blk();

    PS:如果是截获的OC对象,比如截获了NSMutableArray对象array,可以使用array:[array addObject:obj];;但是不能给array赋值:array = [[NSMutableArray alloc] init];。若要给他赋值,还是得使用__block修饰符。

    PS:不会自动截获C语言数组:char text[] = "hello";;但是可以截获指针:char *text = "hallo";

Blocks的实现

  1. Block其实就是Objective-C对象

  2. 来看看具体实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    int main() {
    void (^blk)(void) = ^{printf("Bolck.../n");};
    blk();
    return 0;
    }

    /* 经过编译器转化之后 */

    /* 下面的结构体表示的是Block对象 */
    struct __main_block_impl_0 {
    void *isa; // OC对象都有一个isa指向它的类,表明这个对象是一个什么类

    /* 无关紧要的一些成员 */
    int Flags;
    int Reserved;
    struct __main_block_desc_0 *Desc; // 一个结构体指针,后面会有定义,大概和系统有关

    void *FuncPtr; // 函数指针,指向这个Block的匿名函数,也就是 {printf("Block.../n");}

    /* 结构体的初始化函数 */
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags = 0) {
    // 传入的参数是匿名函数的函数指针,__main_block_desc_0结构体指针和flags
    isa = &_NSConcreteStackBlock; // 初始化isa
    Flags = flags;
    FuncPtr = fp;
    Desc = desc;
    }
    }

    /* 下面代码的结构体 就是上面Block结构体中的那个无关理解的结构体指针成员的定义 */
    static struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
    } __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
    };

    /* 匿名函数的定义,传入的参数是Block结构体指针 */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Block.../n");
    }

    /* main函数的实现 */
    int main {
    void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); // 初始化Block,其实也就是创建Block结构体
    blk->FuncPtr(blk); // 执行Block,也就是执行Block结构体中的函数,传入结构体本身
    return 0;
    }

    上面这个编译的过程就是:

    1. 在main函数外面,先对Block做定义:Block是一个结构体,他有一个成员是匿名函数的函数指针;这个结构体的构造函数需要传入匿名函数的函数指针。
    2. 在main函数外面再定义匿名函数,匿名函数的参数是Block结构体,该参数在截获自动变量时会有用。
    3. 定义main函数,先新建一个Block结构体并初始化;在执行Block,也就是执行Block结构体里面的函数指针指向的函数;再return 0。

    PS:结构体的初始化里面的isa = &_NSConcreteStackBlock;就是在将结构体当做OC对象在处理,他将OC对象的信息放在isa里面。

  3. 看看如何截获自动变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    int main() {
    int dmy = 256; // 不会被截获的变量
    int val = 10; // 被截获
    const chat *fmt = "val = %d/n";
    void (^blk)(void) = ^{printf(fmt, val);};
    blk();
    return 0;
    }

    /* 经过编译之后 */

    /* Block结构体定义 */
    struct __main_block_impl_0 {
    /* 如前所诉 */
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    struct __main_block_desc_0 *Desc;

    /* 截获的自动变量,变成了结构体成员 */
    const char *fmt;
    int val;

    /* 初始化函数,这里会使用截获的自动变量对成员进行赋值 */
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags = 0) :fmt(_fmt), val(_val) {
    /* 如前所诉,就省略了 */
    //...
    }
    }

    /* __main_block_desc_0结构体定义,同上,省略 */

    /* 匿名函数定义 */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    const char *fmt = __cself->fmt;
    int val = __cself->val;

    printf(fmt, val);
    }

    /* main函数实现 */
    int main() {
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d/n";

    void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &main_block_desc_0_DATA, fmt, val); // 这里进行Block的初始化
    blk->FuncPtr(blk); // 执行Block

    return 0;
    }

    从上面可以看出,在初始化得时候,将自动变量传入了Block结构体的初始化函数,进行保存。所以之后对这些自动变量进行修改,Block结构体里面的这些保存在结构体成员里的变量是不受影响的。

  4. __block的实现

    __block变量也定义在main函数之外,不在只是一个简单的变量,而是变成了结构体:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    struct __Block_byref_val_0 {
    void *isa;
    __Block_byref_val_0 *__forwarding; // 指向自己的指针
    int __flags;
    int __size;
    int val; // 原来的自动变量
    }

    /* Block里面含有__block变量的结构体指针 */
    struct __main_block_impl_0 {
    /* 同上的成员省略 */
    __Block_byref_val_0 *val; // 指向val结构体的指针

    /* 结构体初始化函数,使用val结构体的__forwarding成员变量初始化Block结构体的val指针 */
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags = 0) : val(_val->forwarding) {
    /* 同上 省略 */
    }
    }

    /* 匿名函数 */
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_val_0 *val = __cself->val;
    val->forwarding->val = 1; // 在这里 val和val->forwarding其实都指的是自己
    }

    /* main函数里面 */
    int main() {
    int val = 10;
    void (^blk)(void) = ^{val = 1;};
    }

    /* 转化之后 val变成结构体 */
    int main() {
    __Block_byref_val_0 val = {
    0,
    &val, // 指向自身的指针
    0,
    sizeof(__Block_byref_val_0),
    10 // 初始化
    };

    blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val); // 初始化Block结构体时将val结构体地址传过去
    }

    将val变量变成存放在栈上的结构体,这也就是__block变量

  5. Block存储域

    1. _NSConcreteStackBlock将Block存放在栈上
    2. _NSConcreteGlobalBlock将Block存放在数据区域
    3. _NSConcreteMallocBlock将Block存放在堆上

    存放在数据区域的Block相当于全局变量,不能截获自动变量,全局只有一个实例。所有不需要截获自动变量的Block都是这种类型的。

    其他的Block都现在栈上生成,但是当超过这个Block的作用域,但是又要使用该Block时,就会将Block从栈上复制到堆上。例如下面的例子:

    1
    2
    3
    4
    typedef int (^blk_t)(int);
    blk_t func(int rate) {
    return ^(int count)(return rate *count);
    }

    这里Block在函数内生成后,返回时就已经离开Block的作用域了。这时候就需要将他copy到堆上

    1
    2
    3
    4
    5
    blkt func(int rate) {
    blkt tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
    tmp = _Block_copy(tmp); // 将栈上的Block复制到堆上
    return objc_autoreleaseReturnValue(tmp); // 将tmp注册到NSAutoreleasePool,说明Block也是OC对象
    }

    出了函数之后,栈上的Block就会被释放,这个时候只能使用在堆上的Block了。很多情况下编译器会帮助我们自动复制的,但是NSArray得initWithObjects方法就不会自动复制:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    - (id)getBlockArray {
    int val = 10;
    return [[NSArray alloc] initWithObjects:
    ^{NSLog(@"haha");},
    ^{NSLog(@"yaya");}, nil];
    }

    // 这种情况下返回的NSArray,如果去获取NSArray中的Block对象,是会出错的,因为这些Block在出函数时被废弃了, 这时就需要我们手动复制了,也就是调用copy方法

    - (id)getBlockArray {
    int val = 10;
    return [[NSArray alloc] initWithObjects:
    [^{NSLog(@"haha");} copy],
    [^{NSLog(@"yaya");} copy], nil];
    }

    PS:当Block被复制到堆上时,它 使用 的__block变量也会从栈上被复制到堆上,这个时候该变量会被Block 持有。如果还有在栈上的其他Block使用block变量的话,block变量还会保留在栈上,但是他的 forwarding指针已经指向堆上的block变量了,所以改变block变量的话,始终改变的都是同一个结构体。

  6. 看看如何截获OC对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    id array = [[NSMutableArray alloc] init];
    blk = [^(id obj) {
    [array addObject:obj];
    } copy];
    }

    /* 注意 可以理解成 Block里面的array结构体成员强引用NSMutableArray对象 */
    struct __main_block_impl_0 {
    id __strong array; // 强引用
    }

    但实际上结构体对__strong是没有概念的,不过此时的结构体会手动加上ARC的概念上去。当Block从栈上复制到堆上时,OC对象被Block持有;当Block从堆上废弃时,OC对象被释放。所以,很重要的一点:只有Block被复制到堆上时,OC对象,才能被Block持有;也就是要调用Block的copy函数。

  7. __block类型的OC对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    __block id obj = [[NSObject alloc] init];

    /* 编译之后 */
    struct __Block_byref_obj_0 {
    void *isa;
    __Block_byref_obj_0 *__forwarding;
    int __flags;
    int __size;

    void (*__Block_byref_id_object_copy)(void*, void*); // 用来使Block持有对象
    void (*__Block_byref_id_object_dispose)(void*); // 用来使Block释放对象

    __strong id obj;
    }

    所以__block类型的OC对象也是讲OC对象封装成一个结构体,和之前的block类型的自动变量一样,只不过添加了retain和release的概念而已。

    PS:如果对象是weak类型的,和上一篇里面分析weak和strong的一样。Block也只是含有一个指向对象的指针而已,如果该指针为weak,那么该置成nil,还是要置成nil。

  8. 看看使用Block的一个常见问题:循环引用

    最常见的就是在Block中使用self,而这个Block还是self对象的一个变量。那么对象持有Block,Block又持有对象。循环引用。所以在Block中使用self经常需要使用weakself。