一 Block基本语法
1.1 什么是block
带有自动变量(局部变量)的匿名函数;
1.2 Block语法
匿名函数Block,简写方式:
^(int event) {
printf("buttonId:%d event=%d\n", i , event);
}
完整形式如下
^void (int event) {
printf("buttonId:%d event=%d\n", i , event);
}
- 完整的Block与C语言的函数定义相比,仅有两点不同:
- 没有函数名, 因为它是匿名函数
- 返回值类型前带有 “^”, 因为OS X,iOS 应用程序的源代码中将大量使用Block, 所以插入该记号便于查找
Block语法的BN范式:
^ 返回值类型 参数列表 表达式
^int (int count) {
return count +1;
}
Block省略方式一
^ 参数列表 表达式
^ (int count) {
return count +1;
}
Block 省略方式二
^ 表达式
不使用参数的完整Block语法
^void (void) {
printf("Block");
}
省略方式如下
^ {
printf("Block");
}
1.3 截获的自动变量
自动变量值的截获:block表达式会保存自动变量的值,所以在执行block语法后,及时改写Block中使用的自动变量的值也不会影响block执行时自动变量的值;
截获的OC对象,调用变更对象的方法会产生编译错误么?
NSMutableArray *arrM = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id object = [[NSObject alloc] init];
[arrM addObject:object];
};
以上是没有问题的,源代码截获的变量值为NSMutableArray 类的对象,即截获 NSMutableArray 类用的结构体示例指针,虽然赋值给截获的自动变量arrM操作会产生编译错误,但使用截获的值却不会有任何问题;
1.4 __block 说明符
实际上,自动变量值的截获只能保存执行Block语法瞬间的值,保存后就不能改写该值,如果改写的话,则会进行报错;
typedef int (^blk_t) (int);
int main(int argc, char * argv[]) {
int val = 0;
void (^blk)(void) = ^{
val = 1;// 此处会报错
}
}
使用附有__block说明符的自动变量可在Block中赋值,该变量称为__block变量;
二 Block实现
2.1 Block实质
clang(LLVM编译器)
具有转换为我们可读源代码的功能,通过-rewrite-objc
选项就能将含有Block语法的源代码变换为C++的源代码, 其实仅仅使用了struct结构,其本质是C语言源代码
终端执行以下代码即可
clang -rewrite-objc 源代码文件名
我们将以下代码转换为Block语法
int main(int argc, char * argv[]) {
void (^blk)(void) = ^ {
printf("Block");
};
blk();
return 0;
}
以上代码仅有6行,但是通过clang转换后,得到的main.cpp文件,达到130+行代码,我们看最初源代码中的block语法。
^ {
printf("block");
}
变换后的源代码中也有相同的表达式:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block");
}
该函数的__cself其实就是C语言中的this,OC实例方法中指向对象自身的变量self,即参数__cself,为指向Block值的变量;
- 该参数的声明如下:
struct __main_block_impl_0 *__cself
__cself 是 __main_block_impl_0 结构体的指针;
该结构体声明如下
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;
}
};
第一个成员变量是 impl,看一下这个成员变量的结构体声明
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
第二个成员变量是desc指针:
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
那么我们再来看一下 初始化含有这些结构体的的main_block_impl_0函数的 构造函数
__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;
}
我们可以来看看该构造函数的调用:
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
因为转换较多,看起来不是很清楚,我们去掉转换的部分,具体如下:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
该源代码将 __main_block_impl_0
结构体类型的变量,即栈上生成的 __main_block_impl_0
结构体实例的指针,赋值给了 __main_block_impl_0
结构体指针的变量blk
,以下为这部分的最初源代码
void (^blk)(void) = ^ {
printf("block");
};
以上代码它等同于将 __main_block_impl_0
结构体实例的指针赋值给变量blk
, 该源代码中的Block就是 __main_block_impl_0
结构体类型的自动变量,即栈上生成的 __main_block_impl_0
的结构体示例
来看看__main_block_impl_0
结构体示例构造函数
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA)
-
__main_block_func_0
为C语言函数指针 -
&__main_block_desc_0_DATA
为静态全局变量初始化的__main_block_desc_0
的结构体示例指针;
以下为 __main_block_desc_0
初始化部分
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_block_impl_0
结构体实例(即Block)是按照如下方式进行初始化的
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0 *Desc;
}
这个结构体根基构造函数想下面这样进行初始化
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0
FuncPtr = __main_block_impl_0;
Desc = &__main_block_desc_0_DATA;
blk()
转换为了以下代码
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
去掉转换部分,就是函数指针调用函数,
(*blk->impl.FuncPtr)(blk);
这就是简单的使用函数指针调用函数
正如我们刚确认的,有Block
语法转换的 main_block_func_0
函数的指针被赋值成员变量FuncPtr
中; 另外也说明了main_block_func_0
函数的参数__cself
指向Block
值,在调用该函数的源代码中可以看出Block
正是作为参数进行了传递;
接下来看 & _NSConcreteStackBlock
到底是什么;
isa = &_NSConcreteStackBlock;
其实Block指针赋值给Block的结构体成员变量isa,为了理解他,首先要理解OC类和对象的本质,其实block就是OC对象;
即有该类生成的对象的各个结构体示例,通过成员变量isa保持该类的结构体实例指针,如下OC类与对象的实质
_NSConcreteStackBlock
相当于Class
结构体实例,在将Block
作为OC
的对象处理时,关于该类的的信息放置于 _NSConcreteStackBlock
中;
2.2 截获自动变量值
将截获的自动变量值的源代码通过clang转换为为以下代码
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, char * argv[]) {
int val = 10;
const char *fmt = "val = %d";
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));
return 0;
}
Block语法表达式中使用的自动变量被追加到了 __main_block_impl_0 结构体中
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;
};
初始化构该结构体实例的构造函数时,根据传递给构造函数的参数进行初始化
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "val = %d";
val = 10
2.3 __Block说明符
我们先来看下OC中静态变量、静态全局变量、全局变量的源代码实现
源代码
// 全局变量
int global_val = 1;
// 静态全局变量
static int static_global_val = 2;
int main(int argc, char * argv[]) {
// 静态变量
static int static_val = 3;
void (^blk)(void) = ^ {
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
};
return 0;
}
转换后的代码
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;
int *static_val;
__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_val) *= 3;
}
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, char * argv[]) {
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));
return 0;
}
结论:静态全局变量static_global_val 和全局变量 global_val 的访问和转换前完全相同,来看下静态变量的转换
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
(*static_val) *= 3;
}
将静态变量static_val
的指针传递给了__main_block_impl_0
结构体的构造函数进行保存,这是超出作用域使用变量的最简单的方式;
使用__block说明符
__block int val = 10;
void (^blk)(void) = ^ {
val = 1;
};
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, char * argv[]) {
__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));
return 0;
}
只是在自动变量上附加了__block说明符,源代码就急剧增加;
__block int val = 10;
转换成了如下
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
(void*)0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
10
};
变成了一个结构体;__block
变量也同Block
一样变成了 __Block_byref_val_0
结构体类型的自动变量,即栈上生成了 __Block_byref_val_0
结构体实例,初始化为10;
该结构体声明如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
而赋值的代码又转换成了什么呢?
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
向静态变量赋值时,使用了指向该静态变量的指针;
block
变量赋值则是,Block的 __main_block_impl_0
结构体实例持有指向__Block变量的 __Block_byref_val_0
结构体实例的指针;
__Block_byref_val_0
结构体实例的成员变量 __forwarding
持有指向该实例自身的指针。通过成员变量__forwarding
访问成员变量val;
为什么会有成员变量 __forwarding 指向自己呢? 这个放在后边讲
__Block
生成的结构体 __Block_byref_val_0
结构体不在Block __main_block_impl_0
中,是为了在多个Block中使用__Block
变量;
2.4 Block存储域
Block与__Block变量的实质
名称 | 实质 |
---|---|
Block | 栈上Block的结构体实例 |
__block 变量 | 栈上__block变量的结构体实例 |
将Block
当做OC
对象看,该Block
的类为 _NSConcreteStackBlock
,还有很多与之类似的类有
类 | 设置对象的存储域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区) |
_NSConcreteMallocBlock | 堆 |
在记录全局变量的地方使用Block语法时,则Block
为 _NSConcreteGlobalBlock
类对象
-
以下
Block
为NSConcreteGlobalBlock
类对象,- 记述全局变量的地方有Block语法时
- Block语法的表达式中不使用应截获的自动变量时;
配置在栈上的Block,其变量作用域销毁如下
所以Blocks提供了将栈上的Block复制到堆上的方法来解决这个问题,如下图
此时Block的结构体实例的成员变量isa变化如下
impl.isa = &_NSConcreteMallocBlock;
而__block
变量用结构体成员变量__forwarding
可以实现无论是__block
变量配置在栈上,还是在堆上,都能正确的访问__Block
变量;
实际上当ARC有效的时候,大多数情形下,编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上的代码。
可以自动生成Block从栈上赋值到堆上的代码
查看此源代码
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count)(return rate * count);
}
该源代码返回配置在栈上的Block函数,即程序执行中从该函数返回函数函数调用方时变量作用域结束,因此栈上的Block也被废弃。但是该源代码通过对应的ARC编译器可转换如下
blk_t fun(int rate) {
// 通过block语法生成的Block,即配置在栈上的Block用结构体实例赋值给相当于Block类型的变量tmp中
blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
// 将栈上的Block复制到堆上。复制后,将堆上的地址作为指针赋值给变量tmp中
tmp = objc_retainBlock(tmp);
// 将堆上的Block作为Objectice-C对象 注册到autoreleasepool中,然后返回该对象
return objc_autoreleaseReturnValue(tmp);
}
通过Objc4
运行时库的 runtime/objc-arr.mm
可知, objc_retainBlock
函数实际上就是Block_copy
函数,即
tmp = _Block_copy(tmp);
那么编译器不能进行判断是否从将Block
从栈上复制到堆上有哪些情况呢?
- 向方法或函数的参数中传递Block时,此种情况就需要手动复制 否则就会出现野指针Crash
typedef void (^blk_t)(void);
- (id)getBlockArrar {
int val = 10;
// 此种写法会crash
// return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);}, ^{NSLog(@"blk1:%d",val);}, nil];
// 正确的写法
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d",val);} copy], [^{NSLog(@"blk1:%d",val);} copy], nil];
}
- (void)test {
NSArray * obj = [self getBlockArrar];
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
}
- 如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了,以下方法就不用手动复制
- Cocoa框架的方法且方法中含有usingBlock时
- Grand Central Dispatch的API
2.5 __Block变量存储域
使用__block变量的Block 从栈复制到堆上,__block变量同样跟着受影响
__Block变量的配置存储域 | Block 从栈上复制到堆上的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
使用__block
变量的Block
持有__block
变量,若Block
被废弃,他所持有的__block
变量也就被释放
2.6 截获对象
示例代码
typedef void (^blk_t)(id);
blk_t blk;
{
NSMutableArray * array = [[NSMutableArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@" count = %ld", [array count]);
} copy];
}
blk([NSObject new]);
blk([NSObject new]);
// 输出为
count = 1
count = 2
上述的代码能够正常运行,意味着赋值给变量Array的NSMutableArray类的对象在该源代码最后Block的执行部分超出其变量作用域而存在,通过编译器转换后的核心源代码如下
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;
}
};
但是在OC中,C语言结构体里不能含有附有__strong
修饰的变量,因为编译器不知道应该何时初始化和废弃C结构体,不能很好的管理内存;
但是OC的运行时库能够准确的把Block
从栈复制到堆上以及堆上的Block被废弃的时机,是在__main_block_desc_0
结构体中增加的成员变量copy
和dispose
,以及作为指针赋值给改成员变量的__main_block_copy_0
函数和__main_block_dispose_0
函数实现的;
// _Block_object_assign 相当于retaain实例方法函数,将对象赋值在对象类型的结构体变量中
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 相当于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类型成员变量时
- 在方法名中含有usingBlock的Cocoa框架方法 或Grand Central Dispatch的API传递Block时
-
什么时候堆上的Block被废弃呢?
- 在释放复制到堆上的block后,谁都不持有Block而使其被废弃时调用dispose函数。相当于对象的dealloc实例方法
2.7 __Block 变量和对象
__block说明符可指定任何类型的自动变量。
我们上边讲述的是只有附有__strong修饰符的id类型或对象类型自动变量。如果使用__weak修饰符会如何呢?
typedef void (^blk_t)(id);
blk_t blk;
{
NSMutableArray * array = [[NSMutableArray alloc] init];
id __weak weakArray = array;
blk = [^(id obj) {
[weakArray addObject:obj];
NSLog(@" count = %ld", [weakArray count]);
} copy];
}
blk([NSObject new]);
blk([NSObject new]);
//输出为
count = 0
count = 0
【结论】:结果与上边的执行结果不同,但是该代码能够正常执行;这是因为array 在变量作用域结束的时候,被释放和废弃,nil被赋值给了weakArray;
如果使用__block和__weak修饰符修饰的呢?
typedef void (^blk_t)(id);
blk_t blk;
{
NSMutableArray * array = [[NSMutableArray alloc] init];
__block __weak NSMutableArray *weakArray = array;
blk = [^(id obj) {
[weakArray addObject:obj];
NSLog(@" count = %ld", [weakArray count]);
} copy];
}
blk([NSObject new]);
blk([NSObject new]);
//输出为
count = 0
count = 0
【结论】即使加了__block ,array 在变量作用域结束的时候,被释放和废弃,nil被赋值给了weakArray;
参考:
《Objective-C高级编程iOS与OS X多线程和内存管理》