美文网首页程序员iOS DeveloperiOS点点滴滴
《Objective-C 高级编程》Blocks 笔记摘要

《Objective-C 高级编程》Blocks 笔记摘要

作者: 世界的一缕曙光 | 来源:发表于2018-01-25 15:27 被阅读90次

    温馨说明:

    1. 下文出现的 Block(首字母大写)指的是形如 ^(参数){执行的任务} 的对象,它是一个结构体对象。
    2. 下文的局部变量和书中提到的自动变量是一个概念。由于个人习惯,所以称作为局部变量。

    Blocks 模式

    截获局部变量值

    - (void)captureValue {
        int val = 10;
        const char *fmt = "val = %d\n";
      // 将 val 和 fmt 的值拷贝到内部,以后外部的 val 和 fmt 怎么变都不影响内部。
        void (^blk)(void) = ^{
            printf(fmt, val);
        };
        val = 2;
        fmt = "These values were changed. val = %d\n";
        // 打印:val = 10
        blk();
    }
    

    __block 说明符

    // __block:使得 val 在 block 内部也能修改。
    __block int val = 0;
    void(^blk)(void) = ^{ val = 1; };
    bike();
    // val = 1
    pritf("val = %d\n", val);
    

    截获的局部变量

    NSMutableArray *array = [NSMutableArray array];
    void(^blk2)(void) = ^{
                id o = [[NSObject alloc] init];
                // 不报错
                [array addObject:o];
                // 报错。需要在外部添加 __block
                // array = nil;
                NSLog(@"%@", array);
            };
    blk2();
    
    /*
    在使用 C 语言数组时必须小心使用其指针。
    下面代码段只是使用 C 语言的字符串字面量数组,并没有向截获的自动变量赋值,看似没有问题,实际还是会编译报错。
    因为在 Blocks 中,截获自动变量的方法并没有实现对 C 语言数组的截获。
    这时使用指针可以解决该问题。
    */
    const char text[] = "hello";
    void (^blk3)(void) = ^{
        // 报错
        // Cannot refer to declaration with an array type inside block
        printf("%c\n", text[2]);
    };
    
    // 将text改成指针就能解决问题
    const char *text = "hello";
    void (^blk4)(void) = ^{
        printf("%c\n", text[2]);
    };
    blk4();
    

    const char *text 与 const char text[] 的区别链接2

    前者创建的是一个指针,后者创建的是一个数组。

    const char* ptr = "Hello World!";
    const char  arr[] = "Hello World!";
    
    ptr = "Goodbye"; // okay
    arr = "Goodbye"; // illegal
    
    sizeof(ptr) == size of a pointer, usually 4 or 8
    sizeof(arr) == number of characters + 1 for null terminator
    
    // 字符的个数,不会包含 null;如果是中文字符,一个字符可能是3个长度。
    strlen(ptr);
    strlen(arr);
    

    Blocks 的实现

    不包含外部参数的 Block

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

    将上述代码通过 clang -rewrite-objc main.m 转成 C++ 代码,简化之后如下:

    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;
        // 这里的fp就是传入的__main_block_func_0函数指针
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    // 将 __main_block_impl_0 中的变量进一步展开
    // struct __main_block_impl_0 {
    //   void *isa;
    //   int Flags;
    //   int Reserved;
    //   void *FuncPtr;
    //   struct __main_block_desc_0* Desc;
    // };
    
    // __main_block_impl_0 初始化的样子
    // struct __main_block_impl_0 {
    //   void *isa = &_NSConcreteStackBlock;
    //   int Flags = 0;
    //   int Reserved = 0;
    //   void *FuncPtr = __main_block_func_0;
    //   struct __main_block_desc_0* Desc = &__main_block_desc_0_DATA;
    // };
    
    // Block 的执行任务
    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;
      // Block 的大小
      size_t Block_size; 
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; // 这里使用了 __main_block_impl_0 结构体的实例大小进行初始化
    
    /* main 函数 */
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
                                        
            // void (*blk)(void) = ^{printf("Blocks\n");};
            /* 
            简化:
            void (*blk)(void) = &__main_block_impl_0(__main_block_func_0,   &__main_block_desc_0_DATA);
            由此可知:__main_block_impl_0 中传入 __main_block_func_0 和静态全局变量初始化的 __main_block_desc_0_DATA 函数指针
            */                    
            void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));                            
            
            // blk();
            /*
            简化:(blk->FuncPtr)(blk);
            */                    
            (((__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
        return 0;
    }
    

    执行步骤解释

    // 1. 原始代码:赋值
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    // 转换之后的代码
    void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);         
    
    /* 解释
     __main_block_impl_0 是在栈上生成的结构体实例,然后把该结构体实例的指针赋值给了 blk 变量。
    
    这个结构体需要传入两个参数。
    参数一是 __main_block_func_0(包装需要执行的任务),参数二是 &__main_block_desc_0_DATA(设置结构体的大小信息)。
    __main_block_impl_0 内部有一个 struct __block_impl 类型的变量 impl。
    这个 impl 是一个结构体,它有一个函数指针 FuncPtr。
    __main_block_func_0 就是赋值给函数指针 FuncPtr 的值。
    */
    
    // 2. 原始代码:执行
    blk();
    // 转换之后的代码
    (blk->FuncPtr)(blk);
    
    /* 解释
    取去 blk 中的函数指针 FunPtr。
    向 FunPtr 中传入参数 blk。
    即 __main_block_func_0(blk);
    */
    
    // 最终执行的也就是下面这个函数。 __cself 可以看成是 ObjC 中的 self,用于指向自身。
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("Block\n");
    }
    
    /* 关于 isa = &_NSConcreteStackBlock
    impl 这个结构体中有个成员变量 isa,一看到这个让我联想到 runtime 中也有 isa。
    在对象的结构体和类的结构体中都有这个 isa 结构体指针。
    说白了它就是一级一级地指向比它大一层级的那个结构体,这样就包含了它上一层级的所有信息。有点像继承的关系。
    最终 isa 指向根元类(root metaClass),根元类的 isa 指向它本身。
    
    从 isa 中就能理解,isa = &_NSConcreteStackBlock 其实就是将 Block 作为 ObjC 的对象来处理,将 Block 对象的类信息放置在 _NSConcreteStackBlock 中。
    所以,我们可以把 Block 看成是一个对象。
    */
    

    截获局部变量

    // 变量 a 不参与 Block
    int a = 20;
    int myVal = 10;
    const char *fmt = "myVal = %d\n";
    void (^blk)(void) = ^{
        printf(fmt, myVal);
    };
    myVal = 2;
    fmt = "These values were changed. val = %d\n";
    // 打印:myVal = 10
    blk();
    

    将上述代码通过 clang -rewrite-objc main.m 转成 C++ 代码,简化之后如下(大致步骤与前面部分类似,不再详细解释):

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      /* 新增加了 fmt,myVal变量。
      该结构体中只会追加在 Block 中使用到的局部变量。
      如果变量(如 int a)在 Block 中没有使用到,是不会追加到该结构体中的。
      */
      const char *fmt;
      int myVal;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _myVal, int flags=0) : fmt(_fmt), myVal(_myVal) {
        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 myVal = __cself->myVal; // bound by copy
      printf(fmt, myVal);
    }
    
    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 argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 20;
        int myVal = 10;
        const char *fmt = "myVal = %d\n";
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, myVal));
        // 简化
        /* 最后两个参数传入的是fmt, myVal。所以在这之后即使改变了 fmt 和 myVal 的值,也不会影响 Block 内部的变量。*/
        void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, myVal);
        myVal = 2;
        fmt = "These values were changed. val = %d\n";
    
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        // 简化
        (blk->FuncPtr)(blk);
        }
        return 0;
    }
    

    __Block 说明符

    要想改变 block 中的外部值,有两种方法。

    方法一:使用静态变量、静态全局变量、全局变量

    int global_val = 1;
    static int static_global_val = 2;
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            static int static_val = 3;
            void(^blk)(void) = ^{
                global_val = 11;
                static_global_val = 22;
                static_val = 33;
              // 11, 22, 33
                printf("%d, %d, %d\n", global_val, static_global_val, static_val);
            };
            // 在外部改变静态变量、全局变量、静态全局变量的值后,调用 blk(),打印的是改变之后的值。      
            // global_val = 11;
            // static_global_val = 22;
            // static_val = 33;           
            blk();
        }
        return 0;
    }       
    

    将上述代码通过 clang -rewrite-objc main.m 转成 C++ 代码,简化之后如下

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    int global_val = 1;
    static int static_global_val = 2;
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      // 静态变量被block截获,成为 __main_block_impl_0 结构体的成员变量。
      int *static_val;
      // 将静态变量 static_val 的指针传递给 __main_block_impl_0 结构体的构造函数并保存。
      __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
        /*
        从这里可知,转换后对静态全局变量 static_global_val 和全局变量 global_val 的访问与转换前完全       一样。但是对于静态变量 static_val,它是通过 static_val 的指针进行访问的。
      */ 
      global_val = 11;
      static_global_val = 22;
      (*static_val) = 33;
      printf("%d, %d, %d\n", global_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(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            static int static_val = 3;
    
            void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
    
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
    
        return 0;
    }
    

    Q:静态变量的这种通过指针访问变量的方法似乎也适用于局部变量,但是我们为什么没有这么做呢?
    A:实际上,在由 Block 语法生成的代码中,可以存有超过其变量作用域的被截获对象的局部变量。变量的作用域结束的同时,原来的局部变量被废弃,因此 Block 内部(就是 Block 的 {...} 内部)超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的局部变量。

    方法二:使用 __block 说明符

    更准确的表述方式为“__block 存储域类说明符”。

    存储域类说明符:告知编译器其声明的对象或函数的持续时间和可见性,以及应将该对象存储到的位置。(摘自 MSDN

    __block 说明符类似于 C 语言中的 staticautoregister 说明符,它们用于指定将变量值设置到那个存储域中。例如,auto 表示作为局部变量存储在栈中,static 表示作为静态变量存储在数据区中。

    __block int val = 10;
    void(^blk)(void) = ^(void) {
        val = 1;
    };        
    

    将上述代码通过 clang -rewrite-objc main.m 转成 C++ 代码,简化之后如下

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    struct __Block_byref_val_0 {
      void *__isa;
    __Block_byref_val_0 *__forwarding;
     int __flags;
     int __size;
     int val;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_val_0 *val; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
      
      (val->__forwarding->val) = 1;
    }
    
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
      _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
    }
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
      
            __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,         (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
                                  
            void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
                                
            ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
        }
    
        return 0;
    }
    

    步骤讲解

    1. __block int val = 10; 中的 __block 被转成了一个结构体 __Block_byref_val_0 ,该结构体构造是:

      struct __Block_byref_val_0 {
        void *__isa;
        __Block_byref_val_0 *__forwarding;
        int __flags;
        int __size;
        int val;
      };
      

      从转换后的源码看出,传入该结构体的参数是:

      __Block_byref_val_0 val = {
          0,
          &val,
          0,
          sizeof(__Block_byref_val_0),
          10
      };
      

      从该结构体中可以看出,编译器将截获到的局部变量 val 添加到了结构体,作为它的一个成员变量。里面还是一个 __forwarding 指针,它的类型也是这个结构体类型,其实这个指针就是指向该实例自身的指针。

    2. ^(void){ val = 1; } 转换成:

      static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        __Block_byref_val_0 *val = __cself->val; // bound by ref
        
        (val->__forwarding->val) = 1;
      }
      

      __cself->val 表示 Block 的 __main_block_impl_0 持有指向 __block 变量的 __Block_byref_val_0 结构体实例的指针。

      然后通过该结构体实例指针的成员变量 __forwarding 访问自身的 (int 型)val 变量,将 1 赋值给该 val 变量。

    Block 存储域

    Block 与 __block 变量的实质

    名称 实质
    Block 栈上 Block 的结构体实例
    __block 变量 栈上 __block 变量的结构体实例

    注:Block 的结构体就是 __main_block_impl_0

    Block的类型: _NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock

    设置对象的存储域
    _NSConcreteStackBlock
    _NSConcreteGlobalBlock 程序的数据区(.data 区)
    _NSConcreteMallocBlock

    出现 _NSConcreteGlobalBlock 的情形

    • 创建全局变量形式的 Block
    • Block 语法表达式中没有使用局部变量(虽然通过 clang -rewrite-objc main.m 转成的 C++ 中是 _NSConcreteStackBlock,但是通过断点打印可以发现它是 _NSConcreteGlobalBlock )

    在以上情况下,Block 配置在程序的数据区中。

    注:因为 Global 形式的 Block 不依赖于执行时的状态,所以整个程序中只需一个实例。

    将 Block 配置在堆上的 _NSConcreteMallocBlock 类何时使用呢

    问题:配置在全局变量上的 Block,从变量作用域外也可以通过指针安全地使用。但是设置在栈上的 Block,如果其所属的变量作用域结束,该 Block 就被废弃。由于 __block 变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该 __block 变量也会被废弃。那么,如何在超出作用域后,依然能使用 Block 呢?

    解决办法:Blocks 提供了将 block 和 __block 变量从栈上复制到堆上的方法来解决这个问题。这样即使 Block 的变量作用域结束,堆上的 Block 还可以继续存在。

    复制到堆上的 Block 将 _NSConcreteMallocBlock 类对象写入 Block 结构体实例的成员变量 isa 中。

    impl.isa = &_NSConcreteMallocBlock;

    而 __block 变量结构体的成员变量 __forwarding 可以实现无论 __block 变量在栈上还是在堆上,都能够正确地访问 __block 变量。(这就是 __forwarding 存在的意义 )

    将 Block 作为函数返回值返回时,编译器会自动生成复制到堆上的代码。

    typedef int (^blk_t)(int);
    blk_t func(int rate) {
        return ^(int count){return rate * count;};
    }
    
    /* 编译器转换为以下代码(ARC下) */
    blk_t func2(int rate) {
        // 因为是 ARC 下,所有 blk_t tmp 与 blk_t __strong tmp 相同。
        blk_t tmp = &__func_block_impl_0(__func_block_func_0,
                                         &__func_block_desc_0_DATA,
                                         rate);
        // 通过 objc4 运行时库的 runtime/objc-arr.mm 可知,objc_retainBlock 函数实际就是 _Block_copy 函数。
        /* _Block_copy 将栈上的 Block 复制到堆上。
           复制后,将堆上的地址作为指针赋值给变量 tmp。
         */
        tmp = objc_retainBlock(tmp);
        /* 将堆上的 Block 作为 Objc 对象注册到 autoreleasepool 中,然后返回该对象。
         */
        return objc_autoreleaseReturnValue(tmp);
    }
    

    编译器不能进行判断的情况:

    • 向方法或函数的参数中传递 Block 时

    但如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了。

    id getBlockArray(void) {
        int val = 10;
    //    id result = [NSArray arrayWithObjects:^{NSLog(@"blk0:%d", val);},
    //                 ^{NSLog(@"blk1:%d", val);}, nil];
      
      // 将 Block 从栈复制到了堆上,不会报错。如果没调用 copy 方法则会在超出作用域后直接释放。
        return [[NSArray alloc] initWithObjects:
                [^{NSLog(@"blk0:%d", val);} copy],
                [^{NSLog(@"blk1:%d", val);} copy],nil];
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            id obj = getBlockArray();
            void(^blk)(void) = obj[0];
            blk();
        }// 如果 getBlockArray 函数中返回的数组中如果 block 元素没有调用 copy 方法,则超出这个作用域后就会被释放,运行时直接报错。
        return 0;
    }
    
    

    以下方法或函数不用手动复制:

    • Cocoa 框架的方法且方法名中含有 usingBlock 等
    • GCD 的API
    Block 的类 副本源的配置存储域 复制效果
    _NSConcreteStackBlock 从栈复制到堆
    _NSConcreteGlobalBlock 程序的数据区(.data 区) 什么也不做
    _NSConcreteMallocBlock 引用计数增加,不会生成新的对象

    不管 Block 配置在何处,用 copy 方法复制都不会引起任何问题。在不确定时调用 copy 方法即可。

    Q:但是在 ARC 下不能显式地 release,那么多次调用 copy 方法进行复制有没有问题呢?
    A:没有任何问题。中间过程中 block 对象的引用计数不断增减,但是最终还是为 1。

    blk = [[[[blk copy] copy] copy] copy];
    
    /* 源码可解释为 */
    {
      // 将 blk 从栈复制到堆上,赋值给变量 tmp。此时tmp 强引用着 堆上的 block 对象。
      blk_t tmp = [blk copy];
      // 将 tmp 指向堆上的 block 赋值给 blk,此时 tmp 和 blk 都强引用 block 对象。
      blk = tmp;
    } // 超出作用域后,tmp 被废弃,引用计数减 1 ,此时只有 blk 强引用这 block 对象。
    {
      // 将堆上的 block 对象复制一份,并赋值给 tmp。此时 block 的引用计数为 2。
      blk_t tmp = [blk copy];
      // tmp 赋值给 blk,那么原先 blk 指向的 block 对象强引用失效。此时引用计数还是 2(相当于 2 - 1 + 1)。
      blk = tmp;
    } // 超出作用域,tmp 强引用失效,引用计数减 1。此时 block 的引用计数为 1。
    
    /* 下面的分析相同,最终 block 的引用计数始终为 1 */
    {
      blk_t tmp = [blk copy];
      blk = tmp;
    }
    {
      blk_t tmp = [blk copy];
      blk = tmp;
    }
    
    

    __block 变量存储域

    使用 __block 变量的 Block 从栈复制到堆上时,对 __block 变量也会产生影响。

    __block 变量的配置存储域 Block 从栈复制到堆上时的影响
    __block 从栈复制到堆上,并被 Block 持有
    被 Block 持有

    若在一个 Block 中使用 __block 变量,则当该 Block 从栈复制到堆上时,使用的 __block 也会被从栈复制到堆上。此时,Block 持有 __block 变量,__block 变量的引用计数加 1。即使在该 Block 已经被复制到堆上的情形下,复制 Block(也就是[block copy]) 也对所使用的 __block 变量也有任何影响。

    在任何一个 Block 从栈复制到堆上时, __block 变量也会一并从栈复制到堆并被该 Block 所持有。

    当 Block 对象被废弃时,那么它所持有的 __block 变量也就被释放了。这样的思考方式与 ObjC 的引用计数式内存管理完全相同。

    从上就能理解 __block 的结构体变量中成员变量 __forwarding 的作用了。当在栈上时,__forwarding 指向 __block 自己本身,当 __block 从栈复制到堆上时,栈中的 __forwarding 开始指向复制到堆上的 __block 结构体变量。而堆上的 __forwarding 指向堆上的 __block 自己本身。

    通过 __forwarding,无论是在 Block 语法中、Block 语法外使用 __block 变量,还是 __block 变量配置在栈上或堆上,都可以顺利地访问同一个 __block 变量。

    Block从栈复制到堆上.jpeg

    截获对象

    typedef void (^blk_t)(id object);
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            blk_t blk;        
            {
                // 调用了 copy 方法
                id array = [[NSMutableArray alloc] init];
                blk = [^(id object) {
                    [array addObject:object];
                } copy];
            }
            blk([NSObject new]);
            blk([NSObject new]);
            blk([NSObject new]);
        }
        return 0;
    }
    

    将上述代码通过 clang -rewrite-objc main.m 转成 C++ 代码,简化之后如下

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    
    typedef void (*blk_t)(id object);
    
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      id array;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself, id object) {
      id array = __cself->array; // bound by copy
    
                    ((void (*)(id, SEL, ObjectType))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)object);
    }
    
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    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(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            blk_t blk;
    
            {
    
                id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
                blk = (blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));
            }
            ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")));
            ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")));
            ((void (*)(__block_impl *, id))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new")));
        }
        return 0;
    }
    

    讲解

    有没有发现,转换之后的代码和上一节中方法二“__block 说明符”中转换的 C++ 代码构造非常类似,去掉 __block 这一块内容后,逻辑都是一样的。值得注意的是里面的 __main_block_desc_0 结构体中的成员变量 copydispose,以及在构造函数中赋值给它们的 __main_block_copy_0 函数 和 __main_block_dispose_0 函数。

    这两个函数在内部分别调用 _Block_object_assign 函数 和 _Block_object_dispose 函数。

    _Block_object_assign 函数调用相当于 retain 实例方法的函数,将对象赋值在对象类型的结构体成员变量中。

    _Block_object_dispose 函数调用相当于 release 实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。

    这些函数的目的就是为了更方便的管理对象的引用计数问题,如对象的强引用(__strong)与弱引用(__weak)。但是这些函数的最开始的调用位置是在 __main_block_desc_0 的 成员变量 copy 和 dispose 中,在转换后的源代码中,这些函数包括使用指针全都没有被调用。那么这些函数是从哪调用呢?

    在 Block 从栈复制到堆时以及堆上的 Block 被废弃时会调用这些函数。

    Q:为什么 __main_block_impl_0 结构体中可以包含 id array 成员变量 ?
    A:在 ObjC 中,C 语言结构体不能含有 __strong 修饰符的变量。因为编译器不知道应何时进行 C 语言结构体的初始化和废弃操作,不能很好地管理内存。但是 ObjC 的运行时库能够准确地把握 Block 从栈复制到堆上的 Block 被废弃的时机。因此 Block 中即使含有 __strong 或 __weak 修饰的变量,也能恰当地进行初始化和废弃。

    调用 copy 函数和 dispose 函数的时机

    函数 调用时机
    copy 函数 栈上的 Block 赋值到堆上时
    dispose 函数 堆上的 Block 被废弃时

    那么什么时候栈上的 Block 会复制到堆呢?

    • 调用 Block 的 copy 实例方法时
    • Block 作为函数返回值时
    • 将 Block 赋值给附有 __strong 修饰符 id 类型的类或 Block 类型成员变量时
    • 在方法名中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时

    截获对象时和使用 __block 变量时的不同

    __block 变量 对象
    BLOCK_FIELD_IS_BYREF BLOCK_FIELD_IS_OBJECT

    通过 BLOCK_FIELD_IS_OBJECT 和 BLOCK_FIELD_IS_BYREF 参数,区分 copy 函数 和 dispose 函数的对象类型是对象还是 __block 变量。

    但是与 copy 函数持有截获的对象、dispose 函数释放截获的对象相同,copy 函数持有所使用的 __block 变量,dispose 函数释放所使用的 __block 变量。

    由此可知,Block 中使用的赋值给附有 __strong 修饰符的局部变量的对象(这里是 block 内部的 array 对象)和复制到堆上的 __block 变量由于被堆上的 Block 所持有,因为可超出其变量作用域而存在。

    如果没有调用 copy 方法,执行该源代码之后,程序会强制结束。

    typedef void (^blk_t)(id object);
    
    blk_t blk;
    {
      id array = [[NSMutableArray alloc] init];
      /* 没有调用 copy 方法 */
                blk = ^(id object) {
                    [array addObject:object];
                    NSLog(@"array count = %ld", [array count]);
                };
    }
    
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    

    因为只有调用 _Block_copy 函数(或者 copy 方法)才能持有截获的附有 __strong 修饰符的对象的局部变量,所以像上面源码那样不调用 _Block_copy 函数的情况下,即使截获了对象,它也会随着变量作用域的结束而被废弃。

    因此,Block 中使用对象类型的局部变量时,除以下情形外,推荐调用 Block 的copy 实例方法。

    • Block 最为函数返回值返回时
    • 将 Block 赋值给类的 __strong 修饰符的 id 类型或者 Block 类型成员变量时
    • 向方法中含有 usingBlock 的 Cocoa 框架方法或 GCD 的 API 中传递 Block 时

    __block 变量和对象

    __block id objc = [NSObject new];       
    

    将上述代码通过 clang -rewrite-objc main.m 转成 C++ 代码,简化之后如下

    typedef void (*blk_t)(id object);
    
    struct __Block_byref_obj_0 {
      void *__isa;
      __Block_byref_obj_0 *__forwarding;
      int __flags;
      int __size;
      void (*__Block_byref_id_object_copy)(void*, void*);
      void (*__Block_byref_id_object_dispose)(void*);
      id obj;
    };
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {(void*)0,(__Block_byref_obj_0 *)&obj, 33554432, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};
        }
        return 0;
    }
    

    上面转换后的代码中,__block 转换后的结构体中同样包含 _Block_object_assign 和 _Block_object_dispose 函数。其实 __block 变量当被 __strong 修饰的时候,当 __block 变量从栈复制到堆上时,_Block_object_assign 持有赋值给 __block 变量的对象,当 __block 变量被废弃时,使用 _Block_object_dispose 废弃该对象。

    当使用 __block 和 __weak 同时修饰一个对象时:

    typedef void (^blk_t)(id object);
    
    blk_t blk;
    {
      id array = [[NSMutableArray alloc] init];
      // 或者 __block id __weak array2 = array;
      id __weak array2 = array;
      blk = [^(id object) {
          [array2 addObject:object];
          NSLog(@"array2 count = %ld", [array2 count]);
      } copy];
    }
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    
    /*结果 array2 count = 0 */
    /*
        这是因为 array 变量在作用域结束的同时被释放、废弃,nil 被赋值给 __weak 修饰的 array2。即使附加了 __block 说明符也是一样的结果。
    */
    

    因为没有设定 __autoreleasing 修饰符与 Block 同时使用的方法,所以没有必要使用 __autoreleasing 修饰符。另外,它与 __block 说明符同时使用会产生编译错误。

    // 编译错误 __block 和 __autoreleasing 不能同时使用。
    // __block variables cannot have __autoreleasing ownership.
    __block id __autoreleasing obj = [NSObject new];
    

    Block 循环引用

    Block 的循环引用问题是一个常见的问题,主要的解决办法就是通过弱引用某一方来打破引用循环,这需要在编程的时候多加留意,由于涉及到实际应用问题,这里不做分析。

    copy / release

    在 MRC 下,需要手动将 Block 从栈复制到堆上。当不再使用时,也要手动释放复制的 Block。用 copy 实例方法来复制,用 release 实例方法来释放。

    /* MRC 下 */
    void (^blk_on_heap)(void) = [blk_on_stack copy];
    [blk_on_heap release];
    

    当 Block 在堆上时,可以调用 retain 实例方法持有 Block。但是需要注意的是,retain 方法只适用于堆上的 Block,对栈上的 Block,retain 方法不起任何作用,需要用 copy 实例方法来持有。

    在 C 语言中对应的 copy / release 方法是 Block_copy 函数Block_release 函数

    相关文章

      网友评论

        本文标题:《Objective-C 高级编程》Blocks 笔记摘要

        本文链接:https://www.haomeiwen.com/subject/gxteaxtx.html