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 

- (void)finish;

@end

@interface LMRequestManager : NSObject

@property(nonatomic,strong)id delegate;

- (void)requstData;

@end
@interface ViewController ()

@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 


- (void)finish;


@end


@interface LMRequestManager : NSObject


@property(nonatomic,weak)id 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引用指向它的父对象。
  • 连接对象不应该持有它们的目标对象,目标对象角色是持有者。连接对象包括使用代理的对象,观察者。
  • 定时器需要显式的进行销毁。
  • 延迟执行代码需要显式的进行取消

结尾

内存泄漏对于我们开发者而言,可能是一生之敌。上面只是简单的列举一些开发过程中比较常见的场景,希望能够帮助到大家避免这些问题,提高应用性能。

你可能感兴趣的:(ios)