前言
对于iOS开发人员来说,block可以说是很熟悉的了。在平时的开发中,我们经常会使用到block作为回调,或作为一个属性,或用作方法的参数。当然,在面试中,block相关的问题,也会经常被问到,例如block的分类等。本篇我们就来进行一下block的基础探索,主要分为两个方面:
block有哪几种
block的循环引用问题
一、block的分类
block还有分类吗?答案当然是有的。为了验证这一点,我们新建一个iOS工程,在viewDidLoad方法中,写下如下代码:
int a = 10;
void (^myBlock)(void) = ^{
};
void (^testBlock)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", myBlock);
NSLog(@"%@", testBlock);
分别打印两个block的结果如下:
可以发现myBlock
和testBlock
虽然看起来是一样的,都是无参数无返回值的,但是却分别为两种不同类型__NSGlobalBlock__
和__NSMallocBlock__
,也就是全局block
和堆block
。
对比两者,其唯一的区别就在于testBlock
引用了一个外部的局部变量a,那么将testBlock
中的代码注释掉呢?其结果如下:
结果表明只要不引用外部的局部变量,testBlock
也是一个全局block。那么我们再试一下,testBlock
引用一个全局变量或静态变量:
int globalA = 20; // 新增
static int b = 30; // 新增
int a = 10;
void (^myBlock)(void) = ^{
};
void (^testBlock)(void) = ^{
NSLog(@"%d", a);
};
// 新增
void (^globalVarBlock)(void) = ^{
NSLog(@"%d", globalA);
};
// 新增
void (^staticTestBlock)(void) = ^{
NSLog(@"%d", b);
};
NSLog(@"%@", myBlock);
NSLog(@"%@", testBlock);
NSLog(@"%@", globalVarBlock); // 新增
NSLog(@"%@", staticTestBlock); //新增
打印结果如下:
结果显示,当引用全局变量和静态变量时,globalVarBlock
和staticTestBlock
依然是全局block。因此可以发现,block是否为全局block,在于其是否捕获外部局部变量。
常说堆栈,既然有堆block
,那么有没有栈block
呢?当然有。在上面的demo中,再加一个例子进行测试:
int a = 10;
void (^testBlock)(void) = ^{
NSLog(@"%d", a);
};
void (^ __weak weakBlock)(void) = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", testBlock);
NSLog(@"%@", weakBlock);
打印结果如下:
结果显示,当加上__weak
修饰后,weakBlock
的类型就变成了__NSStackBlock__
,也即栈block。
我们继续测试,将weakBlock
拷贝给另一个block,代码如下:
int a = 10;
void (^testBlock)(void) = ^{
NSLog(@"%d", a);
};
void (^ __weak weakBlock)(void) = ^{
NSLog(@"%d", a);
};
void (^ copyBlock)(void) = [weakBlock copy];
NSLog(@"%@", testBlock);
NSLog(@"%@", weakBlock);
NSLog(@"%@", copyBlock);
结果如下所示: 很显然,将一个栈block拷贝给一个变量后,新的变量不再是栈block,而是一个堆block。可以发现,testBlock是因为强引用,copyBlock是因为copy,两者皆是堆block,也就是说堆block和栈block的区别为是否是强引用或copy。
不过上面这个结果并不能信服,因为我们使用的都是局部变量。下面我们分别看下,block作为返回值和属性的情况。
1、作为方法返回值
首先新写一个方法,并且分别用__weak接收
和不用__weak接收
,代码如下:
- (void (^)(void))getBlock {
int a = 10;
return ^{
NSLog(@"%d", a);
};;
}
void (^ returnBlock)(void) = [self getBlock];
void (^ __weak weakReturnBlock)(void) = [self getBlock];
NSLog(@"%@", returnBlock);
NSLog(@"%@", weakReturnBlock);
打印结果如下:
结果显示,不管是否使用__weak修饰,block作为方法参数返回值时,都会拷贝到堆区。
2、作为属性 新建三个block属性,分别使用copy、strong、weak修饰,然后赋值相同的block,代码如下:
@property (nonatomic, copy) void (^blockCopy)(void);
@property (nonatomic, strong) void (^blockStrong)(void);
@property (nonatomic, weak) void (^blockWeak)(void);
self.blockCopy = ^{
NSLog(@"%d", a);
};
self.blockStrong = ^{
NSLog(@"%d", a);
};
self.blockWeak = ^{
NSLog(@"%d", a);
};
NSLog(@"%@", self.blockCopy);
NSLog(@"%@", self.blockStrong);
NSLog(@"%@", self.blockWeak);
打印结果如下:
结果也是均为堆block。
根据测试结果,可以总结如下:
- block可以分为三种,
全局block、堆block和栈block
,分别对应__NSGlobalBlock__
、__NSMallocBlock__
和__NSStackBlock__
三种类型 - 不引用外部局部变量,或只引用静态或全局变量的为全局block
- 使用外部局部变量的为堆block或栈block,两者区别在于:
在函数内部使用__weak修饰的为栈block
赋值给强引用或者手动copy的为堆block
作为方法返回值或者属性的也为堆block
二、block与循环引用
在iOS中采用的是ARC的内存管理方式,其中很重要的一点就是引用计数方式,也就是说当一个对象被alloc、copy或者retain
时,其引用计数就会加1
,当一个对象的引用计数为0时,就表示该对象不再被引用,可以释放了。
如果这种引用关系是单向的,例如 A -> B -> C
,那么就不会有问题。但是,在实际的开发中,经常会存在两个对象相互引用的情况,例如A<=>B
或者 A -> B -> C -> A
,像这样就构成了一个引用的闭环,即循环引用,如果这个环无法打破,其结果就是对象无法释放,造成内存泄漏。
说到循环引用,很多人第一个想到的就是block,而提起block,也会自然想到循环引用。可见两者之间的联系十分紧密。事实上,block使用不当确实会造成循环引用,但是OC中引起循环引用的除了block之外,还有其他的方式,例如代理。下面我们就一起探索下,引起循环引用的方式,以及如何解决循环引用问题。
2.1 如何检测循环引用
工欲善其事,必先利其器。探索循环引用之前,我们首先要知道如何检测循环引用,这里有两种方法可以供我们使用。
-
1、第一种也是最为熟知的一种,就是在类中重写
dealloc方法
,该方法是OC类的析构方法,当对象被释放前,会调用该方法,如果对象没有被释放则不会调用。这一方式大家非常熟悉,本篇就不再演示。- 这种方式可以很准确的检测出对象是否释放,一旦发现该方法没有调用,就可以考虑是否出现了循环引用。
- 不过这种方式有一个局限性,就是我们需要提前知道是哪些类需要进行检测,但是如果我们想看下工程中是否出现了内存泄漏,就不是那么容易做到了。
-
2、还有一种是利用XCode的性能检测工具
Instruments
中的Allocations
工具来检测,通过该工具启动工程,就可以检测到运行时对象的开辟和释放情况,而且很方便的一点是我们不需要知道,。本文的demo使用的是XCode 13.1
,Instruments
的打开方式是左上XCode菜单 -> Open Developer Tool -> Instruments
,打开后如下图所示:
按照步骤1和步骤2选择好后,点击Choose,进入下一界面:
如图所示,进入LeakViewController
页面两次,第二次进入没有退出的情况下,内存中应该只有一个对象,但是结果显示的是两次。这说明LeakViewController
和LeakShowTool
在第一次退出时没有被释放,可以检查是否发生了循环引用(事实上确实有循环引用,为了测试专门写的)。
2.2 代理为何不使用strong
在平时用到代理时,我们都是使用weak进行修饰,但是为什么呢?如果不使用weak,换成strong或者copy会怎样呢?我们可以做实验看下(实际上,上一节中用到的就是这个例子),将一个代理的属性修饰符改为strong,代码如下:
// LeakShowTool部分
@protocol LeakProtocol <NSObject>
- (void)show;
@end
@interface LeakShowTool : NSObject
@property (nonatomic, strong) id<LeakProtocol> delegate;
@end
// LeakViewController部分
@interface LeakViewController () <LeakProtocol>
@property (nonatomic, strong) LeakShowTool *tool;
@end
@implementation LeakViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tool = [[LeakShowTool alloc] init];
self.tool.delegate = self;
}
- (void)show {
NSLog(@"show");
}
@end
该实验的结果,如图所示: 当使用strong修饰代理属性时,立马也产生了循环引用,可见循环引用并不是block的专属。
此时发生循环引用的原因如下图:
如果改成weak,则是下面的情况:
2.3 block造成循环引用的原因
在上一小节中展示了使用代理造成循环引用的情况,本小节回归到block上来,继续探索下block造成循环引用原因,首先将代码改成block的形式,如下:
@interface ViewController ()
@property (nonatomic, assign) int result;
@property (nonatomic, strong) void(^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.block = ^{
NSLog(@"value = %d", self.result);
};
}
@end
这段代码不用运行也知道是会循环引用的,为了查看原因,我们先看下在底层,block是以一种怎样的形式存在的。
通过clang编译ViewController.m
,其命令如下:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m
编译后得到一个ViewController.cpp
文件,打开如下:
由于代码太多,这里只截图ViewDidLoad
和block
相关的代码。在底层ViewDidLoad
变成了_I_ViewController_viewDidLoad
,如图的最下面所示,在调用setBlock:
时,传入了一个参数,该参数是__ViewController__viewDidLoad_block_impl_0
类型,该类型其实是一个结构体,如图最上方。
在这个结构体中,可以看到一个成员ViewController *self;
,该成员的赋值可以在__ViewController__viewDidLoad_block_impl_0
的构造函数中找到,就是外部传入的self
。
结合上面的OC代码,最终的情况是self持有了block,block也持有了self,两者相互等待释放,形成了循环引用,如下图所示:
三、解决block循环引用的方式
上一节主要了解了造成循环引用的原因,本章节继续探索如何解决block的循环引用问题。解决循环引用问题,关键的一点是将对象间的引用闭环打破,当其中某一个环能够被销毁时,环上的其他对象也可以得以释放,从而解决问题。根据这一点,可以通过以下几种方式来解决循环引用,下面分别来看下。
3.1 _ _weak解决循环引用
首先在LeakViewController
中写下如下代码:
@interface LeakViewController ()
@property (nonatomic, assign) int result;
@property (nonatomic, strong) void(^block)(void);
@end
@implementation LeakViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"进入%@页面", [**self** class]);
self.block = ^{
NSLog(@"value = %d", self.result);
};
}
- (void)dealloc {
NSLog(@"%@--%s", [self class], __FUNCTION__);
}
@end
上诉代码肯定会造成循环引用,打印结果如下:
可以发现,退出页面后页面本该销毁,结果并没有走dealloc
方法。此时将代码改成如下所示:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"进入%@页面", [self class]);
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"value = %d", weakSelf.result);
};
}
执行结果如下:
很显然循环引用解决了,其原理如下:
不过上述代码存在一个问题,如果加上了延迟执行,则weakSelf会提前释放,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"进入%@页面", [self class]);
self.result = 2;
__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
NSLog(@"value = %d", weakSelf.result);
});
};
self.block();
}
执行结果如下:
后面两次是进入页面马上退出后的结果,很显然此时weakSelf
已经为nil
,所以打印结果value = 0
。正常情况下,这样写没有太大问题,但是如果在退出页面时,确实有延迟执行的需求,那么这种写法就会存在不可预知的风险。因此,如果有这种需求,可以将代码改成如下所示:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"进入%@页面", [self class]);
self.result = 2;
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
NSLog(@"value = %d", strongSelf.result);
});
};
self.block();
}
在block内部strongSelf
对weakSelf
做一次强引用,weakSelf
可以正常释放,strongSelf
因为是block内部的局部变量,所以当block释放时,strongSelf
也会被释放。打印结果如下:
3.2 临时变量和参数
上面__weak和__strong的方式中,strongSelf
实际上只是一个局部变量,通过这种方式避免了self的释放后代码块中的代码执行异常。借助这种思想,我们可以通过使用局部变量的方式来打破循环引用。
在OC中,可以通过__block来实现,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"进入%@页面", [self class]);
self.result = 2;
__block LeakViewController *vc = self;
self.block = ^{
NSLog(@"value = %d", vc.result);
vc = nil;
};
self.block();
}
执行结果如下:
这种方式也可以打破循环引用的闭环,而且不用担心延迟执行时的提前释放问题,只要在代码块只想结束时将vc
这一临时变量置为nil
。
不过这种实现方式并不优雅,每次都要手动将vc置为nil,增加了操作成本,并且难保不会忘记,一旦忘记还是会造成循环引用。所以,可以进行如下改动:
@property (nonatomic, strong) void(^block)(LeakViewController *vc); // 将block的定义修改下,将self作为一个参数传入
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"进入%@页面", [self class]);
self.result = 2;
self.block = ^(LeakViewController *vc){
NSLog(@"value = %d", vc.result);
};
self.block(self);
}
因为vc是block的一个参数,当代码块执行完毕后,vc会自动被置为nil,从而打破了循环引用。
总结
本篇探索了block的基础,包含了block的分类以及循环引用问题,总结下来,有以下几点:
- block总体上分为三种
__NSGlobalBlock__
、__NSMallocBlock__
和__NSStackBlock__
- 不引用外部的局部变量的为
__NSGlobalBlock__
,否则为__NSMallocBlock__
或__NSStackBlock__
__NSMallocBlock__
和__NSStackBlock__
的区别在于是否是强引用或者copy,如果是则为__NSMallocBlock__
,否则为__NSStackBlock__
- 不引用外部的局部变量的为
- block使用不当,会造成循环引用,即对象的引用形成了一个闭环,最终相互等待,谁也无法释放
- 解决block循环引用的方式有
__weak和__strong
、临时变量和参数
以上为对于block的基础总结,其中对于block如何拷贝self和其他变量,本篇只是简略的提及,后续会继续探索,期待继续关注,对于本篇中表述不当的地方,也欢迎大家指正。