iOS与OS X多线程和内存管理 - Blocks

前言

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。如下:

    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. 来看看具体实现

    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. 看看如何截获自动变量

    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函数之外,不在只是一个简单的变量,而是变成了结构体:

    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从栈上复制到堆上。例如下面的例子:

    typedef int (^blk_t)(int);
    blk_t func(int rate) {
          return ^(int count)(return rate *count);
    }
    

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

    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方法就不会自动复制:

    - (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对象

    {
          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对象

    __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。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器