《Objective-C 高级编程》Blocks 笔记摘要
温馨说明:
- 下文出现的 Block(首字母大写)指的是形如
^(参数){执行的任务}
的对象,它是一个结构体对象。 - 下文的局部变量和书中提到的自动变量是一个概念。由于个人习惯,所以称作为局部变量。
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 语言中的 static
、auto
和 register
说明符,它们用于指定将变量值设置到那个存储域中。例如,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;
}
步骤讲解
-
__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 指针,它的类型也是这个结构体类型,其实这个指针就是指向该实例自身的指针。
-
^(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 变量。
截获对象
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 结构体中的成员变量 copy
和 dispose
,以及在构造函数中赋值给它们的 __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 函数。