[iOS开发]NSTimer与循环引用的理解

NSTimer在runloop中我们有接触过,当时只是简单了解,NSTimer其实是很重要的一部分知识,我们需要好好了解一下这部分和循环引用部分的理解

问题引入

我们已几个问题来展开

  • 什么是循环引用?
  • NSTimer里面为什么要强引用target?
  • 有什么更好的方式来解决这种循环引用吗?

循环引用

简单理解的循环引用

简单理解就是

对象A持有对象B,同时对象B也持有对象A

可能还是有点抽象
可以举个例子

@interface FirstPerson : NSObject 
 
@property (nonatomic, strong) SecondPerson *test; 
 
@end 
复制代码
@interface SecondPerson : NSObject 
 
@property (nonatomic, strong) FirstPerson *test; 
 
@end 
复制代码

如果不是循环引用,就是单向持有,假设是A持有B,也就是B是A的一个属性

若对A发送release消息,发现持有对象B,则向对象B发送release消息,B对象执行dealloc方法,引用计数为0,释放内存,同时A对象引用计数也为0,内存得到正确释放

如果是循环引用

若对A发送release消息,发现持有B对象,则会向B对象发送release消息,等待b释放内存。B收到release消息后,发现持有A,于是也向A发送release消息,等待A释放内存。此时就会发生A等待B释放内存,B又等待A释放内存,造成了死锁,发生内存泄漏

如果我们使用weak就可以破解这个循环引用

若对A发送release消息,发现持有B对象,则会向B对象发送release消息,等待B释放内存。B收到release消息后,虽然持有A(weak),但不会等待A释放内存,此时引用计数为0,执行dealloc,释放内存,同时A的引用计数变成了0,执行dealloc,释放内存

Block中的循环引用强弱共舞

如果我们在某个页面的属性中声明了一个block,那么就相当于我们这个页面的self持有了这个block
如果我们在block再持有self,这个时候就会造成循环引用

怎么解决呢?

持有个“假self”就行了(弱引用一下)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
      __weak typeof(self) weakSelf = self;
      self.blk = ^{
          //5秒后执行
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              NSLog(@"%@", weakSelf);
          });
      };
      self.blk();
      [self dismissViewControllerAnimated:YES completion:nil];
}
复制代码

self对象持有block,block里面又持有self,为了破除循环引用,使用weak也没啥问题

但是如果是在新视图中呢?我们在执行完block后直接就把该视图dismiss掉了,这个时候会出现什么问题?

什么都不加的情况,我们dismiss掉了之后,并不会走dealloc,其内部还在相互循环等待,造成页面dismiss了,但是dealloc方法并没有走,导致了内存泄漏。

我们仅仅加上weak

五秒钟之后打印self,就是这个新视图,视图dismiss了,不存在了,再打印self,肯定就是null了

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
      __weak typeof(self) weakSelf = self;
      self.blk = ^{
          __strong typeof(self) strongSelf = weakSelf;
          //5秒后执行
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              NSLog(@"%@", weakSelf);
          });
      };
      self.blk();
      [self dismissViewControllerAnimated:YES completion:nil];
}
复制代码

这样就行了

外部的weak是为了打破循环引用,内部的strongSelf是为了让新页面的对象retain+1,当外部释放对象时,此时其引用计数不为0,所以不能释放。由于strongSelf是在栈上 分配的内存,一旦执行完后,对象的引用计数变成了0,内存得到了释放。

NSTimer

之前学习的循环引用部分我们就结束了,下面步入正题NSTimer

创建NSTimer

创建NSTimer的常用方法是
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

不常用方法是
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

这几种方法除了创建方式(参数)不同,方法类型不同(类方法,对象方法),还有什么不同?

我们常用的scheduledTimerWithTimeInterval相比与其他两种方法,其不仅仅是创建了NSTimer对象,还把该对象加入到了当前的runloop中

NSTimer只有被加入到runloop中,才会生效,NSTimer才会真正执行。

这也就是说,如果我们想使用timerWithTimeInterval或initWithFireDate的话,需要使用NSRunloop的以下方法将NSTimer加入到runloop中
- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode

销毁NSTimer

之前没有考虑过什么方法,之前理解的是
我直接置nil不就完事了,系统会帮我们回收

但其实并不是这样的

invalidate与fire

看一下官方文档里这两个的意思

  • invalidate

Stops the receiver from ever firing again and requests its removal from its run loop

This method is the only way to remove a timer from an NSRunLoop object

阻止接收器再次触发,并请求将其从runloop中移除

此方法是从NSRunLoop对象中删除计时器的唯一方法

  • fire

Causes the receiver’s message to be sent to its target

If the timer is non-repeating, it is automatically invalidated after firing

使接收方的消息发送到其目标

如果计时器不重复,则会在触发后自动失效

总之,如果想要销毁NSTimer,那么一定要使用invalidate方法

invalidate与 = nil

就像销毁其他强引用对象一样,我们能否将NSTimer置nil,让iOS系统帮我们销毁NSTimer呢?

答案是否定的

为什么呢?

我们看一下ARC下的引用计数应该就明白了

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));

    _timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerSelector) userInfo:nil repeats:NO];

    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));

    [_timer invalidate];

    NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));

}

- (void) timerSelector {
    NSLog(@"----");
}
复制代码

我们打印self(就是当前VC)的引用计数
在这里插入图片描述

加入NSTimer后,引用计数从29变成了30
怎么理解呢?

其实就是timer对VC进行了强引用
因为如果要让timer运行的时候执行VC下面的timerSelector:,timer需要知道target,并且保存这个target。以便于在以后执行[target performSelector:],这里的target就是指VC(self)。

所以,NSTimer和VC时相互强引用的。这样看来就形成了循环引用的问题。

为了解除循环引用,在invalidate这个方法下,timer之前保存的target被设置成了nil,强制断开了循环引用。这点和直接设置timer = nil是差不多的。但是invalidate还做了另外一个动作,就是解除了runLoop对timer的强引用,使得timer成功停止。

我们从官方文档中可以看到这一点
在这里插入图片描述

  1. 解除强引用
  2. 从runloop中移除该定时器(解除RunLoop对timer的强引用)

所以在创建NSTimer对象的类的dealloc里面去invalidate timer。这就是想当然的做法,因为是强引用的,在没有被invalidate之前,dealloc都不可能被执行到

面试题

  • NSTimer一定会持有self吗?

从官方文档看三种创建方法
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
在这里插入图片描述
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
在这里插入图片描述
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
在这里插入图片描述
这三种方法无一例外,根据官方文档,都会对target产生强引用
所以,只要使用了target-action就会引用self。
苹果应该也发现了timer导致的内存泄漏的问题,所以iOS10之后,这三个出了对应新的API,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
调用这些新的API不会导致内存泄漏的问题

  • 如何解决NSTimer强持有的问题

合适的时机销毁Timer,或者使用一个代理对象,让timer引用代理对象,代理对象弱引用self。最合适的就是使用代理对象转发,系统提供的NSProxy(消息转发机制)。我们下面说

  • 那这里self不持有NSTimer也会出现循环引用吗

感觉像是设套的 你不能回答是或者不是 你只能说和循环引用无关 是大家相互引用,导致无法释放。使用NSTimer你需要加到Runloop中,如果是target-action,NSTimer是需要引用target也就是self。这样就形成了一个runloop->timer->self 都是强引用。大家都释放不了
在这里插入图片描述

在这里插入图片描述

  • 那runloop会持有NSTimer吗

NSTimer只有被加入到runloop中,才会生效,NSTimer才会真正执行,真正执行了RunLoop就会持有NSTimer。

如何解决NSTimer强持有的问题?

这是上面的第二个面试题

中间的代理对象

使用NSObject类实现消息转发

在这里插入图片描述
图就长这样

正常使用target-action,当页面销毁时,不走dealloc方法,造成循环引用
在这里插入图片描述

_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TestProxy proxyWithTarget:self] selector:@selector(timerSelector) userInfo:nil repeats:YES];




@interface TestProxy : NSObject

+ (instancetype) proxyWithTarget:(id)target;
@property (nonatomic, weak) id target;

@end


#import "TestProxy.h"

@implementation TestProxy

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

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

@end
复制代码

我们将target的self换成中间类,进行消息转发
在这里插入图片描述

成功打印dealloc

使用NSProxy类实现消息转发

这是一个专门用于做消息转发的类,我们需要通过子类的方式来使用它

需要注意

NSProxy的子类需要实现两个方法,就是上面那两个,即:methodSignatureForSelector:和forwardInvocation:

@interface TestNSProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@property (nonatomic, weak) id target;

@end




#import "TestNSProxy.h"

@implementation TestNSProxy

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

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

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

@end

复制代码

NSProxy类的优点在于直接执行了消息转发机制三次拯救的第三步的调用methodSignatureForSelector:返回方法签名,如果方法签名不为nil,调用forwardInvocation:来执行该方法,会跳过前面的步骤,提高性能

改变timer引用

我一开始就想着 我们解决block中的循环引用是怎么解决的?__weak typeof(self) weakSelf = self;,然后持有这个假self。但事实上,我们在这里使用target-action+“假self”并不能解决

为什么?

我们在block说过,如果外面是个强指针,block引用的时候哪股就用强指针保存,如果外面是个弱指针,block引用的时候内部就用弱指针保存,所以对于block我们使用weakSelf有用。

但是对于CADisplayLink和NSTimer来说,无论外面传递的是弱指针还是强指针,都会传入一个内存地址,定时器内部都是对这个内存地址产生强引用,所以传递弱指针没有用。

那么用block就可以达到这个目的

__weak typeof(self) weakSelf = self;
    self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf doSomething];
    }];
复制代码

在合适的地方调用invalidate方法

这算一个比较取巧的。 最合适的就是使用代理对象转发,系统提供的NSProxy。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享