天天看点

iOS内存管理01-定时器One More Thing

这一阶段我们主要来讲讲iOS内存管理方面的知识,面试的时候可能大家多多少少都会被问及这方面的问题,那我们就从常见的面试题开讲

  • 使用

    CADisplayLink

    NSTimer

    有什么注意点?
  • 介绍下内存的几大区域
  • 讲一下你对iOS内存管理的理解
  • ARC

    都帮我们做了什么?
  • weak指针

    的实现原理
  • autorelease

    对象在什么时机会被调用

    release

  • 方法里有局部对象,出了方法后会

    立即释放

    吗?

我们一个一个来,今天我们就先来讲讲第一条使用

CADisplayLink

NSTimer

有什么注意点?

主要就是

CADisplayLink

NSTimer

会对

target

产生

强引用

,如果

target

又对它们产生

强引用

,那么就会引发

循环引用

循环引用大家都知道吧,这样就会导致内存泄漏,我们写代码来看看

首先我们看下

CADisplayLink

,我们新建一个

NavigationController

,然后点击一个按钮

push

到我们

ViewController

NavigationController

就一个按钮比较简单我就不写了,下面看下我们的

ViewController

里的代码

@interface ViewController ()
@property (strong, nonatomic) CADisplayLink *link;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 保证调用频率和屏幕的刷帧频率一致,60FPS
    self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
}

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

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

可能有些同学没用过

CADisplayLink

我们先简单了解下,

CADisplayLink

其实也是一个定时器,只不过这个定时器

不用你来设置时间

,它是要保证

调用频率

屏幕的刷帧频率一致

,通常来说大概是

60FPS

,当然如果你主线程要是做了很多耗时操作的话也可能就不到60了,也就是说我们的

linkTest方法

大概一秒钟会调用

60次

的样子, 那我们运行程序看下控制台的输出也证实了这一点

那接下来我们来看问题

我们看到我们

displayLinkWithTarget

这个类方法会传入一个

self

,这样,

CADisplayLink

对象就会强引用

self

,而

self

强引用了

@property (strong, nonatomic) CADisplayLink *link

,所以就产生了

循环引用

,导致两者

都不会被释放

,可能我们很多同学也都会在

dealloc

方法里调用

[self.link invalidate]

其实是没有用的,我们点下返回键就可以看到,

dealloc

根本不用被调用,那如何解决呢,我们先来看看

Timer

是不是也有这个问题,给Controller添加一个属性

@property (strong, nonatomic) NSTimer *timer;
           

然后初始化并每隔一秒调用

timerTest

方法

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

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

           

我们运行程序看到,返回的时候

timer

并没有停止,

dealloc

也没有调用说明

timer

也存在这个问题,那接下来我们先来解决

timer

的问题,然后再解决

CADisplayLink

的问题

可能很多同学会说我来个弱指针

__weak typeof(self) weakSelf = self

,把

weakSelf

传入

target

, 不就行了吗,那大家想想管用吗?我们运行下程序发下然并卵返回

timer

依旧跑着,

dealloc

也没调用,那为什么之前的都好使这次不好使了呢,之前是因为我们在

block

里的

循环引用

可以用

weakSelf

来解决,我们现在是没有

block

的,而且,

weakSelf

self

其实都是同一个内存地址,我们只是把它当做

参数

来传给

target

这个形参的,所以没用,依旧还是强引用,我们可以这样认为

@interface NSTimer()
@property(strong, nonatomic) id target
@end
           

NSTimer

内部强引用了

target

跟你外部传入强引用还是弱引用没有半毛钱关系,所以这是不能解决问题的,那怎么办?其实要是

NSTimer

的话有好几种解决方案,我们先来看看第一种方案

1、更换

timer

的初始化方法,用带

block

的方法,这个时候就可以用

weakSelf

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerTest];
    }];
           

我们再运行下程序,我们看到

dealloc

调用了

timer

也停止了,所以问题也解决了

2、那我们再换回第一种方法,是否可以解决呢,答案是肯定的,我们来引入一个OtherObject当target,我们来画个图来理解下

iOS内存管理01-定时器One More Thing

这样,

timer

强引用

OtherObject

OtherObject

弱引用

Controller

就这样就行了,那具体如何来做呢

我们新建一个

XXProxy

@interface XXProxy : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation XXProxy

+ (instancetype)proxyWithTarget:(id)target
{
    XXProxy *proxy = [[XXProxy alloc] init];
    proxy.target = target;
    return proxy;
}
           

然后修改我们的

Controller

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[XXProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
           

这样就解决循环引用了,但是我们发现另一个问题,现在

target

里并没有

timerTest

这个方法,运行肯定会

crash

,当然我们也可以直接在

XXProxy

类里写一个

timerTest

- (void)timerTest
{
    [self.target timerTest];
}
           

但是大家想没想过,

XXProxy

这个可能不止被一个

timer

用,要是有很多

timer

总不至于把所有的方法都写上吧,大家想想有没有什么更好的方法呢?其实,这个时候我们就可以用

消息转发机制

,之后可以给大家详细说说,其实消息转发是有

三个阶段

forwardInvocation

methodSignatureForSelector

forwardingTargetForSelector

,我们现在就直接用第三阶段了直接

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
           

这样不管外界调用我什么方法我都直接转发给控制器对应的方法,这样是不是就一劳永逸了

现在我们运行下程序,我们可以看到dealloc是有调用的,所以问题解决

那我们现在再看下CADisplayLink是不是可以用相同的办法解决,我们修改下代码

// 保证调用频率和屏幕的刷帧频率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:[XXProxy proxyWithTarget:self] selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
           

我们运行下程序,看下日志,同样解决问题了

One More Thing

喜欢的朋友可以扫描关注我的公众号(您的支持是我写作的最大动力)

iOS内存管理01-定时器One More Thing