什么是block
在iOS 4.0之后,block横空出世,它本身封装了一段代码并将这段代码当做变量,通过block()的方式进行回调。
自从block出现之后,很多API都开始采用这样的结构,由此可见,block确实有许多优势存在,这里将一些简单用
法总结如下:
1、如何声明一个block变量
我们通过^符号来声明block类型,形式如下:
(1) 标准声明与定义
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };
blockName(var);
(2) 当返回类型为void
void (^blockName)(var_type) = ^void (var_type varName) { // ... };
blockName(var);
可省略写成
void (^blockName)(var_type) = ^(var_type varName) { // ... };
blockName(var);
(3) 当参数类型为void
return_type (^blockName)(void) = ^return_type (void) { // ... };
blockName();
可省略写成
return_type (^blockName)(void) = ^return_type { // ... };
blockName();
(4) 当返回类型和参数类型都为void
void (^blockName)(void) = ^void (void) { // ... };
blockName();
可省略写成
void (^blockName)(void) = ^{ // ... };
blockName();
(5) 匿名Block
Block实现时,等号右边就是一个匿名Block,它没有blockName,称之为匿名Block:
^return_type (var_type varName)
{ //... };
2.2 typedef简化Block的声明
利用typedef简化Block的声明:
声明
typedef return_type (^BlockTypeName)(var_type);
例子1:作属性
//声明 typedef void(^ClickBlock)(NSInteger index); //block属性 @property (nonatomic, copy) ClickBlock imageClickBlock;
例子2:作方法参数
//声明 typedef void (^handleBlock)(); //block作参数 - (void)requestForRefuseOrAccept:(MessageBtnType)msgBtnType messageModel:(MessageModel *)msgModel handle:(handleBlock)handle{
...
2、block的常见用法
1 局部位置声明一个Block型的变量
位置
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };
blockName(var);
例子
void (^globalBlockInMemory)(int number) = ^(int number){
printf("%d \n",number);
};
globalBlockInMemory(90);
2 @interface位置声明一个Block型的属性
位置
@property(nonatomic, copy)return_type (^blockName) (var_type);
例子
//按钮点击Block @property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);
3 在定义方法时,声明Block型的形参
用法
- (void)yourMethod:(return_type (^)(var_type))blockName;
例子
- (void)addClickedBlock:(void(^)(id obj))clickedAction;
4 在调用如上方法时,Block作实参
例子
- (void)addClickedBlock:(void(^)(id obj))clickedAction{
self.clickedAction = clickedAction; // :先判断当前是否有交互事件,如果没有的话。。。所有gesture的交互事件都会被添加进gestureRecognizers中
if (![self gestureRecognizers]) {
self.userInteractionEnabled = YES; // :添加单击事件
UITapGestureRecognizer *tap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];
[self addGestureRecognizer:tap];
}
}
- (void)tap{
if (self.clickedAction) {
self.clickedAction(self);
}
}
3、block的少见用法
1 Block的内联用法
这种形式并不常用,匿名Block声明后立即被调用:
^return_type (var_type varName)
{ //... }(var);
2 Block的递归调用
Block内部调用自身,递归调用是很多算法基础,特别是在无法提前预知循环终止条件的情况下。注意:由于Block内部引用了自身,这里必须使用__block避免循环引用问题。
__block return_type (^blockName)(var_type) = [^return_type (var_type varName)
{ if (returnCondition)
{
blockName = nil; return;
} // ... // 【递归调用】 blockName(varName);
} copy];
【初次调用】
blockName(varValue);
3 Block作为返回值
方法的返回值是一个Block,可用于一些“工厂模式”的方法中:
用法:
- (return_type(^)(var_type))methodName
{ return ^return_type(var_type param) { // ... };
}
4、block中访问对象的微妙关系
1、如果你在一个block块中仅仅访问对象,而不是对他进行修改操作,是没有任何问题的:
- (void)viewDidLoad {
[super viewDidLoad];
int tem=2;
block1 = ^(int a,int b){
int count= tem+1;
return count;
};
NSLog(@"%d",block1(1,1));
}
2、如果在block块中直接修改变量,编译器会报错:
block1 = ^(int a,int b){
tem+=1;
return tem+1;
};
为什么会出现这样的情况,根据猜测,可能是block内部将访问的变量都备份了一份,如果我们在内部修改,外部的变量并不会被修改,我们可以通过打印变量的地址来证明这一点:
- (void)setUp {
int i = 1;
NSLog(@"%p + out",&i);
self.block = ^(int a,int b){
NSLog(@"%p + inside",&i);
return 1;
};
self.block(2,3);
}
打印结果如下:
2017-03-06 11:11:07.657 SwiftDemo[7890:9947536] 0x7fff535bba7c + out
2017-03-06 11:11:07.657 SwiftDemo[7890:9947536] 0x7fff535bba78 + inside
可以看出,变量的地址已经改变。
3、block在捕获变量的时候只会保存变量被捕获时的状态(对象变量除外),之后即便变量再次改变,block中的值也不会发生改变。
4、如何对外部变量进行修改,__block 做了什么
为了可以在block块中访问并修改外部变量,我们常会把变量声明成__block类型,通过上面的原理,可以发现,其实这个关键字只做了一件事,如果在block中访问没有添加这个关键字的变量,会访问到block自己拷贝的那一份变量,它是在block创建的时候创建的,而访问加了这个关键字的变量,则会访问这个变量的地址所对应的变量。我们可以通过代码来证明:
- (void)setUp {
__block int i = 1;
NSLog(@"%p + out",&i);
self.block = ^(int a,int b){
NSLog(@"%p + inside",&i);
return 1;
};
self.block(2,3);
}
结果:
2017-03-06 11:17:16.438 SwiftDemo[8088:9950943] 0x7fff51ef0a78 + out
2017-03-06 11:17:16.439 SwiftDemo[8088:9950943] 0x7fff51ef0a78 + inside
由此,我们可以理解,如果block中操作的对象是指针,那么直接可以进行修改,这包括OC对象,如果不是,则需要用__block关键字修饰。
5、关于引用计数
在block中访问的对象,会默认retain:
- (void)setUp {
UIImage *image = [UIImage new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(image)));
self.block = ^(int a,int b){
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(image)));
};
self.block(2,3);
}
结果如下:
2017-03-06 11:27:47.856 SwiftDemo[8494:9958586] 1
2017-03-06 11:27:47.856 SwiftDemo[8494:9958586] 3
而添加__block的对象不会被retain;
2017-03-06 13:41:02.850 SwiftDemo[8747:9968324] 1
2017-03-06 13:41:02.850 SwiftDemo[8747:9968324] 1
注意:如果我们访问类的成员变量,或者通过类方法来访问对象,那么这些对象不会被retain,而类对象会被retain,最常见的是在block中直接使用self会有warning:
! Capturing 'self' strongly in this block is likely to lead to a retain cycle
循环引用
block在iOS开发中被视作是对象,因此其生命周期会一直等到持有者的生命周期结束了才会结束。另一方面,由于block捕获变量的机制,使得持有block的对象也可能被block持有,从而形成循环引用,导致两者都不能被释放。导致内存泄露。所谓的内存泄露就是本应该释放的对象,在其生命周期结束之后依旧存在。
解决办法
系统提供给我们__weak的关键字用来修饰对象变量,声明这是一个弱引用的对象
__weak typeof(*&self) weakSelf = self;
所有的Block里面的self必须要weak一下?
很显然答案不都是,有些情况下是可以直接使用self的,比如调用系统的方法:
[UIView animateWithDuration:0.5 animations:^{
NSLog(@"%@", self);
}];
因为这个block存在于静态方法中,虽然block对self强引用着,但是self却不持有这个静态方法,所以完全可以在block内部使用self。
5、block与内存管理
根据Block在内存中的位置分为三种类型:
NSGlobalBlock
NSGlobalBlock是位于全局区的block,它是设置在程序的数据区域(.data区)中。
只用到全局变量、静态变量的block。生命周期为程序的生命周期,不持有对象。
static NSString *string = @"hello word";
typedef void(^Block)();
Block block = ^{
NSLog(@"%@",string);
};
NSLog(@"%@",block);
//<__NSGlobalBlock__: 0x10ec4c090>
NSStackBlock
NSStackBlock是位于栈区,超出变量作用域,栈上的Block以及 __block变量都被销毁。
只用到外部局部变量、成员属性变量、没有强指针引用的block,copy操作会复制到堆上
@property (nonatomic, strong) NSString *str;
int multiplier = 7;
NSLog(@"%@",^(int num) {
NSLog(@"%@",_str);
return num * multiplier;
});
//<__NSStackBlock__: 0x7fff5f8989e8>
NSMallocBlock
NSMallocBlock是位于堆区,在变量作用域结束时不受影响。
有强指针引用或者调用copy方法复制到堆中的block,生命周期由程序员控制
- (void(^)())blockReturn {
NSString *strBlock = @"NSMallocBlock";
return ^(){
NSLog(@"%@",strBlock);
};
}
NSLog(@"%@",[self blockReturn]);
//<__NSMallocBlock__: 0x600000243f90>
在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。这一句验证怎么不对呢
Block的复制
- 在全局block调用copy 什么也不做
- 在栈上调用copy那么复制到堆上
- 在堆上调用copy block 引用计数增加
6、block的实质是什么
一个对象,一个结构体
有isa指针指向自己的类(global malloc stack),
有desc结构体描述block的信息,forwarding指向自己或堆上自己的地址,
如果block对象截获变量,这些变量也会出现在block结构体中。
最重要的block结构体有一个函数指针,指向block代码块。block结构体的构造函数的参数,包括函数指针,描述block的结构体,自动截获的变量(全局变量不用截获),引用到的block变量。(__block对象也会转变成结构体)
block代码块在编译的时候会生成一个函数,函数第一个参数是前面说到的block对象结构体指针。执行block,相当于执行block里面__forwarding里面的函数指针。
__block_impl结构体定义
// 结构体 __block_impl 的声明
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
实质的更多描述
1、默认block的类型是NSGlobalBlock,一旦在block中引用自动变量(包括OC对象),无论是否修改,block类型变为NSMallocBlock。
2、默认情况下,在block内不能对截获的自动变量(或OC对象)进行修改,编译器会报错。
3、默认情况下,在block中引用的自动变量或OC对象(包括调用OC对象方法),block中的自动变量地址不同,而引用前后自动变量的地址一致
4、在自动变量(或OC对象)前添加__block,无论是否修改自动变量,引用后与block自动变量的地址一致,而且与引用前不一致。
另外,需要注意的一点是,block目前没有实现对C语言数组的截获
7、block和函数指针的理解
相似点:
1、函数指针和Block都可以实现回调的操作,声明上也很相似,实现上都可以看成是一个代码片段。
2、函数指针类型和Block类型都可以作为变量和函数参数的类型。(typedef定义别名之后,这个别名就是一个类型)
不同点:
1、函数指针只能指向预先定义好的函数代码块(可以是其他文件里面定义,通过函数参数动态传入的),函数地址是在编译链接时就已经确定好的。
2、Block本质是Objective-C对象,是NSObject的子类,可以接收消息。
3、函数里面只能访问全局变量,而Block代码块不光能访问全局变量,还拥有当前栈内存和堆内存变量的可读性(当然通过__block访问指示符修饰的局部变量还可以在block代码块里面进行修改)。
4、从内存的角度看,函数指针只不过是指向代码区的一段可执行代码,而block实际上是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期。