Blocks

什么是Blocks

BlockC 语言的扩充功能:带有自动变量的匿名函数。

  • 自动变量: 局部变量
  • 匿名函数: 不带有名称的函数

C 语言的标准库中不允许存在这样的函数。

函数调用方式

  • 使用函数名称调用函数
  • 使用函数指针调用函数
// 函数声明
int func(int count);

// 使用函数名称调用函数
int result = func(10);

// 使用函数指针调用函数
int (*funcptr)(int) = &func;
int result = (*funcptr)(10);
  • & 取地址
  • * 取值

C 语言的函数中可能使用的变量:

  • 自动变量(局部变量)
  • 函数的参数
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

其中,在函数的多次调用之间能够传递值的变量有:

  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

C++ 和 Objective-C 使用类可以保持变量值且能够多次持有该变量自身。

但是声明和实现 C++ 和 Objective-C 的类增加了代码长度,这事就需要使用 Blocks了; Blocks 提供了类似由 c++ 和 Objective-C 类生成实例或对象来保持变量值的方法。

Blocks 模式

Block 语法

完整形式的 Block 语法与一般的 C 语言函数定义相比,仅有两点不同:

  • 没有函数名
  • 带有 ^

C 语言函数定义如下:

返回值类型 函数名称 参数列表 表达式

Block 语法格式如下:

^ 返回值类型 参数列表 表达式

省略返回值后的格式如下:

^ 参数列表 表达式

同时省略返回值和参数列表的格式如下:

^ 表达式

Block 类型变量

在定义 C 语言函数时,可以将所定义的函数的地址赋值给函数指针类型变量中:

int func(int count) {
    return count + 1;
}

int (*funcptr)(int) = &func;

这样一来,函数 func 的地址就能赋值给函数指针类型变量 funcptr 中了。

同样的,在 Block 语法下,可将 Block 语法赋值给声明为 Block 类型的变量中。

int (^blk)(int);

声明 Block 类型变量仅仅是将声明函数指针类型变量的 * 变为 ^

Block 类型的变量的用途:

  • 自动变量
  • 函数参数
  • 静态变量
  • 静态全局变量
  • 全局变量

使用 Block 语法将 Block 赋值为 Block 类型变量示例如下:

int (^blk)(int) = ^(int count) { return count + 1; };

因为与通常的变量相同,所以可以由 Block 类型变量向 Block 类型变量赋值:

int (^blk1)(int) = blk;

int (^blk2)(int);
blk2 = blk;

在函数参数中使用 Block 类型变量向函数传递 Block:

void func(int (^blk)(int)) {
}

将 Block 作为函数的返回值:

int (^func())(int) {
    return ^(int count) { return count + 1; };
}

由上可知,在函数参数和返回值中使用的 Block 类型变量时,记述方式极为复杂。 这时,可以像使用函数指针类型时那样,使用 typedef 来解决该问题:

typedef int (^blk_t)(int);

// 作为函数参数
void func1(blk_t blk) {
}

// 作为函数返回值
blk_t func2() {
    return ^(int count) { return count + 1; };
}

截获自动变量值

“带有自动变量值”在 Blocks 中表现为“截获自动变量值”。

void autoGetValueForBlock() {
    
    int dmy = 256;
    int val = 10;
    
    const char *fmt = "val = %d\n";
    
    void (^blk)(void) = ^ { printf(fmt, val); } ;
    
    val = 2;
    
    fmt = "These values were changed. val = %d\n";
    
    blk();
}

Block 语法的表达式使用的它之前声明的自动变量 fmtval。 Blocks 中,Block 表达式所使用的自动变量的值,即保存该自动变量的瞬间值。 因为 Block 表达式保持了自动变量的值,所以在执行 Block 语法后,即使改写 Block 中使用的自动变量的值也不会影响执行时自动变量的值。

执行结果:

val = 10

__block 说明符

自动变量值截获只能保存执行 Block 语法瞬间的值。 保存后就不能改写该值。

尝试改写截获的自动变量的值:

int val = 0;
    
void (^blk)(void) = ^{ val = 1; };
    
blk();
    
printf("val = %d\n", val);

该代码会产生编译错误:

Variable is not assignable (missing __block type specifier)

若想在 Block 语法的表达式中将值赋给在 Block 语法外声明的自动变量, 需要在该自动变量上附加 __block 说明符。

__block int val = 0;
    
void (^blk)(void) = ^{ val = 1; };
    
blk();
    
printf("val = %d\n", val);

执行结果:

val = 1

截获的自动变量

将值赋值给 Block 中截获的自动变量,会产生编译错误;

对于截获的 Objective-C 对象,调用变更对象的方法不会产生编译错误。

NSMutableArray *array = [[NSMutableArray alloc] init];
   
void (^blk)(void) = ^ {
  id obj = [[NSObject alloc] init];
  [array addObject:obj];
};

该代码中截获的变量值为 NSMutableArray 类的对象。如果用 C 语言来描述,即是截获 NSMutableArray 类对象用的结构体实例指针。

虽然赋值给截获的自动变量 array 的操作会产生编译错误,但使用截获的值却不会有任何问题。

Blocks 的实现

Block 的实质

Block 语法看上去好像很特别,但它实际上是作为极普通的 C 语言源代码来处理的。 通过支持 Block 的编译器,含有 Block 语法的源代码转换为一般 C 语言编译器能够处理的源代码,并作为极为普通的 C 语言源代码被编译。

clang(LLVM 编译器)具有转换为我们可读源代码的功能。 通过 -rewrite-objc 选项就能将含有 Block 语法的源代码变换为 C++ 的源代码。

说是 C++,其实也仅是使用了 struct 结构,其本质是 C 语言源代码。

clang -rewrite-objc 源代码文件名

待转换的 Block 源代码:

int main(int argc, const char * argv[]) {
    void (^blk)(void) = ^{ printf("Block\n"); };
    
    blk();
    
    return 0;
}

转换后的代码(部分):

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 printf("Block\n"); }

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[]) {
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

所谓 Block 就是 Objective-C 对象

截获自动变量值

待转换的 Block 源代码:

int main(int argc, const char * argv[]) {
    int dmy = 256;
    int val = 10;
    
    const char *fmt = "val = %d\n";
    
    void (^blk)(void) = ^ { printf(fmt, val); } ;
    
    blk();
    return 0;
}

转换后的代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  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) {
    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 val = __cself->val; // bound by copy
 printf(fmt, 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[]) {
    int dmy = 256;
    int val = 10;

    const char *fmt = "val = %d\n";

    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val)) ;

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

所谓 “截获自动变量值” 意味着在执行 Block 语法时, Block 语法表达式所使用的自动变量值被保存到 Block 的结构体实例(即 Block 自身)中

__block 说明符

__block 存储域类说明符 (__block storage-class-specifier)

C 语言中有以下存储域类说明符:

  • typedef
  • extern
  • static
  • auto
  • register

__block 说明符类似于 staticautoregister 说明符,它们用于指定将变量值设置到哪个存储域中。 例如: auto 表示作为自动变量存储在栈中, static 表示作为静态变量存储在数据区中。

待转换的 Block 源代码:

int main(int argc, const char * argv[]) {
    __block int val = 0;
        
    void (^blk)(void) = ^{ val = 1; };
        
    blk();
        
    printf("val = %d\n", val);
    return 0;
}

转换后的代码:

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[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};

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

    printf("val = %d\n", (val.__forwarding->val));
    return 0;
}

__block 变量也同 Block 一样变成 __Block_byref_val_0 结构体类型的自动变量,即栈上生成的 __Block_byref_val_0 结构体实例。

__block 变量赋值原理:

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

__Block_byref_val_0 结构体实例变量 __forwarding 持有指向该实例自身的指针。 通过成员变量 __forwarding 访问成员变量 val

Block 存储域

  • Block 转换为 Block 的结构体类型的自动变量
  • __block 变量转换为 __block 变量的结构体类型的自动变量

所谓结构体类型的自动变量,即栈上生成的该结构体的实例。

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

Block 也是 Objective-C 的对象。将 Block 当作 Objective-C 对象来看时,该 Block 的类为 _NSConcreteStakBlock

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

应用程序的内存分配:

 ------------------
|                  |
|    (程序区域)    |
|	   .text       |
|                  |
|------------------|
|                  |
|	 (数据区域)    |
|	   .data       |
|                  |
|------------------|
|                  |
|	   (堆)       | 
|                  |
|------------------|
|                  |
|	   (栈)       | 
|                  |
 ------------------

在记述全局变量的地方使用 Block 语法时,生成的 Block 为 _NSConcreteGlobalBlock 类对象。 因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此,Block 用结构体实例的内容不依赖与执行时的状态,所以在整个程序中只需一个实例。 因此将 Block 用结构体实例设置在与全局变量相同的数据区域中即可。

只要 Block 不截获自动变量,就可以将 Block 用结构体实例设置在程序的数据区域

配置在全局变量上的 Block, 从变量作用域外也可以通过指针安全地使用。 但设置在栈上的 Block, 如果其所属的变量作用域结束,该 Block 就被废弃。 由于 __block 变量也配置在栈上,同样地,如果其所属的变量作用域结束,则该 __block 变量也会被放弃。

Blocks 提供了将 Block 和 __block 变量从栈上复制到堆上的方法来解决这个问题

将配置在栈上的 Block 复制到堆上,这样即使 Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在。

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

imp.isa = &_NSConcreteMallocBlock;

__block 变量用结构体成员变量 __forwarding 可以实现 无论 __block 变量配置在栈上还是堆上时都能够正确地访问 __block 变量。

当 ARC 有效时,大多数情况下编译器会恰当的进行判断,自动生成将 Block 从栈上复制到堆上的代码。 在此之外的情况下,我们可以使用 copy实例方法,手动将 Block 从栈上复制到堆上。

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

对于 Block 语法可直接调用 copy 方法, 当然对于 Block 类型变量也可以调用 copy 方法:

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

Block 类调用 copy 所产生的效果:

Block 的类 副本源的配置存储域 复制效果
_NSConcreteStakBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 生么也不做
_NSConcreteMallocBlock 引用计数增加

多次调用 copy 方法也不会存在问题

__block 变量存储域

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

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

在多个 Block 中使用 __block 变量时,因为最先会将所有的 Block 配置在栈上,所以 __block 变量也会配置在栈上。 在任何一个 Block 从栈复制到堆时,__block 变量也会一并从栈复制到堆并被该 Block 所持有。 当剩下的 Block 从栈复制到堆时,被复制的 Block 持有 __block 变量,并增加 __block 变量的引用计数。

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

截获对象

为了在附有 __strong__weak 修饰符的变量,也可以恰当的进行初始化和废弃。为此需要在 __main_block_desc_0 结构体中增加成员变量 copydispose,以及作为指针赋值给该成员变量的 __main_block_copy_0__main_block_dispose_0 函数。

待转换的 Block 源代码:

blk_t blk;
    
{
   NSMutableArray *array = [[NSMutableArray alloc] init];
   blk = [^(id obj) {
       [array addObject:obj];
       NSLog(@"array count = %zd", [array count]);
   } copy];
}
    
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

转换后的代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *array;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_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 obj) {
  NSMutableArray *array = __cself->array; // bound by copy

            ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_yd_m1j1pblx2kj64rpyd701tt3c0000gn_T_main_85c1bf_mi_0, ((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));
        }
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
};

__main_block_copy_0 函数使用 _Block_object_assign 函数将该对象类型对象赋值给 Block 用结构体的成员变量 array 中并持有该对象。

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

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*/);
}

__main_block_dispose_0 函数使用 _Block_object_dispose 函数,释放赋值在 Block 用结构体成员变量 array 中的对象。

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

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
   _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

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

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

栈上的 Block 复制到堆上的情况:

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

Block 循环引用

在 Block 中使用附有 __strong 修饰符的对象类型自动变量,当 Block 从栈复制到堆时,该对象为 Block 所持有。这样容易引起循环引用。

typedef void(^blk_t)(void);

@interface MyObject : NSObject {
    blk_t blk_;
}

@end

@implementation MyObject

- (instancetype)init {
    self = [super init];
    
    blk_ = ^{ NSLog(@"self = %@", self); };
    
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}

@end

int main(int argc, const char * argv[]) {
     
    id o = [[MyObject alloc] init];
    
    NSLog(@"%@", o);
    
    return 0;
}

MyObject 类对象的 Block 类型成员变量 blk_ 持有赋值为 Block 的强引用。init 实例方法中执行的 Block 语法使用附有 __strong 修饰符的 id 类型变量 self。并且由于 Block 语法赋值在了成员变量 blk_,因此通过 Block 语法生成在栈上的 Block 此时右栈复制到堆,并持有所使用的的 self。 self 持有 Block,Block 持有 self,因此造成循环引用。

Loading Disqus comments...
Table of Contents