iOS中常见的内存泄漏,及避免泄漏的最佳方案
引言
在iOS应用开发中,内存泄漏是一个常见而严重的问题。本文将探讨一些iOS应用中常见的内存泄漏原因,并提供一些最佳实践,帮助开发者避免这些问题,提高应用性能。
什么是内存泄漏
内存泄漏是指在程序运行时,由于错误的内存管理,分配的内存空间无法被正常释放,导致系统中的可用内存逐渐减少,最终可能导致应用程序性能下降甚至崩溃的问题。iOS中的内存管理机制是依赖引用计数进行自动管理,而引用计数的最大缺陷就在于它不能处理环状的引用关系。
常见的iOS内存泄漏场景
1.子对象持有它的父对象
@interface LMAlbum : NSObject
@property(nonatomic, copy)NSString * title;
@property(nonatomic, copy)NSArray * photos;
@end
@interface LMPhoto : NSObject
@property(nonatomic, copy)NSString * name;
@property(nonatomic, strong)HPAlbum * album;//LMPhoto通过强引用指向它所属的相册
@end
当我们创建一个相册album对象,相册中包含一个有许多照片LMPhoto对象的数组,
照片LMPhoto对象又包含一个所属相册的属性。
照片LMPhoto对象在album的photos中有强引用,引用计数为1。
album对象又在照片LMPhoto对象的album中有强引用,引用计数为1。所以当这些对象不再被使用的时候,它们的内存也不会被释放,因为它们的引用计数不会被降为0。
解决方案:
我们可以通过子对象用weak引用指向它的父对象的方式解决该问题。
@interface LMPhoto : NSObject
@property(nonatomic, copy)NSString * name;
@property(nonatomic, weak)LMAlbum * album;//LMPhoto通过弱引用指向它所属的相册
@end
2.代理
@protocol LMRequestManagerDelegate <NSObject>
- (void)finish;
@end
@interface LMRequestManager : NSObject
@property(nonatomic,strong)id<LMRequestManagerDelegate> delegate;
- (void)requstData;
@end
@interface ViewController ()<LMRequestManagerDelegate>
@property(nonatomic,strong)LMRequestManager * requestManager;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.requestManager = [[LMRequestManager alloc] init];
self.requestManager.delegate = self;
[self.requestManager requstData];
}
- (void)finish {
}
上述案例中ViewController通过属性强持有requestManager。
而self.requestManager.delegate = self;
此句代码使得LMRequestManager强持有了self,这就是产生循环引用的地方。
解决方案:
我们需要保持对回调代理的弱引用,或者不需要将LMRequestManager设置为属性。本质上这里和上一个例子是相同的。
@protocol LMRequestManagerDelegate <NSObject>
- (void)finish;
@end
@interface LMRequestManager : NSObject
@property(nonatomic,weak)id<LMRequestManagerDelegate> delegate;
- (void)requstData;
@end
3.block
- (void)method{
self.name = @"Joyme";
self.block = ^{
NSLog(@"%@",self.name);
};
self.block();
}
这也将产生循环引用,因为self持有了block,然后在block中捕获了self。
解决方案:
我们可以使用?__weak typeof(self) weakSelf = self;的方式进行解决。
- (void)method{
self.name = @“Joyme”;
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%@",weakSelf);
};
self.block();
}
4.计时器
@implementation LMNewsFeedViewController
- (void)startCountdown{
self.timer = [NSTimer scheduledTimerWithTimeInterval:120 target:self selector:@selector(updateFeed:) userInfo:nil repeats:YES];
}
- (void)dealloc{
[self.timer invalidate];
}
@end
上述代码中有非常明显的循环引用,对象持有了计时器,同时计时器也持有了对象。此时我们不能通过不设置为属性,并且我们也不可以使用weak来修饰timer。相反我们需要持有timer属性,以便可以在后续被销毁。
这种情况我们不能指望dealloc能够清理这些对象,因为建立了循环引用,dealloc方法永远都不会被调用,计时器也永远都不会执行invalidated。
要解决这个问题有两个方案:
- 主动调用invalidate
- 将代码分离到多个类中
第一个方案可以写在当视图控制器退出时
- (void)didMoveToParentViewController:(UIViewController *)parent{
if(parent == nil){
[self cleanup];
}
}
- (void)cleanup{
[self.timer invalidate];
}
或者通过拦截返回按钮的响应
- (id)init{
if(self = [super init]){
self.navigationItem.backBarButtonItem.target = self;
self.navigationItem.backBarButtonItem.action = @selector(backButtonPreDetected);
}
return self;
}
- (void)backButtonPressDetected:(id)sender{
[self cleanup];
[self.navigationController popViewControllerAnimated:TRUE];
}
- (void)cleanup{
[self.timer invalidate];
}
另一个方案更优雅一些,是将持有关系分散到多个类中。
@interface LMNewFeedUpdateTask
@property(nonatomic,weak)id target;//target属性是弱引用。target会在这里实例化任务并持有它。
@property(nonatomic,assign)SEL selector;
@property(nonatomic,strong)NSTimer * timer;
@end
@implementation LMNewFeedUpdateTask
- (void)initWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector{
if(self = [super init]){
self.target = target;
self.selector = selector;
self.timer = [NSTimer scheduledTimerWithInterval:interval target:self selector:@selector(fetchAndUpdate:) userInfo:nil repeats:YES];
}
return self;
}
- (void)fetchAndUpdate:(NSTimer*)timer{//fetchAndUpdate:方法会周期性地执行
__weak typeof(self)weakSelf = self;
dispatch_async(dispatch_get_main_queue(),^{
__strong typeof(self) sself = weakSelf;
if(!sself){
return;
}
if(sself.target == nil){
return;
}
id target = sself.target;
SEL selector = sself.selector;
if([target respondsToSelector:selector]){
[target performSelector:selector withObject:@""];
}
});
}
- (void)shutdown{//shutdown方法对计时器调用invalidate。运行循环会终止对计时器的调用,于是计时器成为任务对象持有的唯一引用。
[self.timer invalidate];
}
@end
@implement LMNewsFeedViewController
- (void)viewDidLoad{//对任务对象进行初始化,其内部会触发计时器。
self.updateTask = [LMNewsFeedUpdateTask initWithTimeInterval:120 target:self selector:@selector(updateUsingFeed:)];
}
- (void)updateUsingFeed:(id)obj{
//更新UI
}
- (void)dealloc{//负责调用任务对象的shutdown方法,其内部会销毁计时器。注意,dealloc在此处是明确可用的,因为该对象没有被其他的地方所引用。
[self.updateTask shutdown];
}
5.延迟执行
#import "LMDataListViewController.h"
@implementation LMDataListViewController.h
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelector:@selector(run) withObject:nil afterDelay:30];
}
- (void)run{
}
上述案例当LMDataListViewController退出的时候会出现延迟释放的情况,
当执行[self performSelector:@selector(run) withObject:nil afterDelay:30];代码的时候会对self进行一个捕获,当前self的引用计数进行+1直到延迟方法执行后才会进行-1操作。
所以self在延迟调用的方法执行之前会始终得不到释放。
解决方案:
其一还是显示的调用下面方法。
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(run) object:nil];
其二参考定时器的解决方案,我们可以设计出相似类来解决这个问题。
#import "LMAfterTask.h"
@interface LMAfterTask ()
@property(nonatomic,weak)id target;//target属性是弱引用。target会在这里实例化任务并持有它。
@property(nonatomic,assign)SEL selector;
@end
@implementation LMAfterTask
- (id)initWithAfterInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector{
if (self = [super init]) {
self.target = target;
self.selector = selector;
[self performSelector:@selector(performMethod) withObject:nil afterDelay:interval];
}
return self;
}
- (void)performMethod{
if(self.target == nil){
return;
}
if([self.target respondsToSelector:self.selector]){
[self.target performSelector:self.selector withObject:nil];
}
}
- (void)cancel{
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(performMethod) object:nil];
}
#import "LMNewFeedUpdateTask.h"
#import "LMAfterTask.h"
@interface LMDataListViewController ()
@property(nonatomic,strong)LMAfterTask * afterTask;
@end
@implementation LMDataListViewController.h
- (void)viewDidLoad {
[super viewDidLoad];
self.afterTask = [[LMAfterTask alloc] initWithAfterInterval:15 target:self selector:@selector(run)];
}
- (void)run{
NSLog(@"跑起来");
}
- (void)dealloc{
[self.afterTask cancel];
}
@end
使用GCD的延迟执行也会有同样的问题。
#import "LMDataListViewController.h"
@implementation LMDataListViewController.h
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self run];
});
}
- (void)run{
}
但我们可以通过使用__weak来进行解决,此时虽然self会得到正常的释放,但是延迟的的代码块还是会执行的。操作不当还是会出现其它不可预知的情况,所以我们还需要显示的取消该任务块。
#import "LMDataListViewController.h"
@interface LMDataListViewController.h ()
{
dispatch_block_t _taskBlock;
}
@end
@implementation LMDataListViewController.h
- (void)viewDidLoad {
[super viewDidLoad];
__weak LMDataListViewController.h * weakSelf = self;
_taskBlock = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
[weakSelf run];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(20 * NSEC_PER_SEC)), dispatch_get_main_queue(), _taskBlock);
}
- (void)run{
NSLog(@"跑起来");
}
- (void)dealloc{
if (_taskBlock) {
dispatch_block_cancel(_taskBlock);
}
}
@end
最佳实践
我们可以遵循以下最佳实践避免内存泄漏
- 对象不该持有它的父对象,应该用weak引用指向它的父对象。
- 连接对象不应该持有它们的目标对象,目标对象角色是持有者。连接对象包括使用代理的对象,观察者。
- 定时器需要显式的进行销毁。
- 延迟执行代码需要显式的进行取消
结尾
内存泄漏对于我们开发者而言,可能是一生之敌。上面只是简单的列举一些开发过程中比较常见的场景,希望能够帮助到大家避免这些问题,提高应用性能。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!