一.概述
多个对象相互持有,A对象强引用B对象,同时B对象也强引用于A对象,两者相互等待对方发消息告诉自己需要Release,一直等待,形成闭环,内存无法释放,导致内存泄露。
iOS内存中的分区有:堆、栈、静态区。其中,栈和静态区是操作系统自己管理回收,不会造成循环引用。所以我们只需要关注堆的内存分配,循环引用会导致堆里的内存无法正常回收。
栈区:由编译器自动分配释放, 存放函数的参数值, 局部变量的值等。
堆区:一般由程序员分配释放,存放new,alloc等关键字创造的对象。
二.产生及解决方法
1.Block
首先我们要先了解block为什么要用copy修饰?
block是一个对象,在创建时内存默认分配在栈上,不是堆上,所以它的作用域仅限创建时候的当前上下文(函数, 方法...), 当我们在该作用域外调用该block时, 程序就会崩溃.
所以当我们需要在block定义域以外的地方使用时就需要用到Copy,将block从内存栈区移到堆区。
Block引起循环引用的几种场景及解决方案?
(1).@property修饰的属性
@interface Page1ViewController ()
@property(copy,nonatomic)dispatch_block_t block;
@property(nonatomic,strong)NSString *str;
@end
@implementation Page1ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.str = @"123";
self.block = ^{
NSLog(@"%@**********",self.str);
};
}
self将block作为自己的属性变量,持有block对象,而在堆中的block的方法体里面又引用了 self ,就会导致循环引用。
Xcode也很人性化的提示:Capturing 'self' strongly in this block is likely to lead to a retain cycle,由此我们也会注意到自己这里编写不规范。
解决方法:
@property(copy,nonatomic)dispatch_block_t block;
@property(nonatomic,strong)NSString *str;
@end
@implementation Page1ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.str = @"123";
__weak typeof(self) weakself = self;
self.block = ^{
NSLog(@"%@**********",weakself.str);
};
}
当两个对象相互强引用对方时,我们需要把其中一方变为弱引用,这里我们把self利用__weak变成了弱引用,解决了这种循环引用的问题!
(2).不用@property修饰的私有属性或全局变量
@interface Page1ViewController ()
@property(copy,nonatomic)dispatch_block_t block;
@end
@implementation Page1ViewController
{
NSString *_str;//全局变量
}
- (void)viewDidLoad {
[super viewDidLoad];
_str = @"123";
self.block = ^{
NSLog(@"%@**********",_str);
};
}
编译器报警告:
Block implicitly retains 'self'; explicitly mention 'self' to indicate this is intended behavior
Insert 'self->'
Capturing 'self' strongly in this block is likely to lead to a retain cycle
编译器建议我们用self->去修饰,然后self用weakself来代替即为:weakself->时,如下图报错:Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to strong variable first。
根据提示需要使用一个strong类型,因此在Block里头使用__strong转一下weakSelf即可:
- (void)viewDidLoad {
[super viewDidLoad];
_str = @"123";
__weak typeof(self) weakself = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%@**********",strongSelf->_str);
};
}
Block使用 __weak可能会引起内存提前释放的问题?
- (void)viewDidLoad {
[super viewDidLoad];
_str = @"123";
__weak typeof(self) weakself = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakself.str);
});
};
}
在上面代码中,在5s内的时间,该控制器pop到上一级,该控制器执行dealloc方法被销毁,内存被提前释放,从而weakself.str即为null。
解决办法( __strong):
- (void)viewDidLoad {
[super viewDidLoad];
_str = @"123";
__weak typeof(self) weakself = self;
self.block = ^{
__strong typeof(self) strongSelf = weakself;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", strongSelf.str);
});
};
}
原理:用__weak来解决循环引用问题,block内部的strongSelf仅仅是个局部变量,存在栈中,会在block执行结束之后回收,不会再造成循环引用,并且会使页面返回上一级时,不执行dealloc方法,直到block执行完,控制器执行dealloc方法,内存释放!
注:每次写很复杂,可以定义宏全局
// weak obj
/#define WEAK_OBJ(type) __weak typeof(type) weak##type = type;
// strong obj
/#define STRONG_OBJ(type) __strong typeof(type) str##type = weak##type;
2. Delegate
如果你知道Delegate为什么用weak修饰不用strong,也就明白了它为什么能造成循环引用,也能更好的避免发生此问题。
@interface TestViewController : UIViewController
@property (nonatomic, strong) id delegate;
@end
@implementation Page1ViewController
- (void)viewDidLoad {
[super viewDidLoad];
TestViewController *testVC = [[TestViewController alloc]init];
testVC.delegate = self;
}
页面Page1强引用了Test页面,Test的delegate属性指向Page1,因为delegate是用strong修饰的,所以Test也强引用了Page1,造成循环引用,要想打破循环引用,要像上面block一样,一方变为弱引用,所以修饰delegate要用weak不能用strong。
3. NSTimer
因为NSTimer 的 target 对传入的参数都是强引用,所以当类具有NSTimer类型的成员变量,并且需要反复执行计时任务时容易造成循环引用。
@implementation Page1ViewController{
NSTimer *_timer;
}
- (void)viewDidLoad {
[super viewDidLoad];
_timer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:@selector(addCount) userInfo:nil
repeats:YES];
}
因为控制器强引用了_timer, _timer 的 target 对传入的参数self也是强引用,相互持有,形成闭环。
解决方法(手动释放):
[_timer invalidate];
_timer = nil;
注意:有人把销毁_timer的方法放在dealloc里,感觉就是自我安慰,循环引用造成不调用dealloc方法,放在viewDidDisappear中又限制太死,最好的方法为(NSTimer的类别):
@interface NSTimer (EXBlock)
+ (NSTimer *)ex_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
@implementation NSTimer (EXBlock)
+ (NSTimer *)ex_scheduledTimeWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ex_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)ex_blockInvoke:(NSTimer *)timer{
void(^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
使用这个类别的方式如下:
- (void)viewDidLoad {
[super viewDidLoad];
__weak Page1ViewController * weakSelf = self;
_timer = [NSTimer ex_scheduledTimeWithTimeInterval:4.0f
block:^{
Page1ViewController * strongSelf = weakSelf;
[strongSelf addCount];
}
repeats:YES];
}
-(void)dealloc
{
[_timer invalidate];
}
原理:在NSTimer类别定义的类方法中,有一个类型为块的参数(定义的块位于栈上,为了防止块被释放,需要调用copy方法,将块移到堆上),__strong ViewController *strongSelf = weakSelf主要是为了防止执行块的代码时,类被提前释放了。
三.检测循环引用造成的内存泄漏
我们在编写项目时,并不是所有的循环引用编译器都会提示,所以在做完项目后,我们还需要检测项目中是否有内存泄漏的情况,以下是几种检测方法。
1.Analyze静态分析
打开product--->Analyze,项目会自动运行,工具对代码直接进行分析根据代码的上下文的语法结构, 让编译器分析内存情况, 检查是否有内存泄露。
Analyze主要分析以下四种问题:
1、逻辑错误:访问空指针或未初始化的变量等;
2、内存管理错误:如内存泄漏等;
3、声明错误:从未使用过的变量;
4、Api调用错误:未包含使用的库和框架。
缺点: 静态内存分析由于是编译器根据代码进行的判断, 做出的判断不一定会准确,不能把所有的内存泄露查出来,有的内存泄露是在运行时,用户操作时才产生的。
2.Instruments中的Leak动态分析
2.Instruments中的Leak动态分析
product->profile ->leaks 打开工具主窗口,手动运行检测:
使用Leak动态分析,我们可以快速定位到内存泄漏的代码,方便我们检测!
3.第三方工具MLeaksFinder
优点:可以自动在 App 运行过程检测到内存泄露的对象并立即提醒,无需打开额外的工具,无需添加任何业务逻辑代码,而且只在 debug 下开启,完全不影响你的 release 包。
原理:MLeaksFinder是从UIViewController入手的,UIViewController在POP或dismiss之后该控制器及其上的view,view的subviews都会被释放掉,MleaksFinder就是在控制器POP或dismiss之后去查看该控制器和其上的view是否都被释放掉。
使用:使用pods或者[下载]MLeaksFinder导入项目,运行,通过提示框和控制器台打印来提示哪里有内存泄漏的问题。
4.自定义检测工具
需求:检测UIViewController 是否发生内存泄漏。
思路:我们需要检测控制器对象在POP后是否还存活,存活则表示有内存泄漏。
原理:利用dispatch_after的延时处理事物的方式,当页面被POP后,延迟事件还能响应,则判断控制器未被释放,有内存泄漏。
做法:分类+runtime
(1).给NSObject添加一个分类:实现方法交换
+(void)swizzleSEL:(SEL)originSEL withSEL:(SEL)swizzlingSEL{
Class class = [self class];
Method originMethod = class_getInstanceMethod(class, originSEL);
Method currentMethod = class_getInstanceMethod(class, swizzlingSEL);
BOOL didAddMethod = class_addMethod(class, originSEL, method_getImplementation(currentMethod), method_getTypeEncoding(currentMethod));
//这种判断方式使代码更健壮 防止currentMethod 方法未实现
if(didAddMethod){
class_replaceMethod(class, swizzlingSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else
{
method_exchangeImplementations(originMethod, currentMethod);
}
}
(2).给UIViewController添加一个分类
在 + (void)load 通过swizzleSEL 实现 viewWillAppear和viewDidDisAppear 和新方法的交换,并在viewWillAppear方法绑定一个标志,NO则表示Push,YES则表示Pop,当标志为YES时,则实现延迟方法。
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(dt_viewWillAppear:)];
[self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(dt_viewDidDisAppear:)];
});
}
-(void)dt_viewWillAppear:(BOOL)animate{
[self dt_viewWillAppear:animate];
objc_setAssociatedObject(self, @"VCFLAG", @(NO), OBJC_ASSOCIATION_ASSIGN);
}
-(void)dt_viewDidDisAppear:(BOOL)animate{
[self dt_viewDidDisAppear:animate];
if([objc_getAssociatedObject(self, @"VCFLAG") boolValue]){
__weak typeof(self) weakself = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6*NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong typeof(weakself) strongself = weakself;
NSLog(@"发生内存泄漏的控制器------->%@",strongself);
});
}
}
(3).给UINavigationController添加一个分类
在 load方法中实现popViewControllerAnimated和新方法的交换,并在新方法中赋值标志为YES,让其触发延迟事件。
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(popViewControllerAnimated:) withSEL:@selector(dt_popViewControllerAnimated:)];
});
}
-(UIViewController*)dt_popViewControllerAnimated:(BOOL)animated{
UIViewController *popVC = [self dt_popViewControllerAnimated:animated];
objc_setAssociatedObject(popVC, @"VCFLAG", @(YES), OBJC_ASSOCIATION_ASSIGN);
return popVC;
}
```
(4).测试,在控制内写一个循环引用问题,如下图:
四.总结
反思自己在开发中,很多知识点总是会用,却不懂原理,没有系统的学习研究,几年的开发,仍然处在业务层,要多学习整理底层原理,才会对代码有更清晰的认识!
推荐阅读