程序员iOS DeveloperiOS点点滴滴

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

2018-01-25  本文已影响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 配置在程序的数据区中。

注:因为 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);
}

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

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

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

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

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 变量时的不同

__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 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 函数

上一篇下一篇

猜你喜欢

热点阅读