iOS性能优化(四) —— 内存优化之泄露及其检测(一)

版本记录

版本号 时间
V1.0 2018.12.22 星期六

前言

很多时候我们做APP,不是完成相关的功能就行,很多时候需要我们不断的进行优化,特别对于像淘宝和微信等巨型app来说,如果没有优化,那会是什么样的结果,一定会有很多人“脱坑”,又何谈发展啊。感兴趣的可以看一下上一篇。
1. iOS性能优化(一)
2. iOS性能优化(二)
3. iOS性能优化(三)

内存泄露

首先看下本文写作环境

iOS 12.1、模拟器iPhone XS Max、xcode 10.1

相信大家都知道内存泄露的危害,会使很多对象无法释放,可能造成各种异常问题,甚至崩溃。所以,不可不重视,接下来这几篇我们就看一下内存优化中的内存泄露、以及如何避免内存泄露。

1. 内存泄露的引起

其实不管是什么外在的表现导致了内存泄露,其实归根结底都是因为对象的引用计数不能为0,导致了系统不能对其进行收回,从而导致了内存泄露,很常见的情况就是循环引用,比如block和NSTimer的不合理使用等都会造成内存的泄露,所以在使用block和NSTimer的时候要特别的注意,不要引入循环引用,造成内存泄露。


调试环境的搭建

为了演示问题,首先我们在sb中拖进去点东西。

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第1张图片

下面看一下在这几个VC中的代码

1. AVC
#import "AVC.h"

@interface AVC ()

@end

@implementation AVC

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.title = NSStringFromClass([self class]);
}

- (void)dealloc
{
    NSLog(@"%@被释放了", self.title); // iOS性能优化(三)
}

@end
2. BVC
#import "BVC.h"

@interface BVC ()

@end

@implementation BVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = NSStringFromClass([self class]);
}

- (void)dealloc
{
    NSLog(@"%@被释放了", self.title);
}

@end
3. CVC
#import "CVC.h"

@interface CVC ()

@end

@implementation CVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = NSStringFromClass([self class]);
}

- (void)dealloc
{
    NSLog(@"%@被释放了", self.title);
}

@end

下面我们就Run一下,然后看控制台输出,看是否会调用dealloc析构函数。

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第2张图片

从上面我们可以看到,A push到B,在push到C,Cpop回B的时候C的析构函数被调用了,C被释放了,Bpop回A,这个时候B的析构函数被调用了,B也被释放了,回到了A。这个都是正常了。

目前为止,我们的演示工程就已经搭建完毕了,后面会在这个工程上进行改动给大家演示内存泄露和相关检测工具的使用。


Block引起的内存泄露

为了演示,首先我们要对B进行改动,如下图所示:

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第3张图片

其实写到这里大家可以看到那个黄色的提示循环引用的警告

Capturing 'self' strongly in this block is likely to lead to a retain cycle

这里我们暂时不管,还是直接run

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第4张图片

大家可以看到,A push到B,block执行了打出来NSLog,但是pop回到A发现B的析构函数中的XXX被释放了并没有调用,也就是说B没有被释放,造成了内存的泄露。

具体怎么修改这都是很常见的问题了,但是为了照顾初学者,还是粘贴出来了。

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第5张图片

也就是说将self进行弱化,下面我们再次运行看控制台输出。

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第6张图片

大家可以看见,这个时候我们从B pop回到A,就发现B的析构函数dealloc被调用了,输出BVC被释放了。

1. weakSelf使用注意

在使用weakSelf弱化self的时候,我们不是所有的block就直接用weakSelf而不区分场景,下面我们看一种场景:那就是在block里面模拟网络延时,接着我们修改下代码,如下所示:

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第7张图片

在这里面,我们在block内延时5s模拟网络延时,下面我们就run,然后在5s内popo回去看会发生什么。

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第8张图片

这里大家可以看见,5s内如果pop回去那么weakSelf就是nil,也不能正确打印出字符串了,这个都是正常的,这个类都不要了,也没必要要什么属性了,但是如果我就是想要pop还可以打印出这个属性呢?

这个时候我们就需要在block内部给self进行强化strong一下。

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第9张图片

这个就输出,可以看见尽管pop回去了,但是还是等5s打印出正确的属性值才会调用析构函数释放B对象。


NSTimer引起的内存泄露

下面我们还是看一段测试代码

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第10张图片

这里就是实例化一个timer,下面我们run一下。

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第11张图片

可以看见,Bpop回去,定时器还是一直在调用,而且其析构函数也没被调用。

下面我们就看一下解决NSTimer造成循环引用的方法。

1. 在didMoveToParentViewController:方法中处理

相信很多人都按照下面这个方式写过代码

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第12张图片

其实这不一定正确,这个做正确的前提是dealloc会被调用,可是这里析构函数是不会被调用的,也就是说这个断点或者这个方法是不会被调用的,这个补救措施可能只是自欺欺人的~~

那么是不是还有人想到了在下面这个方法停止定时器

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    
    [self.timer invalidate];
    self.timer = nil;
}

这么做可以吗?这个看情况,如果B中没有push到另外一个页面的就可以,如果有的话,比如B可以push到C,那么C的view显示的时候,B的定时器就停止了,很明显这个不是我们想要的逻辑,需要在B的view再显示的时候再度启用定时器,这多了很多的逻辑,容易出错,在项目逻辑复杂的时候更是不好维护,也是bug容易产生的地方。

我们可以用下面这个方法

//parent添加到父视图有值,从父视图移除就是nil
- (void)didMoveToParentViewController:(UIViewController *)parent
{
    if (!parent) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

添加完以后我们run起来

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第13张图片

这里应用了parent这个参数,如果是添加到父视图,parent有值,从父视图移除就是nil,所以从Bpop回到A定时器就停掉了,并且调用了析构函数,而B push到C,或者从C pop回到B都不会影响B中的定时器逻辑。

2. NSProxy

这里我们利用NSProxy的消息转发。

首先自定义一个类集成自JJProxy,然后写一些转发逻辑如下

1. JJProxy.h
#import 

NS_ASSUME_NONNULL_BEGIN

//负责消息转发到真正的代理类
@interface JJProxy : NSProxy

@property (nonatomic, weak) id target;

@end

NS_ASSUME_NONNULL_END
2. JJProxy.m
#import "JJProxy.h"

@implementation JJProxy

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

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

@end

然后我们需要在BVC中做一些逻辑

#import "BVC.h"
#import "JJProxy.h"

@interface BVC ()

@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) JJProxy *proxy;

@end

@implementation BVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = NSStringFromClass([self class]);
    
    _proxy = [JJProxy alloc];
    _proxy.target = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_proxy selector:@selector(timerFire) userInfo:nil repeats:YES];
}

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%@被释放了", self.title);
}

- (void)timerFire
{
   NSLog(@"timerfire");
}

@end

接着我们run一下

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第14张图片

可以看见,解决了内存泄露问题,析构函数已经被调用了。

3. runtime

下面我们就看一下利用runtime解决内存泄露的问题。

引入runtime的头文件并改动BVC中的代码,如下:

#import "BVC.h"
#import 

@interface BVC ()

@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) id timerTarget; //中间者

@end

@implementation BVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.title = NSStringFromClass([self class]);
    
    _timerTarget = [NSObject new];
    
    //runtime动态添加方法
    Method method = class_getInstanceMethod([self class], @selector(timerFire));
    class_addMethod([_timerTarget class], @selector(timerFire), method_getImplementation(method), "v@:");
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_timerTarget selector:@selector(timerFire) userInfo:nil repeats:YES];
}

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%@被释放了", self.title);
}

- (void)timerFire
{
   NSLog(@"timerfire");
}

@end

下面我们同样run一下

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第15张图片

可以看见,我们一样解决了循环引用,B的析构函数被调用了。

这里三种方法,其实不管你使用什么方式去实现,最终的原理只有一个,打破循环引用联调,让给对象的引用计数为0就会调用该对象的析构方法。

后记

本篇主要讲述了内存优化之泄露及其解决方法,感兴趣的给个赞或者关注~~~

iOS性能优化(四) —— 内存优化之泄露及其检测(一)_第16张图片

你可能感兴趣的:(iOS性能优化(四) —— 内存优化之泄露及其检测(一))