iOS中常见的内存泄漏,及避免泄漏的最佳方案

引言

在iOS应用开发中,内存泄漏是一个常见而严重的问题。本文将探讨一些iOS应用中常见的内存泄漏原因,并提供一些最佳实践,帮助开发者避免这些问题,提高应用性能。

什么是内存泄漏

内存泄漏是指在程序运行时,由于错误的内存管理,分配的内存空间无法被正常释放,导致系统中的可用内存逐渐减少,最终可能导致应用程序性能下降甚至崩溃的问题。iOS中的内存管理机制是依赖引用计数进行自动管理,而引用计数的最大缺陷就在于它不能处理环状的引用关系。

常见的iOS内存泄漏场景

1.子对象持有它的父对象

objectivec 复制代码
@interface LMAlbum : NSObject

@property(nonatomic, copy)NSString * title;
@property(nonatomic, copy)NSArray * photos;

@end
objectivec 复制代码
@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引用指向它的父对象的方式解决该问题。

objectivec 复制代码
@interface LMPhoto : NSObject


@property(nonatomic, copy)NSString * name;
@property(nonatomic, weak)LMAlbum * album;//LMPhoto通过弱引用指向它所属的相册


@end

2.代理

objectivec 复制代码
@protocol LMRequestManagerDelegate <NSObject>

- (void)finish;

@end

@interface LMRequestManager : NSObject

@property(nonatomic,strong)id<LMRequestManagerDelegate> delegate;

- (void)requstData;

@end
objectivec 复制代码
@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设置为属性。本质上这里和上一个例子是相同的。

objectivec 复制代码
@protocol LMRequestManagerDelegate <NSObject>


- (void)finish;


@end


@interface LMRequestManager : NSObject


@property(nonatomic,weak)id<LMRequestManagerDelegate> delegate;


- (void)requstData;


@end

3.block

objectivec 复制代码
- (void)method{
    self.name = @"Joyme";
    self.block = ^{
        NSLog(@"%@",self.name);
    };
    self.block();
}

这也将产生循环引用,因为self持有了block,然后在block中捕获了self。

解决方案:

我们可以使用 __weak typeof(self) weakSelf = self;的方式进行解决。

objectivec 复制代码
- (void)method{
    self.name = @"Joyme";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        NSLog(@"%@",weakSelf);
    };
    self.block();
}

4.计时器

objectivec 复制代码
@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
  • 将代码分离到多个类中

第一个方案可以写在当视图控制器退出时

objectivec 复制代码
- (void)didMoveToParentViewController:(UIViewController *)parent{
    if(parent == nil){
        [self cleanup];
    }
}

- (void)cleanup{
    [self.timer invalidate];
}

或者通过拦截返回按钮的响应

objectivec 复制代码
- (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];
}

另一个方案更优雅一些,是将持有关系分散到多个类中。

objectivec 复制代码
@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
objectivec 复制代码
@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.延迟执行

objectivec 复制代码
#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在延迟调用的方法执行之前会始终得不到释放。

解决方案:

其一还是显示的调用下面方法。

objectivec 复制代码
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(run) object:nil];

其二参考定时器的解决方案,我们可以设计出相似类来解决这个问题。

objectivec 复制代码
#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];
}
objectivec 复制代码
#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的延迟执行也会有同样的问题。

objectivec 复制代码
#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会得到正常的释放,但是延迟的的代码块还是会执行的。操作不当还是会出现其它不可预知的情况,所以我们还需要显示的取消该任务块。

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

结尾

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

相关推荐
若水无华1 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"2 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂3 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T3 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20253 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz3 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频