美文网首页
NSTimer循环引用的问题

NSTimer循环引用的问题

作者: 浩然爸 | 来源:发表于2019-08-14 21:51 被阅读0次

前言

我们在开发中,经常会用到NSTimer这个类的+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo方法,但是NSTimer会对传入的target对象进行强引用,如果target又对timer进行了强引用,那么就会出现循环引用,今天我们就研究一下如何解决这个问题。

例如:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

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

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

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}
@end

当控制器pop出去的时候,会执行dealloc方法,看上去好像没什么问题,但是我们实际的运行结果是这样的:

2019-08-14 20:26:39.636581+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:40.636660+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:41.636513+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:42.636270+0800 定时器-01[16487:12091593] -[ViewController timerTest]

当我们的控制器pop消失以后控制台还在一直打印timerTest的方法,dealloc方法也没有执行。这是为什么?原因就是控制器timer进行了强引用,而timer又会对传入的target也就是控制器进行了强引用,这就造成了循环引用。

这时候我们就会想,将self通过__weak修饰一下不就可以了,那么我们来试一下,代码经过改造后如下:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}

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

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}
@end

这时候我们再次运行代码,结果还是一样的,控制器消失了,但是控制台还在一直打印timerTest方法,这是为什么呢?

其实原因还是上面所说的,timer对传入的terget也就是self进行了强引用,虽然我们在外部传入的是一个弱引用的weakSelf,但是timer内部还是有一个强指针指向了self的内存地址。

那么我们应该怎么解决这个问题呢?在这里我提供了三种解决方式:

  • 方案1:在- (void)viewWillDisappear:(BOOL)animated方法中调执行[_timer invalidate]方法

改造后的代码如下:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

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

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

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    //在此处调用,可以避免循环引用导致的内存问题
    [_timer invalidate];
}

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

运行结果如下:

2019-08-14 20:54:36.662354+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:37.662384+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:38.662300+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:39.662305+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:40.210602+0800 定时器-01[16508:12095299] -[ViewController dealloc]

我们看到,当 控制器消失的时候,执行了dealloc方法,为什么呢?原因是在[_timer invalidate]执行以后,释放了对target也就是self的强引用,循环引用被破坏,因此控制器可以被释放。

这种方式仅限于控制器中,当我们的传入的target不是一个控制器,而是一个普通的继承自NSObject的对象的时候,就不能用这种方式,因此我们来看下面的方法

  • 方案2:使用+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block方法

改造后的代码如下:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    if (@available(iOS 10.0, *)) {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf timerTest];
        }];
    } else {
        // Fallback on earlier versions
    }
}

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

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}

执行结果如下:

2019-08-14 20:44:26.588273+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:27.588241+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:28.588314+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:29.588270+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:29.611601+0800 定时器-01[16505:12094230] -[ViewController dealloc]

这时候,当控制器消失的时候,执行了dealloc方法,原因是虽然timerblock是强引用,但是block内部对于self是弱引用的,这个时候,破坏了循环引用,控制器没有被timer强引用,所以可以被释放。但是我们发现,这个方法只有在iOS10以后才有的,所以,如果我们的app的兼容版本是iOS 10以后的,那么我们可以这么写,如果要兼容更低版本,那么我们只能载想其他的办法。

  • 方案3:使用代理对象

首先我们创建一个代理对象,代码如下:

#import <Foundation/Foundation.h>

@interface TimerProxy : NSObject
@property (weak, nonatomic) id target;

+ (instancetype)proxyWithTarget:(id)target;

@end
#import "TimerProxy.h"
@implementation TimerProxy

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

//利用消息转发机制,将方法转发给target处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}
@end

然后改造原有的代码:

#import "TimerProxy.h"

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //此时,传入的target就不是self,而是TimerProxy对象
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[TimerProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

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

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}

执行结果如下:

2019-08-14 21:17:47.476647+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:48.475937+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:49.476636+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:50.476643+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:51.120013+0800 定时器-01[16527:12097738] -[ViewController dealloc]

这种方案,我们采用的是代理对象的方式,控制器对于timer是强引用,timer对于代理对象是强引用,而代理对象对于控制器是弱引用,这样我们就破坏了循环引用,最终调用了dealloc方,从而解决问题。

  • 方案4:使用NSProxy(推荐方案)

首先,我们自定义一个继承自NSProxy的类

#import <Foundation/Foundation.h>

@interface TimerProxy2 : NSProxy
@property (weak, nonatomic) id target;

+ (instancetype)proxyWithTarget:(id)target;
@end
#import "TimerProxy2.h"

@implementation TimerProxy2
+ (instancetype)proxyWithTarget:(id)target {
//    这句代码会报错,因为NSProxy无init方法,直接alloc即可
//    TimerProxy2 *proxy = [[TimerProxy2 alloc] init];
    TimerProxy2 *proxy = [TimerProxy2 alloc];
    proxy.target = target;
    return proxy;
}

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

//消息转发
- (void)forwardInvocation:(NSInvocation *)invocation {
    invocation.target = self.target;
    [invocation invokeWithTarget:self.target];
}
@end

改造原有代码

#import "TimerProxy2.h"

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //此时,传入的target就不是self,而是TimerProxy对象
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[TimerProxy2 proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

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

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}

执行结果如下:

2019-08-14 21:17:46.476786+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:47.476647+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:48.475937+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:49.476636+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:50.476643+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:51.120013+0800 定时器-01[16527:12097738] -[ViewController dealloc]

这种方案也可以解决循环引用的问题,但是大家可能觉得,这种写法相比方案3更麻烦一些,代码写的要更多,但是为什么更推荐这种实现方式呢?原因还要从oc方法调用分析。

oc的方法调用最终会转换为objc_msgSend函数进行调用,objc_msgSend的执行流程分为三大阶段

  • 消息发送
  • 动态解析
  • 消息转发

其中消息发送阶段,就会进行多次方法查找。首先从自身的方法缓存中进行查找,找不到会在本类的方法列表进行查找,找不到再去父类的缓存中进行查找,找不到再去父类的方法列表中进行查找...一直超找到根类,如果还没有找到的话,就会进行动态方法解析,没有实现动态方法解析的话,最后就会进入消息转发阶段。

而使用NSProxy会先从自身方法列表进行查找,找不到的话,会直接进入消息转发阶段,省去了去父类一层层查找和动态方法解析这些操作,因此效率更高。

自此,我们关于NSTimer引起的循环引用问题就算是彻底解决了,其实方法4也是给我们提供了一种思路,如果以后开发中遇到了类似于NSTimer这种的循环引用问题,我们完全可以通过代理对象这种方式来解决这种问题。

相关文章

  • ios学记0016-循环引用问题的解决方法

    Block,NSTimer循环引用问题的解决方法 摘要:NSTimer, Block, 循环引用, 内存泄漏 注:...

  • 解决NSTimer循环引用的三种方法

    总结一下项目中遇到的NSTimer循环引用问题 上述方法NSTimer作为属性时会产生循环引用问题,weaksel...

  • ios中循环引用问题

    ios中循环引用问题 NO1: NSTimer 问题:当你创建使用NSTimer的时候,NSTimer会默...

  • Controller销毁NSTimer释放的细节

    关于NSTimer释放和内存泄漏的问题。 @(NSTimer)[内存管理,NSTimer释放,循环引用] 首先需要...

  • iOS定时器循环引用分析及完美解决方案

    目录 1.NSTimer导致的循环引用分析2.NSTimer循环引用解决思路误区3.NSTimer循环引用解决方案...

  • NSTimer的循环引用

    NSTimer基本使用 NSTimer与RunLoop NSTimer 循环引用的问题 如何在子线程使用NSTim...

  • NSTimer 循环引用问题

    题记 在iOS 10系统之前,系统的NSTimer是会引起循环引用的,导致内存泄漏。下面就针对这个问题给出几种解决...

  • NSTimer循环引用问题

    项目中可能会用到NSTimer实现计时器功能,但NSTimer处理不当,会造成循环引用,内存泄漏。通常我们使用NS...

  • NSTimer循环引用问题

    在实际的开发中,在需要定时器时,这样的代码很常见。 那么这样代码如果处理不当,很容易引起循环引用问题。 循环引用产...

  • 4-8 循环引用

    3种循环引用 自循环引用 相互循环引用 多循环引用 Block的循环引用 NSTimer 的循环引用 破除循环引用...

网友评论

      本文标题:NSTimer循环引用的问题

      本文链接:https://www.haomeiwen.com/subject/kronjctx.html