提到内存泄露,很多人就会想到循环引用,再想到没有走dealloc方法,甚至把这三者等同起来。其实这是不对的,下面我用一个例子来说明这个问题。但是我想先问一个问题,我在某个控制器的viewDidLoad
写了这句代码 [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeChange) userInfo:nil repeats:YES];
。大家肯定知道这个控制器释放不了了,那大家知道它内存泄露的原因吗?
其实循环引用只是导致内存泄露的一种情况,虽然也是主要的情况。还有一种是单例或者类单例引起的内存泄露。某个对象的引用计数始终大于等于1的话,那么系统就不会回收它,它就内存泄露了。比如以上情况,NSTimer对象会被加入到当前的RunLoop,而整个程序运行期间,主线程只有一个(类似于单例),而这个timer会强引用它的target,也就是self对象,所以就有 主线程强引用timer对象强引用self。导致self的引用计数始终大于等于1。
也许这个例子还不好理解,那么我就再举一个单例造成内存泄露的例子。上代码
#import <Foundation/Foundation.h>
typedef void(^MyBlock)(void);
@interface DXSingleObj : NSObject // DXSingleObj是一个单例
+ (instancetype)sharedInstance;
@property (nonatomic, copy) MyBlock myBlock;
@end
#import "DXSingleObj.h"
@interface DXSingleObj ()
@end
@implementation DXSingleObj
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
static id instance;
dispatch_once(&onceToken, ^{
instance = [self new];
});
return instance;
}
- (void)dealloc
{
NSLog(@"%s",__FUNCTION__);
}
@end
我们在某个控制器的viewDidLoad
加上如下代码
- (void)viewDidLoad {
[super viewDidLoad];
DXSingleObj *dxObj = [DXSingleObj sharedInstance];
dxObj.myBlock = ^{
NSLog(@"%@",self.view);
};
}
这种情况下,这个控制器就不会释放了,退出这个控制器,它的dealloc方法也不会走。 引用关系就是 单例强引用block属性强引用self。导致self不能被释放。
解决方法也很简单,加一个weak,代码如下
- (void)viewDidLoad {
[super viewDidLoad];
__weak DXViewController *weakSelf = self;
DXSingleObj *dxObj = [DXSingleObj sharedInstance];
dxObj.myBlock = ^{
NSLog(@"%@", weakSelf.view);
};
}
看起来weak解决内存泄露好像很好用,那么回到上面这句代码,我们把self改为weakSelf,那么它还会内存泄露吗?[NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timeChange) userInfo:nil repeats:YES];
其实这里传weakSelf还是没用。到这里是不有点迷糊了?不是说好的weak是弱引用,我传弱引用还不能打断引用链条吗?其实weak和weakSelf不是万能的,要看到底能不能改变这个属性的强弱引用。因为NSTimer内部有一个strong类型的target成员变量,_target = target,我们传weakSelf的话,就是_target = weakTarget。所以weak到底能否生效的话是看weak能否影响这个属性的强弱类型。
所以循环引用只是内存泄露的子集。判断这个对象是否内存泄露要看它的引用计数,只要引用计数一直大于等于1,就内存泄露了。
如果控制器的dealloc方法走了,那么是不是说明一定就没有内存泄露了? 其实不是的,控制器的dealloc方法走了,只是说明这个控制器释放了,但不能说明它里面没有内存泄露。请看例子
// 这是一个普通类的头文件
#import <Foundation/Foundation.h>
@interface DXAction : NSObject
- (void)doAction;
- (void)cancelAction;
@end
// 再看.m文件
#import "DXAction.h"
@implementation DXAction
- (void)doAction
{
NSLog(@"doAction---doAction");
[self performSelector:@selector(doAction) withObject:nil afterDelay:1];
}
- (void)cancelAction
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
// 再看控制器的代码
#import "DXViewController.h"
#import "DXAction.h"
@interface DXViewController ()
@property (nonatomic, strong) DXAction *action;
@end
@implementation DXViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor orangeColor];
DXAction *action = [[DXAction alloc] init];
[action doAction];
self.action = action;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dealloc
{
NSLog(@"%s",__FUNCTION__);
}
@end
点击进入控制器,然后退出控制器回到上个控制器,看到打印如下:
doAction---doAction
doAction---doAction
-[DXViewController dealloc]
doAction---doAction
说明控制器销毁了,但是这个DXAction
对象还是没有销毁,内存泄露了。先说一下DXAction
对象为啥不能销毁,这也是performSelector: withObject: afterDelay:
使用不当造成内存泄露的原因, 这个方法会被当前线程(这里是主线程)强引用。除非这个方法执行完,否则self对象会一直被主线程强引用,不能释放。
我们平时的习惯是看一下控制器的dealloc
方法有没有走,走了就觉得这个控制器没有内存泄露了,其实这是不完全可靠的。
下面说一下我们经常用的dispatch_after
(现在dispatch_after支持取消啦GCD延时取消),如果这个延时代码写在控制器里面
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 写代码
});
如果这个block里面没有self,那么退出界面,这个block还是会照常执行的。原因是因为主线程强引用这个block,直到block执行完才会将它置空。如果block里面有self的话,那么只有等到执行完这个block,那么这个控制器才能释放。控制器的延时释放,可能会有一些问题。
刚好这个dispatch_after
可以借机来说一下weak-strong dance
,还是上代码
#import "DXViewController.h"
@interface DXViewController ()
@property (nonatomic, copy) NSString *testStr;
@end
@implementation DXViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor orangeColor];
self.testStr = @"test";
__weak DXViewController *weakSelf = self;
// 这里用dispatch_after的block模拟平时的block循环引用
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *arr = [NSMutableArray array];
[arr addObject:weakSelf.testStr];
}
我见过很多人说,我一直用weak,没有配合strong一起使用,也从没出现过问题啊。那么,你跑一下这个代码,工程就会崩溃。什么时候用weak大家应该都比较清楚了,那么strong啥时候必须要配合使用,啥时候不用strong其实也没问题呢?
我先贴一下weak配合strong使用的标准代码吧,这也是苹果文档推荐的
self.testStr = @"test";
__weak DXViewController *weakSelf = self; // 1. 先用weakSelf
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong DXViewController *strongSelf = weakSelf; //2. 再用strongSelf
NSMutableArray *arr = [NSMutableArray array];
if (strongSelf) { // 3. strongSelf判空
[arr addObject:strongSelf.testStr];
}
else
{
NSLog(@"strongSelf为空啦");
}
});
跑一下这个工程,进入控制器后立即退出控制器,就会出现如下打印
-[DXViewController dealloc]
strongSelf为空啦
__strong DXViewController *strongSelf = weakSelf;
这个strongSelf只是一个局部变量,超过作用域后会释放,所以不会造成内存泄露的问题,而strongSelf又保证了在block执行期间,weakSelf不会中途被释放(前提是进入block时weakSelf就不为空)。但是__strong DXViewController *strongSelf = weakSelf;
这句代码起不到使weakSelf不为空的作用,所以还是要对strongSelf
判空一下。否则跑上面的代码,去掉strongSelf
判断空就会崩溃。这才是完整的weak-strong dance。
其实如果能保证weakSelf
在block执行完以前肯定不会为空的话,那么__strong DXViewController *strongSelf = weakSelf;
这句代码可以不用写的。如果不是对内存管理非常清楚的话,还是建议按上面这样写,只要使用了weak就一定配合strong使用,并且对strongSelf判空。
结论,那么平时应该如何正确使用weak呢?首先,weak不能乱用(weak比较耗性能),确定会造成block循环引用的情况下,用weak解决,然后用了weak就配合strong一起用,并且做好strongSelf判空。 当前,如果对这个weak的使用一点都不懂的话,看到block就用weak吧🙂。(这种人我还真见过不少)
最后出2个题,大家也经常遇到的
1.这段代码写在viewDidLoad中,会有问题吗? 如果造成了控制器释放不了,请写出它的引用链条。
self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
NSLog(@"%@--self",self);
}];
2.DXBlockClass类定义如下,
#import <Foundation/Foundation.h>
typedef void(^Block)(void);
@interface DXBlockClass : NSObject
@property (nonatomic, copy) Block myBlock;
+ (void)doBlock:(Block)block;
- (void)doBlock:(Block)block;
@end
#import "DXBlockClass.h"
@implementation DXBlockClass
+ (void)doBlock:(Block)block
{
if (block) {
block();
}
}
- (void)doBlock:(Block)block
{
_myBlock = block;
if (block) {
block();
}
}
@end
在某个控制器的viewDidLoad写在这个代码,需要用weak解循环引用吗?为什么不会造成内存泄露(控制器的dealloc方法会走)?
- (void)viewDidLoad {
[super viewDidLoad];
DXBlockClass *blockClass = [[DXBlockClass alloc] init];
[DXBlockClass doBlock:^{
NSLog(@"%@-类方法-",self);
}];
[blockClass doBlock:^{
NSLog(@"%@-对象方法-",self);
}];
}
假如以前你对啥时候该用weak解内存泄露,啥时候该配合strong一起使用还不是非常了解的话。读完了这篇文章,如果能对weak和strong的使用了然于心的话,那么就请点一个喜欢吧。