声明:本文是读了<Objective-C高级编程>做的笔记,以及结合本人写的例子总结的Block知识。
目录
Block入门
什么是Block
Block是带有自动变量值的匿名函数。
如何定义一个Block
跟定义一个函数是差不多的,只是不需要写名字。
完整的语法:^ 返回值类型 参数列表 表达式
^int (int count){return count + 1;}
若没有返回值,可以省略返回值类型:^ 参数列表 表达式
^ (int count){return count + 1;}
若不使用参数,参数列表也可省略。以下为不适用参数的Block语法:
^void (void){printf("hello world");}
该代码可以省略为如下形式:
^{printf("hello world");}
如何声明一个Block类型的变量
返回值类型 (^变量名称) 参数列表
int (^blk)(int);
其实这就跟C语言中的函数指针很相似:
int func(int count) {
return count + 1;
}
int (*funcptr)(int) = &func;
Block作为一个方法的参数时,声明的写法有点不一样:
+ (void)querDataWithCallBack:(void(^)(id))callBack;
把一个Block赋值给Block类型变量
就是声明写在左边,定义写在右边,中间一个等号。有了以上两个解析,很容易就可以写出:
int (^blk)(int) = ^int (int count) {
return count + 1;
};
也可以这样写:
int (^blk)(int);
blk = ^int (int count) {
return count + 1;
};
int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;
用typedef为Block类型定义一个简单的别名
typedef int (^qhdBlock)(int);
qhdBlock a = ^ int (int count) {
return count + 1;
};
调用Block
就像调用C语言函数一样
int (^blk)(int) = ^int (int count) {
return count + 1;
};
blk(1); //调用
用Block作为回调
- (void)querNetworkDataWithCallBack:(void(^)(id))callBack {
id result = nil;
//这里从网络中获取数据,给result赋值
//通常较耗时,需要开子线程
if (callBack) {
callBack(result);
}
}
- (void)test {
[self querNetworkDataWithCallBack:^(id data) {
//使用网络返回的数据
//NSLog(@"%@", data);
}];
}
用Block实现策略模式
typedef int (^calculateBlock)(int,int);
- (int)calculateBlock:(calculateBlock)type num1:(int)num1 num2:(int)num2 {
return type(num1, num2);
}
- (void)test {
calculateBlock add = ^(int a1, int a2) { return a1 + a1; };
calculateBlock subtract = ^(int a1, int a2) { return a1 - a1; };
calculateBlock multiply = ^(int a1, int a2) { return a1 * a1; };
calculateBlock divide = ^(int a1, int a2) { return a1 / a1; };
NSLog(@"4+5=%d",[self calculateBlock:add num1:4 num2:5]);
NSLog(@"4-5=%d",[self calculateBlock:subtract num1:4 num2:5]);
NSLog(@"4x5=%d",[self calculateBlock:multiply num1:4 num2:5]);
NSLog(@"4/5=%d",[self calculateBlock:divide num1:4 num2:5]);
}
截取自动变量
(这也是Block和函数指针的区别,因为Block的定义可以嵌套在方法里,所以能够截取方法里的自动变量)
举个例子:
- (void)test {
int val = 10;
NSString *str = @"hello";
void (^blk)() = ^ {
NSLog(@"%@:%d", str, val);
};
val = 50;
str = @"good";
blk();
}
这里运行的结果是:hello:10
而不是预期的:good:50
说明了在Block中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行Block语法后,即使改写自动变量的值也不影响Block执行时自动变量的值。
这就是自动变量值的截获。
一些用法疑问
在MRC下,Block在声明property时为什么要用copy
因为Block是在栈上的,不是在堆上的,超出作用域就会被自动释放。因此需要用copy,而不是用retain。如果是在ARC下,copy和strong都行。
以下例子,在Block有读取外部变量的情况下,用retain修饰,不在有效作用域内执行block会报EXC_BAD_ACCESS
错误,即野指针异常问题,为什么会出现此错误,因为Block的作用域范围是在viewDidLoad内,超出作用域Block就会被释放,在viewDidAppear时已经超出了作用域。解决此问题的方法是把retain换成copy。
@interface ViewController ()
@property (retain, nonatomic) void(^myBlock)();
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
int a = 1;
void (^tempBlock)() = ^() {
NSLog(@"a:%d", a);
};
[self setMyBlock:tempBlock];
NSLog(@"finish set");//在这断点,观察myBlock的地址
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];//在这断点,再观察myBlock的地址,已经变了
self.myBlock();
NSLog(@"finish run");
}
@end
__block标识符的用法
截获基本数据类型的变量
假如我们想在Block里面改变在Block以外的变量值,看例子:
int a = 0;
void (^blk)(void) = ^{ a = 1;}; //这里会报错:Variable is not assignable (missing __block type specifier)
blk();
printf("a = %d", a);
看报错提示,我们给int a附加__block
说明符,就能实现Block内赋值:
__block int a = 0;
void (^blk)(void) = ^{ a = 1;};
blk();
printf("a = %d", a);
该代码的执行结果:a = 1
截获对象类型的变量
在截获Objective-C对象时:
NSMutableArray *m = [NSMutableArray array];
void (^blk)(void) = ^{
[m addObject:@"abc"];
};
blk();
NSLog(@"m[0] = %@", m[0]);
这是没有问题的,因为m就是一个对象指针,所以Block是截获了指针,在Block里面对指针所指向的内容进行修改是可以的。
但是给m赋值会怎样:
NSMutableArray *m = [NSMutableArray array];
void (^blk)(void) = ^{
m = [NSMutableArray array];//这里会报错:Variable is not assignable (missing __block type specifier)
};
这是会产生编译错误。
这种情况,需要给截获的自动变量附加__block
说明符。
__block NSMutableArray *m = [NSMutableArray array];
void (^blk)(void) = ^{
m = [NSMutableArray array];
};
Block实质
初探Block转换出的C代码
新建一个block.m文件,这个例子有两个Block,这样比《Objective-C高级编程》中的例子更好说明哪些是公共的,有参数是怎样的。
#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
void (^myBlock)(void) = ^{
printf("hello world\n");
};
myBlock();
int a = 10;
BOOL (^yourBlock)(int) = ^(int b){
int sum = a + b;
printf("%d + %d = %d\n", a, b, sum);
return YES;
};
yourBlock(5);
return 0;
}
运行命令:clang -rewrite-objc block.m
生成文件blcok.cpp,通过”-rewrite-objc”选项就能将含有Block语法的源代码变换为C++的源码,其实只是用到了struct结构,其本质是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;
__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("hello world\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)};
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
int a;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static BOOL __main_block_func_1(struct __main_block_impl_1 *__cself, int b) {
int a = __cself->a; // bound by copy
int sum = a + b;
printf("%d + %d = %d\n", a, b, sum);
return ((bool)1);
}
static struct __main_block_desc_1 {
size_t reserved;
size_t Block_size;
} __main_block_desc_1_DATA = { 0, sizeof(struct __main_block_impl_1)};
int main(int argc, char * argv[]) {
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
int a = 10;
BOOL (*yourBlock)(int) = ((BOOL (*)(int))&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, a));
((BOOL (*)(__block_impl *, int))((__block_impl *)yourBlock)->FuncPtr)((__block_impl *)yourBlock, 5);
return 0;
}
以上,我故意把第一个结构体与下面的代码分开,来表达__block_impl
是默认自带的结构体。
第一眼看起来很多代码,写惯OC的可能看不惯C的写法,其实不太复杂,就是结构体的声明与构造,还有一些类型转换。
我们定义了第一个Block(即myBlock),就生成了:
- __main_block_impl_0(myBlock的结构体)
- __main_block_func_0(myBlock的执行函数)
- __main_block_desc_0(myBlock的描述)
第二个Block(即yourBlock)则生成了:
- __main_block_impl_1(yourBlock的结构体)
- __main_block_func_1(yourBlock的执行函数)
- __main_block_desc_1(yourBlock的描述)
可见Block对应的结构体或函数的命名是跟顺序相关的,并且每新定义1个Block就至少产生2个结构和1个函数。
我们先看__main_block_impl_x
(x是指序号)里面有什么,直接用代码跟注释的方式:
struct __main_block_impl_x {
struct __block_impl impl; //这是Block的公共结构体
struct __main_block_desc_x* Desc; //这是Block的描述,指向__main_block_desc_x结构体
int a; //这里就是截获的自动变量
... //如果截获了多个自动变量,这里会有多个
//这是自身这个结构体的构造函数
__main_block_impl_x(void *fp, struct __main_block_desc_x *desc, int _a, int flags=0) : a(_a) {//这里_a参数会赋值给a变量
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我们再看__block_impl
是什么,继续以代码加注释的方式:
struct __block_impl {
void *isa; //isa有点类似于class的isa,有三种值:&_NSConcreteStackBlock/&_NSConcreteGlobalBlock/&_NSConcreteMallocBlock
int Flags;
int Reserved;
void *FuncPtr; //函数指针
};
然后我们可以看回__main_block_impl_x
的构造函数里的实现,给isa赋了&_NSConcreteStackBlock
,给FuncPtr赋了第一个参数fp,给Desc赋了第二个参数desc,给a赋了第三个参数_a
。
我们再看__main_block_desc_x
的结构体,继续以代码加注释的方式:
static struct __main_block_desc_x {
size_t reserved;
size_t Block_size; //Block的大小
} __main_block_desc_x_DATA = { 0, sizeof(struct __main_block_impl_x)};//这里定义了一个变量__main_block_desc_x_DATA
我们再看Block的执行函数,第一个参数是当前函数所属于的Block,第二个参数开始就是Block的参数了。
static BOOL __main_block_func_x(struct __main_block_impl_x *__cself, int b)
在main函数里,我们把类型转换去掉就清晰多了:
int main(int argc, char * argv[]) {
void (*myBlock)(void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
(myBlock->FuncPtr)(myBlock);
int a = 10;
BOOL (*yourBlock)(int) = (&__main_block_impl_1(__main_block_func_1, &__main_block_desc_1_DATA, a));
(yourBlock->FuncPtr)(yourBlock, 5);
return 0;
}
在main函数里,先初始化Block,再调用Block。第二个Block,在初始化时会传入a变量的值,调用Block时传入5。
至此为止,定义的两个Block算简单,转出来的C代码都比较清晰。
带__block标识符转出来的C代码
下面看看带有__block
标识符的变量转成C代码是怎样的:
#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
__block int a = 1;
void (^aBlock)(void) = ^{
a = 2;
};
aBlock();
return 0;
}
运行”clang -rewrite-objc”后:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 2;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
void (*aBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)aBlock)->FuncPtr)((__block_impl *)aBlock);
return 0;
}
对比之前的例子,我们发现多了一个结构体__Block_byref_a_0
和两个函数__main_block_copy_0
、__main_block_dispose_0
。
我们看结构体__Block_byref_a_0
,里面有一个跟原自动变量的类型和命名都一样的成员变量a,又有一个指向自身类型的指针__forwarding
,这不是链表型的设计吗。后来一看main函数在创建__Block_byref_a_0
类型的a变量时,把__forwarding
指向了自己,再看创建__main_block_impl_0
实例时传递的参数值时&a,即a地址。再看Block的执行函数__main_block_func_0
,原来的a=2;
变成了(a->__forwarding->a) = 2;
,这就是为什么在Block能够改变外部变量的原因,是传递了指针,不是简单地传值。
存储域
学习过C语言的应该知道,一个程序的内存分为:
- 代码区(.text区)
- 数据区(.data区)
- 堆
- 栈
Block的类对应的存储域对应如下:
类 | 设置对象的存储域 |
---|---|
_NSConcreteStackBlock | 栈 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区) |
_NSConcreteMallocBlock | 堆 |
到目前为止以上的例子都是_NSConcreteStackBlock
类,那什么时候是其他类型呢,看以下例子,当全局变量的地方有Block语法时,Block就会是_NSConcreteGlobalBlock
类对象
void (^myBlock)(void) = ^{printf("hello");};
int main(int argc, char * argv[]) {
return 0;
}
那么_NSConcreteMallocBlock
类何时会使用?当在栈区的Block超出了其所属的作用域,该Block就会被废弃。如果我们想在超出其作用域时使用它,那么我们就需要把Block复制到堆上,在堆上的Block其类型就是_NSConcreteMallocBlock
。
以下我们来看个例子:
@interface ViewController ()
@property (copy, nonatomic) void(^myBlock)();
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
int a = 1;
void (^tempBlock)() = ^() {
NSLog(@"a:%d", a);
};
[self setMyBlock:tempBlock];
NSLog(@"finish set");
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.myBlock();
NSLog(@"finish run");
}
@end
如果用Xcode断点调试会发现tempBlock的__isa
是__NSStackBlock__
,而myBlock的__isa
是__NSMallocBlock__
,对Block进行复制会把其从栈复制到堆。在ARC时以上代码把property的修饰符改为strong也是会对Block进行复制的,但用assign是不会复制的。在MRC时property的修饰符设置copy才会复制,retain并不会复制。
总结,如果我们想要长久持有Block,对Block进行复制,要用如下修饰符:
编译环境 | 修饰符 |
---|---|
MRC | copy |
ARC | copy,strong |
另外对在不同区域的Block进行复制的效果如下:
Block的类 | 源所在的存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 数据区域 | 什么都不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
复制Block后,__block变量的存储域变化
当把Block从栈复制到堆后,__block
变量也会复制到堆。复制到堆后,怎么保持两边的值一致呢?这时我们回顾一下结构体成员变量__forwarding
,在复制前,栈上的__block
变量的__forwarding
指向了自己,在复制后,栈上的__block
变量的__forwarding
指向了堆上的__block
变量,而堆上的__block
变量的__forwarding
仍然是指向自己,前面看到读写变量实际是读取__forwarding
指向的结构体下的成员变量,这样就相当于,两边的变量都是用了同一个值。
循环引用问题
@interface User : NSObject
@property (copy, nonatomic) void (^myBlock)();
@end
@implementation User
- (instancetype)init
{
self = [super init];
if (self) {
[self setMyBlock:^{
NSLog(@"%@", self);
}];
}
return self;
}
@end
以上代码,当创建User类的对象时,对象持有成员变量_myBlock
,_myBlock
持有了所使用的self(即对象本身),这就是循环引用,会造成内存泄漏。
可以这样来避免循环引用:
__weak User *ws = self;
[self setMyBlock:^{
NSLog(@"%@", ws);
}];
当然用__unsafe_unretained
也是可以的,但是__weak
可以防止野指针的问题。这是比较简单的循环,因为只有两个对象互相引用,在项目中可能有更多的对象组成了循环,所以写代码时要清晰地知道会不会有强引用。
注意在MRC时,retain并不会复制Block,推荐使用copy来持有Block。
另外在MRC时,__block
说明符被用来避免Block中的循环引用。这是由于Block从栈复制到堆时,若Block使用的变量为附有__block
说明符的id类型或对象类型的自动变量,不会被retain;若没有__block
说明符,则被retain。
在ARC时,__block
的作用只是能否改变外部变量。所以要注意__block
说明符在MRC和ARC是有很大区别的。当然我们现在的项目代码都使用ARC来写项目了。
附上我常用的定义weakSelf,strongSelf的宏:
#define WEAK_OBJ(obj, name) __weak __typeof(obj) name = obj
#define STRONG_OBJ(obj, name) __strong __typeof(obj) name = obj
#define WEAK_SELF(name) WEAK_OBJ(self, name)