iOS 性能优化(内存泄露检测)

2,119 阅读7分钟

前言

之前的一系列文章介绍了内存布局,内存管理策略引用计数,弱引用自动释放池,对内存管理的源码也有了一个大概的了解,这篇文章就来说说检测内存泄露的手段

内存泄露

内存泄露有很多种,下面就介绍一下Block的循环引用NSTimer强引用

Block的循环引用

先看下正常情况下的持有关系,如下

正常情况下A持有B,这时B的引用计数+1

A如果调用了析构函数dealloc,给B发送release信息。这时候A不再持有B,则B的引用计数-1,如果B的引用计数为0,则B就调用自己的析构函数去释放自己

再看一下循环引用

下面用代码来实现一下

#import "NHViewController.h"

typedef void(^NHBlock)(void);

@interface NHViewController ()
@property (nonatomic, copy) NHBlock block;

@property (nonatomic, copy) NSString *name;
@end

@implementation NHViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"Noah";
    self.block = ^{
        NSLog(@"%@",self.name);
    };
    
    self.block();
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}

@end

可以看出,这样写的话Xcode会报警告,Capturing 'self' strongly in this block is likely to lead to a retain cycle,告诉我们会有循环引用,解决这个问题很简单,用__weak typeof(self) weakSelf = self;去代替self即可,代码演示:

__weak typeof(self) weakSelf = self;
self.block = ^{
    NSLog(@"%@",self.name);
};
self.block();

但是这样写也会有个提前释放的问题。比如,App启动的时候进入的是ViewControl,现在Push到NHViewController,NHViewController的block做了延时任务,2秒后才打印name的值,如果Push到NHViewController后马上Pop,会导致打印的name为null,代码演示:

#import "NHViewController.h"

typedef void(^NHBlock)(void);

@interface NHViewController ()
@property (nonatomic, copy) NHBlock block;
@property (nonatomic, copy) NSString *name;
@end

@implementation NHViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"Noah";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        // 2s后才执行打印
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.name); // self - nil name - nil
        });
    };
    
    self.block();
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end

因为2s后NHViewController已经被释放了,所以name的值也被释放了,所以打印出来的name就是null。解决的办法就是在block还没执行完的时候强引用它,代码演示:

__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(self) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@",strongSelf.name); // self - nil name - nil
    });
};
self.block();

还有另外的一种办法,block传值的办法,这样写的性能比较好。代码演示如下:

#import "NHViewController.h"

typedef void(^NHBlockVC)(NHViewController *);

@interface NHViewController ()
@property (nonatomic, copy) NHBlockVC blockVc;
@property (nonatomic, copy) NSString *name;
@end

@implementation NHViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"Noah";
    self.blockVc = ^(NHViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name); 
        });
    };
    self.blockVc(self);
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end

NSTimer强引用

API的使用:

// 需要自己手动添加到runloop中执行,并且需要启动子线程的runloop。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
// 系统默认帮你添加到runloop的defaultmood中了。
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

循环引用的原因是:NSTimer的target被强引用了,而通常target就是所在的控制器,它又强引用的timer,造成了循环引用。并且NSRunLoop还强引用着控制器,用下图表示

代码实现:

#import "TimerViewController.h"

static int num = 0;
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer       *timer;
@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}
- (void)dealloc{

    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}
@end

可以观察到析构函数一直没被调用。目前来说,只要解决了target的强引用问题就可以了。

解决方案:

  • 借助中间代理间接持有Timer
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NHTimerWapper : NSObject

@property (weak, nonatomic) id target;

+ (instancetype) timerWapperWithTarget:(id)target;

@end

NS_ASSUME_NONNULL_END
#import "NHTimerWapper.h"

@implementation NHTimerWapper

+ (instancetype) timerWapperWithTarget:(id)target{
    NHTimerWapper *wapper = [[NHTimerWapper alloc]init];
    wapper.target = target;
    return wapper;
}

// 消息重定向,指定某个对象去处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

@end
#import "NHTimerWapper.h"

static int num = 0;

@interface NHTimerViewController ()
@end

@implementation NHTimerViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[NHTimerWapper timerWapperWithTarget:self] selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}
@end

打印:

2020-02-20 21:18:48.332365+0800 002-强引用[3761:1484710] hello word - 1
2020-02-20 21:18:49.332319+0800 002-强引用[3761:1484710] hello word - 2
2020-02-20 21:18:50.332302+0800 002-强引用[3761:1484710] hello word - 3
2020-02-20 21:18:51.296455+0800 002-强引用[3761:1484710] -[NHTimerViewController dealloc]
  • 继承NSProxy类对消息处理

NSProxy这个类比较陌生,它和NSObject是同个级别的类,而且还是个抽象类,所以不能直接使用,要继承于它的子类才可以直接使用

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NHProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

NS_ASSUME_NONNULL_END
#import "NHProxy.h"

@interface NHProxy()
@property (nonatomic, weak) id object;
@end

@implementation NHProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    NHProxy *proxy = [NHProxy alloc];
    proxy.object = object;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.object methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.object];
}

@end
#import "NHProxy.h"
static int num = 0;

@interface NHTimerViewController ()
@end

@implementation NHTimerViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[NHProxy proxyWithTransformObject:self] selector:@selector(fireHome) userInfo:nil repeats:YES];
    
}
- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}
@end

打印:

2020-02-20 21:34:37.878850+0800 002-强引用[3906:1491522] hello word - 1
2020-02-20 21:34:38.878544+0800 002-强引用[3906:1491522] hello word - 2
2020-02-20 21:34:39.879129+0800 002-强引用[3906:1491522] hello word - 3
2020-02-20 21:34:40.878787+0800 002-强引用[3906:1491522] hello word - 4
2020-02-20 21:34:41.721979+0800 002-强引用[3906:1491522] -[NHTimerViewController dealloc]

NSProxy类是比较强大的一个类,它可以成为任何中间者的角色

检测工具

Instruments

Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还可以广泛收集不同类型的数据。

总结一下instrument能做的事情:

  • Instruments是用于动态调追踪和分析OS X和iOS的代码的性能分析和测试工具;
  • Instruments支持多线程的调试;
  • 可以用Instruments去录制和回放,图形用户界面的操作过程
  • 可将录制的图形界面操作和Instruments保存为模板,供以后访问使用。
  • 追踪代码中的(甚至是那些难以复制的)问题;
  • 分析程序的性能;
  • 实现程序的自动化测试;
  • 部分实现程序的压力测试;
  • 执行系统级别的通用问题追踪调试;
  • 使你对程序的内部运行过程更加了解。

因为网上已经有很多关于使用Instruments的文章,所以本文就不在过多的叙述,下面贴几篇写得比较好的文章
Instruments使用总结
Instruments性能优化-Core Animation

MLeaksFinder

MLeaksFinder 是 iOS 平台的自动内存泄漏检测工具,引进 MLeaksFinder 后,就可以在日常的开发,调试业务逻辑的过程中自动地发现并警告内存泄漏。开发者无需打开 instrument 等工具,也无需为了找内存泄漏而去跑额外的流程。并且,由于开发者是在修改代码之后一跑业务逻辑就能发现内存泄漏的,这使得开发者能很快地意识到是哪里的代码写得问题。这种及时的内存泄漏的发现在很大的程度上降低了修复内存泄漏的成本。

原理

为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}

这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。

这里也不在过多的阐述这个第三方工具,写得比较好的文章:
iOS 内存泄露检测工具MLeaksFinder
内存泄漏的高效检测方法 - MLeaksFinder


参考文章

iOS: Block的循环引用
RunTime的消息机制 & NSTimer的循环引用