这本书作者几乎通篇都在用 C、C++ 语言分析讲解 Block 的实现,初次看真的很吃力。这里推荐一篇文章:《Objective-C 高级编程》干货三部曲(二):Blocks篇。总结的非常精简到位。这篇博客作者有三篇文章分别是:
C 语言相关
Objective-C 转 C++ 的方法
Block 是带有自动变量的匿名函数,实质上就是对 C 语言的扩充,最终通过支持 C 语言的编辑器,将目标代码转化为 C 语言代码被编译,可通过下面命令行转化:
clang -rewrite-objc 源代码文件名
例如:
- 创建 OC 源文件 block.m 并编辑代码。
- 打开终端,cd 到 block.m 所在的文件夹。
- 输入
clang -rewrite-objc block.m
,就会在当前文件夹生成 block.cpp 文件。
C 语言几种变量特点
C 语言函数可能使用的变量:
- 自动变量(局部变量)
- 函数的参数
- 静态变量(局部静态变量)
- 静态全局变量
- 全局变量
其中,在函数的多次调用之间能够传递值的变量有:
- 静态变量(局部静态变量)
- 静态全局变量
- 全局变量
Block 实质
先说结论:Block 即为 Objective-C 的对象。
Block 的构成
以下是 block 语法的 BN 范式:
Blokc_literal_expression ::= ^block_decl compound_statement_body
block_decl ::=
block_decl ::= parameter_list
block_decl ::= type_expression
如下所示:
^
返回值类型
参数列表
表达式
表层分析 Block 实质:它是一个类型
Block 是一种类型,一旦使用了 Block 就相当于生成了可以赋值给 Block 类型变量的类型。举个例子:
int (^blk)(int) = ^(int count) {
return count + 1;
};
- 等号左侧代码定义这个 Block 类型:它接受一个 int 参数,返回一个 int 值。
- 等号右侧代码表示这个 Block 的值:它是左侧定义的 Block 的一种实现。
如果我们项目中经常使用某个类型的 Block,可以通过 typedef
来抽象出这种类型的 Block,如下:
// 抽象成一种类型
typedef int (^AddOneBlock)(int count);
AddOneBlock block = ^(int count) {
// 具体实现代码
return count + 1;
};
这样一来,Block 的赋值和传递就和普通类型一样方便了。
深层分析 Block 的实质:它是 Objective-C 对象
Block 其实就是 Objective-C 对象。它的结构体内含有 ISA 指针。
下面将 Objective-C 的代码转化为 C++ 来看下实现,这里我把方法命名为 test:
#include <stdio.h>
int test()
{
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
C++ 核心代码,这里并不是全部代码,抽取了关键部分来说:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// block 结构体
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// Block 被调用的代码,也是Block被转化为 C 语言的代码
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
printf("Block\n");
}
// 方法的描述
static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
// 自己定义的 test 方法
int test()
{
void (*blk)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
先来看下 OC 中源代码 block 语法 ^{printf("Block\n")};
变化成 C++ 后:
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
printf("Block\n");
}
该函数的参数 __cself 相当于 C++ 实例方法中指向自身实例方法的 this,或是 OC 实例方法中指向对象自身的变量 self,即参数 __cself 为指向 Block 值的变量,也是 __test_block_impl_0 结构体的指针。
结合这个方法,可知 __test_block_impl_0 即为这个 Block 的结构体。也就是说 blk 这个 Block就是通过 __test_block_impl_0 构造出来的。
下面看下这个方法:
// block 结构体
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
// 构造函数
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,该结构体由三部分组成:
第一个成员变量是 impl,其 __block_impl 结构体声明为:
struct __block_impl {
void *isa; // 指针
int Flags; // 标志
int Reserved; // 今后版本升级所需要的区域
void *FuncPtr; // 函数指针
};
第二个成员变量是 Desc 指针,用来描述这个 block 的附加信息,__test_block_desc_0 结构体如下:
// 方法的描述
static struct __test_block_desc_0 {
size_t reserved; // 今后版本升级所需要的区域
size_t Block_size; // block 大小
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
第三部分是 __test_block_impl_0 结构体的构造函数:
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
在这个结构体的构造函数里,ISA 指针保持着所属类的结构体的实例指针。__test_block_impl_0 结构体相当于 Objective-C 类对象的结构体,这里 _NSConcreteStackBlock 相当于 block 结构体的实例。所以说 Block 的实质即为 Objective-C 的对象。
Objective-C 类和对象的实质
id 这一变量用于存储 Objective-C 对象。源码中面对 id 类型的声明为:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
id 为 objc_object 结构体的指针类型。我们再看下 Class:
/// usr/include/objc/objc.h
typedef struct objc_class *Class;
/// usr/include/objc/runtime.h
struct objc_class {
Class isa;
};
各类的结构体就是基于 objc_class 的结构体的 class_t 结构体。class_t 在 objc4 运行时库的 runtime/objc-runtime-new.h 中声明如下:
typedef struct class_t {
struct class_t *isa;
struct class_t *superclass;
Cache cache;
IMP *vtable;
class_rw_t *data;
} class_t;
这里要说一下,objc4-437 还有这段代码,但是最新的 objc4-756.2 中已经没有了。查看一个 NSObject 对象的 C++ 源码,依然可以找到:
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
借用书中的一张图,表达如下:
Block 截获自动变量和对象
Block 截获自动变量(局部变量)
使用 Block 时,不仅可以使用其内部参数,还能使用 Block 外部的局部或者全局变量。
当 Block 使用外部的局部变量时,这些变量就会被 Block 保存,即使在 Block 外部修改这些值,存在于 Block 内部的这些变量也不会被修改,如下:
int a = 10;
int b = 20;
void (^printNumbers)(void) = ^{
printf("a = %d\nb = %d\n", a, b);
};
printNumbers();
// a = 10 b = 20
a = 55;
b = 66;
printNumbers();
// a = 10 b = 20
如果想要在 Block 内部修改外部局部变量值,会编译错误,提示添加 __block。如果操作外部局部变量,则不会有这问题。如:
NSMutableArray *array = [[NSMutableArray alloc] init];
void (^printNumbers)(void) = ^{
[array addObject:@1];
NSLog(@"array1 = %@\n", array);
};
printNumbers();
NSLog(@"array2 = %@\n", array);
[array addObject:@2];
printNumbers();
// 输出:
array1 = [1]
array2 = [1]
arrar1 = [1, 2, 1]
如果 Block 内部执行 array = [NSMutableArray array];
则会编译错误,提示加上 __block。
根据以上例子,可以暂时总结三点:
- Block 可以截获局部变量。
- 修改 Block 外部的局部变量,Block 内部被截获的值不会改变。
- 直接修改 Block 内部的值,编译错误。但是可以操作局部变量。
下面通过 C++ 代码分析下为什么会出现这种现象, C 代码:
#include <stdio.h>
int main() {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^ {
printf(fmt, val);
};
val = 2;
fmt = "Value changed. Val = %d\n";
blk(); // 输出: val = 10
return 0;
}
通过 Clang 将其转化为 C++ 代码,主要代码如下:
// block 结构体
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;
}
};
/// block 函数体
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);
}
/// block 描述
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)};
/// 定义的 main 方法
int main() {
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));
val = 2;
fmt = "Value changed. Val = %d\n";
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
这里加一点个人看法,前面说过,Block 是带有自动变量的匿名函数,我个人认为可以从上面 C++ 代码来做验证,Block 被转化为 static void __main_block_func_0,即是一个函数。
单独看 block 的构成结构体 __main_block_impl_0
:
- 可以看到,在 block 内部使用的局部变量 val、fmt 作为成员变量被追加到 __main_block_impl_0 结构体中。而 block 中没有使用的局部变量不会追加,如 dmy。
- 初始化 block 结构体实例时,即
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
,截获了局部变量 val 和 fmt 来初始化 block 的结构体实例。
再看一下 block 函数体代码:
/// block 函数体
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);
}
可知, val 、fmt 都是从 cself 截获的,而 cself 是指向 block 的对象。所以也能说,val 和 fmt 是属于 block 的。而且从 Clang 生成的注释 bound by copy
,表示对这两个对象做了只读值拷贝,而不是指针传递。所以即使改变了 block 外部局部变量的值,block 内部拷贝的值,也不会发生改变。
不过可以使用 __block 修饰局部变量,来满足 block 内部修改的需求。后面会对 __block 做详细分析。
改变存储于特殊存储区域的变量
在 Block 内部,下列几种值是可以直接访问和修改的:
- 全局变量
- 静态全局变量
- 静态变量
下面通过源码来与自动变量对比来分析下:
#include <stdio.h>
int global_val = 1; // 全局变量
static int static_global_val = 2; // 全局静态变量
int main() {
// 局部静态变量
static int static_val = 3;
void (^blk)(void) = ^ {
printf("global_val = %d\nglobal_val = %d\nstatic_val = %d\n", global_val, static_global_val, static_val);
global_val = 11;
static_global_val = 12;
static_val = 13;
};
global_val = 21;
static_global_val = 22;
static_val = 23;
blk();
printf("global_val2 = %d\nglobal_val2 = %d\nstatic_val2 = %d\n", global_val, static_global_val, static_val);
return 0;
}
调用该方法,打印如下:
global_val = 21
global_val = 22
static_val = 23
global_val2 = 11
global_val2 = 12
static_val2 = 13
可知:
- 这三种类型的值,可以在 Block 内部修改,并且内外修改相互影响。
转化成 C++ 代码:
/// 定义变量
int global_val = 1;
static int static_global_val = 2;
/// block 结构体
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_vtypedefal(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/// block 函数体
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
printf("global_val = %d\nglobal_val = %d\nstatic_val = %d\n", global_val, static_global_val, (*static_val));
global_val = 11;
static_global_val = 12;
(*static_val) = 13;
}
// block 描述
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() {
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));
global_val = 21;
static_global_val = 22;
static_val = 23;
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
printf("global_val2 = %d\nglobal_val2 = %d\nstatic_val2 = %d\n", global_val, static_global_val, static_val);
return 0;
}
通过 __main_block_impl_0
结构体可以看到,全局变量、静态全局变量并没被截获到 block 内部,而局部静态变量 static_val 也是通过指针访问。所以他们在 block 内外的改变都是相互影响的,用的是同一份值。
Block 截获对象
下面看一下 Block 语法中使用 Array 对象的代码,Array已经超过其作用域:
void testBlock() {
typedef void(^blk_t)(id obj);
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
调用 testBlock() 方法,控制台打印:
ViewController.m:31 array count = 1
ViewController.m:31 array count = 2
ViewController.m:31 array count = 3
array 在超出其作用域后,并没有被彻底销毁,看一下其 C++ 代码实现:
struct __testBlock_block_impl_0 {
struct __block_impl impl;
struct __testBlock_block_desc_0* Desc;
id array; // 截获的对象
__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在 OC 中, C 语言的结构体不能含有 __strong 修饰符的变量,因为编译器不知道应该何时进行 C 语言结构体的初始化和废弃操作,不能很好地管理内存。
但是 OC 的运行时库能够准确的把握 Block 从栈复制到堆以及堆上的 Block 函数被废弃的时机。在实现上是通过 __testBlock_block_copy_0
和 __testBlock_block_dispose_0
两个函数进行的:
static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {
_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
其中,_Block_object_assign
函数相当于 retain 实例方法的函数,将对象赋值在对象类型的结构体成员变量中。 _Block_object_dispose
函数相当于调用 release 实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。
这两个函数在源码中并没有被调用,而是在 Block 从栈复制到堆上时以及堆上的 block 被废弃时会调用这些函数,如下表:调用 copy 函数 和 dispose 函数的时机
函数 | 调用时机 |
---|---|
copy 函数 | 栈上的 Block 复制到堆时 |
dispose 函数 | 堆上的 Block 被废弃时 |
什么时候栈上的 Block 会复制到堆上呢?
- 调用 Block 的 copy 实例方法时
- Block 作为函数的返回值时
- 将 Block 赋值给附有 __strong 的修饰符 id 类型的类,或 Block 类型成员变量时
- 在方法名中含有 usingBlock 的 Cocoa 框架方法或者 GCD 的 API 中传递 Block 时
上面四种情况,栈上的 Block 都会复制到堆上,但其实本质都可以归结为 block_copy 函数被调用时 Block 从栈复制到堆。
特殊说明:针对如下修改,书中说程序会强制结束:
void testBlock() {
typedef void(^blk_t)(id obj);
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
但是我测试时,调用 testBlock 函数依然输出:
ViewController.m:34 array count = 1
ViewController.m:34 array count = 2
ViewController.m:34 array count = 3
不确定是 Apple 之后做了修改,还是我不应该借用 OC 的机制这么写。
另外, __testBlock_block_copy_0
函数是否存在,与是否调用了 copy 实例方法并没有关系。
相对的,在释放复制到堆上的 Block 后,谁都不持有 Block 而被废弃时调用 dispose 函数,相当于对象的 dealloc 方法。比如在使用了 __weak 关键字时:
void testBlock() {
typedef void(^blk_t)(id obj);
blk_t blk;
{
id array0 = [[NSMutableArray alloc] init];
id __weak array = array0;
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
输出:
ViewController.m:34 array count = 0
ViewController.m:34 array count = 0
ViewController.m:34 array count = 0
这段代码无法通过 Clang 转化,会报错:
cannot create __weak reference in file using manual reference counting
__block 的实现原理
__block 修饰局部变量
__block 说明符
更精确的表达方式应该是 __block 存储域类说明符(__block storage-class-specifier)
。C 语言有如下存储域类说明符:
- typedef
- extern
- static
- auto
- register
__block 说明符类似 static、auto和 register 说明符,用于指定将变量值放到哪个存储域中。例如,auto 表示作为自动变量存储在栈中,static 表示作为静态变量存储在数据区中。给自动变量加上 __block 说明符后,就会改变这个自动变量的存储区域。
下面通过给局部变量添加 __block 关键字后效果来分析:
typedef void(^PrintNumbers)(void);
void lineBlock() {
__block int a = 10;
int b = 20;
PrintNumbers printBlock = ^() {
a -= 10;
printf("a1 = %d, b1 = %d\n", a, b);
};
printBlock();
a += 20;
b += 20;
printf("a2 = %d, b2 = %d\n", a, b);
printBlock();
}
输出:
a1 = 0, b1 = 20
a2 = 20, b2 = 40
a1 = 10, b1 = 20
附有 __block 修饰符的变量称之为 __block 变量,可以看到,__block 变量在 Block 内部可以被修改,且修改的值被同步到 Block 作用域外。
下面用 Clang 工具看一下 C++ 代码:
OC 代码:
void lineBlock() {
__block int valNumber= 10;
void (^blk)(void) = ^{
valNumber = 1;
};
}
C++ 代码:
struct __Block_byref_valNumber_0 {
void *__isa;
__Block_byref_valNumber_0 *__forwarding;
int __flags;
int __size;
int valNumber;
};
struct __lineBlock_block_impl_0 {
struct __block_impl impl;
struct __lineBlock_block_desc_0* Desc;
__Block_byref_valNumber_0 *valNumber; // by ref
__lineBlock_block_impl_0(void *fp, struct __lineBlock_block_desc_0 *desc, __Block_byref_valNumber_0 *_valNumber, int flags=0) : valNumber(_valNumber->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __lineBlock_block_func_0(struct __lineBlock_block_impl_0 *__cself) {
__Block_byref_valNumber_0 *valNumber = __cself->valNumber; // bound by ref
(valNumber->__forwarding->valNumber) = 1;
}
static void __lineBlock_block_copy_0(struct __lineBlock_block_impl_0*dst, struct __lineBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->valNumber, (void*)src->valNumber, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __lineBlock_block_dispose_0(struct __lineBlock_block_impl_0*src) {_Block_object_dispose((void*)src->valNumber, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __lineBlock_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __lineBlock_block_impl_0*, struct __lineBlock_block_impl_0*);
void (*dispose)(struct __lineBlock_block_impl_0*);
} __lineBlock_block_desc_0_DATA = { 0, sizeof(struct __lineBlock_block_impl_0), __lineBlock_block_copy_0, __lineBlock_block_dispose_0};
void lineBlock() {
__attribute__((__blocks__(byref))) __Block_byref_valNumber_0 valNumber = {(void*)0,(__Block_byref_valNumber_0 *)&valNumber, 0, sizeof(__Block_byref_valNumber_0), 10};
void (*blk)(void) = ((void (*)())&__lineBlock_block_impl_0((void *)__lineBlock_block_func_0, &__lineBlock_block_desc_0_DATA, (__Block_byref_valNumber_0 *)&valNumber, 570425344));
}
可以看到:
- 被 __block 修饰的自动变量,被转变为 __Block_byref_valNumber_0 结构体存在,即栈上生成 __Block_byref_valNumber_0 结构体实例。
- block 的构造结构体
__lineBlock_block_impl_0
中可以看到,通过 __block 变量的结构体指针访问该变量。 - block 函数实现方法
__lineBlock_block_func_0
中看到,block 内部通过valNumber->__forwarding->valNumbe
指针修改 __block 变量。
关于 __block 变量的结构体实现:
struct __Block_byref_valNumber_0 {
void *__isa;
__Block_byref_valNumber_0 *__forwarding;
int __flags;
int __size;
int valNumber;
};
- 结构体的成员变量 valNumber 持有原本变量的字面值。
- __forwarding: 结构体实例的成员变量 __forwarding 持有指向该实例自身的指针,可以通过成员变量 __forwarding 访问成员变量 valNumber,即
valNumber->__forwarding->valNumbe
。如下图:
而 __Block_byref_valNumber_0
结构体之所以不在 __lineBlock_block_impl_0
结构体内部定义,是为了在多个 block 中共用一个 __block 变量结构体。
另外,当 Block 从栈复制到堆时,被 Block 持有的 __block 变量也会复制到堆上,注意,这里会增加 __block 变量的引用计数。同时,如果 Block 被废弃,持有的 __block 变量也会被释放。
不管 __block 变量配置在栈上还是堆上,都能够正确的访问该变量。通过下面这个图可以更深刻的理解这句话:
__block 修饰对象
__block 说明符可以用来指定任何类型的自动变量,下面用来指定 id 类型的自动变量:
void testBlock() {
__block id obj = [[NSObject alloc] init];
void (^blk)(void) = ^{
obj = [NSObject new];
};
}
ARC 有效时,id 类型以及对象类型变量必定附加所有权修饰符,缺省为附有 __strong 修饰符的变量。改代码通过 clang 转换。 __block 变量的结构体如下:
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;
};
被 __strong 修饰的 id 类型或对象类型,生成了 copy 和 dispose 两个方法:
static void __testBlock_block_copy_0(struct __testBlock_block_impl_0*dst, struct __testBlock_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __testBlock_block_dispose_0(struct __testBlock_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}
- 如果 __block 对象变量从栈复制到堆,使用 _Block_object_assign 方法。
- 当堆上的 __block 对象变量被废弃时,使用 _Block_object_dispose 方法。
另外,__weak 与 __block 同时使用,clang 时报错 cannot create __weak reference because the current deployment target does not support weak references
。
说明不支持 __weak。
Block 存储域,即三种 Block
通过前面说明可知, Block 转换为 Block 结构体类型的自动变量, __block 变量转换为 __block 变量的结构体类型的自动变量。而所谓 结构体类型的自动变量,即栈上生成的该结构体的实例。
Block 与 __block 变量的实质:
名称 | 实质 |
---|---|
Block | 栈上 Block 的结构体实例 |
__block变量 | 栈上 __block 变量的结构体实例 |
而 Block 根据存储域的不同,可以归结为三类:
Block 的类 | 存储域 | 拷贝效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈拷贝到堆 |
_NSConcreteGlobalBlock | 程序的数据区域(.data) | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
全局 Block: _NSConcreteGlobalBlock
因为全局 Block 的结构体实例设置在程序的数据存储区,可以在程序任意位置通知指针来访问,只有两种情况下会产生全局 Block:
- 记述全局变量的地方有 Block 语法时。
- Block 语法的表达式中不截获自动变量时。
关于情况一,如下:
#include <stdio.h>
void(^blk_t)(void) = ^{
printf("Global Block.\n");
};
void test() {
blk_t();
}
通过 Clang 分析代码,获取 Block 的结构体实例如下,可知确实是全局 Block:
struct __blk_t_block_impl_0 {
struct __block_impl impl;
struct __blk_t_block_desc_0* Desc;
__blk_t_block_impl_0(void *fp, struct __blk_t_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
特别提示: 第二种情况我按照书中测试,即:
#include <stdio.h>
typedef int (^BlockNum) (int);
void test() {
for(int rate = 0; rate < 5; ++rate) {
BlockNum num = ^(int count) {
return count;
};
}
}
该 Block 的结构体如下
typedef int (*BlockNum) (int);
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
依然被认为存储在栈上,这里暂时不确定原因。
栈 Block:_NSConcreteStackBlock
在生成 Block 后,如果这个 Block 不是全局 Block,那么它就是栈 Block,即 _NSConcreteStackBlock 对象。
配置在全局变量上的 Block,从变量作用域外也可通过指针安全的使用。但设置在栈上的 Block,如果其所属变量作用域结束,该 Block 就被废弃。由于 __block 变量也配置在栈上,同样,其所属的变量作用域结束,则该 __block 变量也被废弃。
Blocks 机制提供了将 Block 和 __block 变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的 Block 复制到堆上,这样即使 Block 语法记述的变量作用域结束,堆上的 Block 还可以继续存在。
堆 Block:_NSConcreteMallocBlock
将栈 Block 复制到堆后,block结构体的 ISA 成员变量变成了 _NSConcreteMallocBlock。
在对 Block 使用 copy 实例方法时:
Block 的类 | 副本源的配置存储域 | 复制(copy)效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈拷贝到堆 |
_NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
不管 Block 配置之在何处,用 copy 方法都不会引起任何问题,在不确定时调用 copy 方法即可。
在 ARC 中不能显示调用 release,多次调用 copy 方法依然没有问题,如下:
typedef int (^BlockNum) (int);
void test(int rate) {
BlockNum num = ^(int count) { return count * rate;};
num = [[[[num copy] copy] copy] copy];
}
该源代码可以解释为:
{
BlockNum tmp = [num copy];
num = tmp;
}
{
BlockNum tmp = [num copy];
num = tmp;
}
{
BlockNum tmp = [num copy];
num = tmp;
}
{
BlockNum tmp = [num copy];
num = tmp;
}
内存管理并不会存在问题。
大多数情况下,编译器会自行判断将 Block 从栈上复制到堆上:
- block 作为函数返回值的时候。
- 部分情况下向方法或函数中传递 Block 时。部分情况主要是下面两种情况:
- Cocoa 框架的方法且方法名中含有 usingBlock 时。
- GCD 中的 API。
除了以上两种情况,我们基本是都要手动 copy Block,才能将其从栈复制到堆。
备注,前文也提到过 Block 从栈复制到堆的情况,那里多了个将 Block 赋值给 __strong 类型的对象,这里却没有说,暂时没有找到验证的方式。
那么 __block 变量在 Block 执行 copy 操作后会发生什么呢?
- 任何一个 Block 被复制到堆上时,使用的 __block 变量也会复制到堆上,并被该 Block 持有。
- 如果接着有其他 Block 复制到堆上,被复制的 Block 也持有 __block 变量,则会增加 __block 变量的引用计数。反过来如果堆上 Block 被废弃,它所持有的 __block 变量也会被释放。
Block 循环引用
如果在 Block 内部使用附有 __strong 修饰符的对象类型自动变量,那么当 Block 从栈复制到堆时,该对象为 Block 所持有。这样容易引起循环引用。如下:
typedef void(^blk_t)(void);
@interface Person : NSObject
{
blk_t blk_;
}
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"self = %@",self);
};
return self;
}
@end
Block blk_t持有self,而self也同时持有作为成员变量的blk_t,导致循环引用,MyObject 的对象实例会无法释放,造成内存泄漏。
另外,编译器有时候能够及时的查处循环引用,进行编译警告 Capturing 'self' strongly in this block is likely to lead to a retain cycle
。
__weak 修饰符
为避免循环引用,可声明附有 __weak 修饰符的变量,并将 self 赋值使用:
- (instancetype)init
{
self = [super init];
if (self) {
id __weak tmp = self;
blk_ = ^ {
NSLog(@"self = %@", tmp);
};
}
return self;
}
在低于 iOS4 时,使用 __unsafe_unretained 修饰符代替 __weak。
另外,以下源代码中 Block 中没有 self 但是同样截获了 self,引起循环引用:
typedef void(^blk_t) (void);
@interface MyObject : NSObject
{
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (instancetype)init
{
self = [super init];
if (self) {
blk_ = ^ {
NSLog(@"obj_ = %@", obj_);
};
}
return self;
}
@end
Block 语法内部使用的 obj_ 实际上截获了 self。对编译器来说,obj_ 只是对象用结构体的成员变量,即:
blk_ = ^ {
NSLog(@"obj_ = %@", self->obj_);
};
这里说个容易出错的情况,如果某个属性用 weak 关键字呢?
@interface Person()
@property (nonatomic, weak) NSArray *array;
@end
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"array = %@",_array);//循环引用警告
};
return self;
}
这里依然会有循环引用的警告,因为循环引用是 self 和 block 之间的事,和被这个 Block 持有的成员变量时strong、weak都没有关系,即使是基本数据类型 assgin 一样会有警告。
__block 修饰符
- (instancetype)init
{
self = [super init];
__block id temp = self;//temp持有self
//self持有blk_
blk_ = ^{
NSLog(@"self = %@",temp);//blk_持有temp
temp = nil;
};
return self;
}
- (void)execBlc
{
blk_();
}
如果不调用 execBlc 实例方法,即不给成员变量 blk_ 赋值,便会循环引用并造成内存泄漏。
使用 __block 避免循环引用的优点:
- 通过 __block 变量可控制对象的持有时间。
- 为避免循环引用必须执行 Block,否则循环引用一直存在。
说一下,使用 __block 来避免循环引用,感觉并不方便。不过也可以更具实际情况去具体选择。