参考: iOS中block的使用、实现底层、循环引用、存储位置
一:Block的使用格式和用途
- 1,声明和定义格式
// 声明:返回值(^block变量名)(参数)
void(^block)();
// 定义
// 方式一:
void(^block1)() = ^(){
NSLog(@"调用block1");
};
// 调用Block,就会去查看下Block保存代码
block1();
// 方式二:block如果没有参数,可以省略()
// void(^)()
void(^block2)() = ^{
};
// 方式三:block定义中,返回值可以省略
// 类型:int(^)()
int(^block3)() = ^int{
return 2;
};
// 作用:保存一段代码
// 类型:int(^)()
// 快捷方式:inline
void(^block4)(int) = ^(int a) {
};
//扩展:如果在定义block的时候在尾部加上括号意味着立即调用block
int(^block3)() = ^int{
return 2;
} ();
-
2, block开发中用途
(1) Block:在一个方法中定义,在另一个方法中调用 (不常用) 保存代码(2) Block:在一个类中定义,在另一个类中调用(常用), -> 代替delegate模式传值
ViewController.m文件
#import "ViewController.h"
#import "ModalViewController.h"
@interface ViewController ()<ModalViewControllerDelegate>
@end
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
ModalViewController *modalVc = [[ModalViewController alloc] init];
//用代理方法实现传值
// modalVc.delegate = self;
//用block实现传值
modalVc.valueBlock = ^(NSString *value){
NSLog(@"接收到%@",value);
};
modalVc.view.backgroundColor = [UIColor yellowColor];
[self presentViewController:modalVc animated:YES completion:nil];
}
#pragma mark -ModalViewControllerDelegate 实现代理方法
- (void)modalViewController:(ModalViewController *)modalVc reciverStr:(NSString *)str
{
NSLog(@"接收到值%@",str);
}
@end
ModalViewController.h文件
#import <UIKit/UIKit.h>
@class ModalViewController;
@protocol ModalViewControllerDelegate <NSObject>
@optional
// 代理方法:想要告诉代理做什么事情
- (void)modalViewController:(ModalViewController *)modalVc reciverStr:(NSString *)str;
@end
@interface ModalViewController : UIViewController
@property (nonatomic ,copy) void(^valueBlock)(NSString *value);
@property (nonatomic, weak) id<ModalViewControllerDelegate> delegate;
@end
ModalViewController.m文件
#import "ModalViewController.h"
@interface ModalViewController ()
@end
@implementation ModalViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//利用block传值给ViewController
if (_valueBlock) {
_valueBlock(@"123");
}
// 利用delegate传值给ViewController
/*
if ([_delegate respondsToSelector:@selector(modalViewController:reciverStr:)]) {
[_delegate modalViewController:self reciverStr:@"123"];
}
}
*/
@end
(3) Block作为方法参数直接传入,起延迟执行代码块的作用
使用场景: 当自己封装一个类的时候,定义方法可以留个接口具体做什么由外部使用者决定,但是什么时候去做,由内部自己决定,这时候采取使用block做方法参数,给外界一个传入代码块的接口
二: 使用Block需要注意:
- 1、在使用block前要明确block参数的传递形式,需要对block指针做判空处理。
block参数传递:
// 如果局部变量被static,__block,那么都是指针传递
// 全局变量.也是指针传递
// __block int a = 5;
int a = 5;//值传递
// 默认局部变量在block中 是 值传递
void(^block)() = ^{
NSLog(@"%d",a);
};
a = 10;
block();
不判空直接使用,一旦指针为空直接产生崩溃。
if (!self.isOnlyNet) {
if (succBlock == NULL) { //后面使用block之前要先做判空处理
return;
}
id data = [NSKeyedUnarchiver unarchiveObjectWithFile:[self favoriteFile]];
if ([data isKindOfClass:[NSMutableArray class]]) {
succBlock(data,YES);
}else{
succBlock(nil,YES);
}
}
- 2、在MRC的编译环境下,block如果作为成员参数要copy一下将栈上的block拷贝到堆上
- 3、在block使用之后要对,block指针做赋空值处理,如果是MRC的编译环境下,要先release掉block对象。
block作为类对象的成员变量,使用block的人有可能用类对象参与block中的运算而产生循环引用。
将block赋值为空,是解掉循环引用的重要方法。(不能只在dealloc里面做赋空值操作,这样已经产生的循环引用不会被破坏掉)
typedef void(^SuccBlock)(id data);
@interface NetworkClass {
SuccessBlock _sucBlock;
}
@property (nonatomic,assign)BOOL propertyUseInCallBack;
- (void) requestWithSucBlock: (SuccessBlock) callbackBlock;
@end
@implementation NetworkClass
- (void) requestWithSucBlock: (SuccessBlock) callbackBlock {
_sucBlock = callbackBlock;//MRC下:_sucBlock = [callbackBlock copy]; 不copy block会在栈上被回收。
}
- (void) netwrokDataBack: (id) data {
if (data != nil && _sucBlock != NULL) {
_sucBlock(data);
}
//MRC下:要先将[_sucBlock release];(之前copy过)
_sucBlock = nil; //Importent: 在使用之后将Block赋空值,解引用 !!!
}
@end
//=======================以下是使用方===========================
@implementation UserCode
- (void) temporaryNetworkCall
{
NetworkClass *netObj = [[NetworkClass alloc] init];
netObj.propertyUseInCallBack = NO;
[netObj requestWithSucBlock: ^(id data) {
//由于block里面引用netObj的指针所以这里产生了循环引用,且由于这个block是作为参数传入对象的,编译器不会报错。
//因此,NetworkClass使用完block之后一定要将作为成员变量的block赋空值。
if (netObj.propertyUseInCallBack == YES) {
//Do Something...
}
}];
}
@end
还有一种改法,在block接口设计时,将可能需要的变量作为形参传到block中,从设计上解决循环引用的问题。
如果上面Network类设计成这个样子:
@class NetowrkClass;
typedef void(^SuccBlock)(NetworkClass *aNetworkObj, id data);
@interface NetworkClass
//...
@end
@implementation NetworkClass
@end
@implementation UserCode
- (void) temporaryNetworkCall
{
NetworkClass *netObj = [[NetworkClass alloc] init];
netObj.propertyUseInCallBack = NO;
[netObj requestWithSucBlock: ^(NetworkClass *aNetworkObj, id data) {
//这里参数中已经有netObj的对象了,使用者不用再从block外引用指针了。
if (aNetworkObj.propertyUseInCallBack == YES) {
//Do Something...
}
}];
}
4、使用方将self或成员变量加入block之前要先将self变为__weak
5、在多线程环境下(block中的weakSelf有可能被析构的情况下),需要先将self转为strong指针,避免在运行到某个关键步骤时self对象被析构。
第四、第五条合起来有个名词叫weak–strong dance,来自于2011 WWDC Session #322 (Objective-C Advancements in Depth)
以下代码来自AFNetworking,堪称使用weak–strong dance的经典
__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
__strong __typeof(weakSelf)strongSelf = weakSelf;
strongSelf.networkReachabilityStatus = status;
if (strongSelf.networkReachabilityStatusBlock) {
strongSelf.networkReachabilityStatusBlock(status);
}
};
第一行:__weak __typeof(self)weakSelf = self;
如之前第四条所说,为防止callback内部对self强引用,weak一下。
其中用到了__typeof(self),这里涉及几个知识点:
a. __typeof、__typeof__、typeof的区别
恩~~他们没有区别,但是这牵扯一段往事,在早期C语言中没有typeof这个关键字,__typeof、__typeof__是在C语言的扩展关键字的时候出现的。
typeof是现代GNU C++的关键字,从Objective-C的根源说,他其实来自于C语言,所以AFNetworking使用了继承自C的关键字。
第三行:__strong __typeof(weakSelf)strongSelf = weakSelf;
按照之前第五条的说法给转回strong了,这里__typeof()里面写的是weakSelf,里面写self也没有问题,因为typeof是编译时确定变量类型,所以这里写self 不会被循环引用。
第四、五、六行,如果不转成strongSelf而使用weakSelf,后面几句话中,有可能在第四句执行之后self的对象可能被析构掉,然后后面的StausBlock没有执行,导致逻辑错误。
最后第五行,使用前对block判空。
详解使用 weak–strong dance 技术来避免循环引用
内联 block 可以直接引用 self,但是要非常小心地在 block 中引用 self。因为在一些内联 block 引用 self,可能会导致循环引用。如下例所示:
@interface KSViewController ()
{
id _observer;
}
@end
@implementation KSViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
KSTester * tester = [[KSTester alloc] init];
[tester run];
_observer = [[NSNotificationCenter defaultCenter]
addObserverForName:@"TestNotificationKey"
object:nil queue:nil usingBlock:^(NSNotification *n) {
NSLog(@"%@", self);
}];
}
- (void)dealloc
{
if (_observer) {
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
}
}
在上面代码中,我们添加向通知中心注册了一个观察者,然后在 dealloc 时解除该注册,一切看起来正常。但这里有两个问题:
a) 在消息通知 block 中引用到了 self,在这里 self 对象被 block retain,而 _observer 又 retain 该 block的一份拷贝,通知中心又持有 _observer。因此只要 _observer 对象还没有被解除注册,block 就会一直被通知中心持有,从而 self 就不会被释放,其 dealloc 就不会被调用。而我们却又期望在 dealloc 中通过 removeObserver 来解除注册以消除通知中心对 _observer/block 的 retain。
b) 同时,_observer 是在 self 所在类中定义赋值,因此是被 self retain 的,这样就形成了循环引用。
上面的过程 a) 值得深入分析一下:
苹果官方文档中对 addObserverForName:object:queue:usingBlock: 中的 block 变量说明如下:
The block is copied by the notification center and (the copy) held until the observer registration is removed.
因此,通知中心会拷贝 block 并持有该拷贝直到解除 _observer 的注册。在 ARC 中,在被拷贝的 block 中无论是直接引用 self 还是通过引用 self 的成员变量间接引用 self,该 block 都会 retain self。
这个问题,可以用 weak–strong dance 技术来解决
__weak KSViewController * wself = self;
_observer = [[NSNotificationCenter defaultCenter]
addObserverForName:@"TestNotificationKey"
object:nil queue:nil usingBlock:^(NSNotification *n) {
KSViewController * sself = wself;
if (sself) {
NSLog(@"%@", sself);
}
else {
NSLog(@"<self> dealloc before we could run this code.");
}
}];
下面来分析为什么该手法能够起作用:
首先,在 block 之前定义对 self 的一个弱引用 wself,因为是弱引用,所以当 self 被释放时 wself 会变为 nil;然后在 block 中引用该弱应用,考虑到多线程情况,通过使用强引用 sself 来引用该弱引用,这时如果 self 不为 nil 就会 retain self,以防止在后面的使用过程中 self 被释放;然后在之后的 block 块中使用该强引用 sself,注意在使用前要对 sself 进行了 nil 检测,因为多线程环境下在用弱引用 wself 对强引用 sself 赋值时,弱引用 wself 可能已经为 nil 了。
通过这种手法,block 就不会持有 self 的引用,从而打破了循环引用。
- 6, 扩展:其他还需要注意避免循环引用的地方
与此类似的情况还有 NSTimer。苹果官方文档中提到"Note in particular that run loops retain their timers, so you can release a timer after you have added it to a run loop.",同时在对接口
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
的 target 说明文档中提到:
The object to which to send the message specified by aSelector when the timer fires. The target object is retained by the timer and released when the timer is invalidated.
结合这两处文档说明,我们就知道只要重复性 timer 还没有被 invalidated,target 对象就会被一直持有而不会被释放。因此当你使用 self 当作 target 时,你就不能期望在 dealloc 中 invalidate timer,因为在 timer 没有被invalidate 之前,dealloc 绝不会被调用。因此,需要找个合适的时机和地方来 invalidate timer,但绝不是在 dealloc 中。
- 7,block 内存管理分析
block 其实也是一个 NSObject 对象,并且在大多数情况下,block 是分配在栈上面的,只有当 block 被定义为全局变量或 block 块中没有引用任何 automatic 变量时,block 才分配在全局数据段上。 __block 变量也是分配在栈上面的。
在 ARC 下,编译器会自动检测为我们处理了 block 的大部分内存管理,但当将 block 当作方法参数时候,编译器不会自动检测,需要我们手动拷贝该 block 对象。幸运的是,Cocoa 库中的大部分名称中包含”usingBlock“的接口以及 GCD 接口在其接口内部已经进行了拷贝操作,不需要我们再手动处理了。但除此之外的情况,就需要我们手动干预了。
- (id) getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:
^{ KSLog(@" > block 0:%d", val); }, // block on the stack
^{ KSLog(@" > block 1:%d", val); }, // block on the stack
nil];
// return [[NSArray alloc] initWithObjects:
// [^{ KSLog(@" > block 0:%d", val); } copy], // block copy to heap
// [^{ KSLog(@" > block 1:%d", val); } copy], // block copy to heap
// nil];
}
- (void)testManageBlockMemory
{
id obj = [self getBlockArray];
typedef void (^BlockType)(void);
BlockType blockObject = (BlockType)[obj objectAtIndex:0];
blockObject();
}
执行上面的代码中,在调用 testManageBlockMemory 时,程序会 crash 掉。因为从 getBlockArray 返回的 block 是分配在 stack 上的,但超出了定义 block 所在的作用域,block 就不在了。正确的做法(被屏蔽的那段代码)是在将 block 添加到 NSArray 中时先 copy 到 heap 上,这样就可以在之后的使用中正常访问。
在 ARC 下,对 block 变量进行 copy 始终是安全的,无论它是在栈上,还是全局数据段,还是已经拷贝到堆上。对栈上的 block 进行 copy 是将它拷贝到堆上;对全局数据段中的 block 进行 copy 不会有任何作用;对堆上的 block 进行 copy 只是增加它的引用记数。
如果栈上的 block 中引用了__block 类型的变量,在将该 block 拷贝到堆上时也会将 __block 变量拷贝到堆上如果该 __block 变量在堆上还没有对应的拷贝的话,否则就增加堆上对应的拷贝的引用记数。