NSTimer循环引用分析,解决

NSTimer常见用法

@interface TimerClass : NSObject
- (void)start;
- (void)stop;
@end
@implementation TimerClass {
    NSTimer *_timer;
}
- (id)init {
    return [super init];
}
- (void)dealloc {
    NSLog(@"%s",__func__);
}
- (void)stop {
    [_timer invalidate];
    _timer = nil;
}
- (void)start {
    _timer = [NSTimerscheduledTimerWithTimeInterval:5.0 
                                            target:self  
                                          selector:selector(doSomething) 
                                          userInfo:nil 
                                           repeats:YES];
}
- (void)doSomething {//doSomething}
@end

上面代码很容易理解成

self 持有成员变量 NSTimer,成员变量 NSTimer 又强引用了 TimerClass 实例,才导致循环引用。

NSTimer思考

思考示例1

self 持有成员变量 NSTimer, 我们试着如果NSTimer不是成员变量,self没有持有成员变量,delloc方法会调用吗?

- (void)test {
    [NSTimer scheduledTimerWithTimeInterval:1
                                  target:self
                                selector:@selector(doSomething)
                                userInfo:nil
                                 repeats:YES];
}
- (void)doSomething {}
- (void)dealloc { NSLog(@"%s",__func__);}
思考示例2

成员变量NSTimer 又强引用了 TimerClass 实例 self ,那么如果向NSTimer中传入 __weak 修饰符修饰的self实例呢,delloc方法会调用吗??

__weak typeof(self) weakSelf = self;
self.mytimer = [NSTimer scheduledTimerWithTimeInterval:1 
                                                target:weakSelf 
                                              selector:@selector(doSomeThing) 
                                              userInfo:nil 
                                              repeats:YES];

在一个Controller加入该代码,我们就会发现dealloc都没有调用了。

那么很明显

TimerClass实例持有了,成员变量 NSTimer,成员变量 NSTimer 又强引用了 TimerClass 实例,才导致循环引用。

定时器加在 runloop 上才会起作用,到达时间点后就会执行 action 方法,并且可以肯定这是一个对象方法。 定时器运行在主线程的 runloop 上,然后又回调方法,这个方法属于你当前这个VC的对象方法。既然是对象方法且能被调用,那么肯定所属的对象一定的要持有,因此这个对象被持有了。

而我们通过 __weak 修饰 self,依然不能打破这个循环引用,说明这个对象依然是被强引用。

定时器加在 runloop 上, runloop 是持有定时器的,当不移除定时器且 runloop 一直存在的话那么每隔一段时间就会调用 action 这个方法,既然要调用这个对象方法,就需要占有这个对象。所以导致当前控制器VC不被释放,也证明了 局部变量的 NSTimer 造成循环引用的原因。

其实我们可以开启子线程的runloop, 添加定时器,通过终止子线程 runloop,就能验证这个问题。
runloop 中把 NSTimer移除 /终止 runloop

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

  • 使用类方法
  • 使用weakProxy
  • 使用GCD timer
weakProxy 解决循环引用

==NSProxy== 本身是一个抽象类,它遵循NSObject协议,提供了消息转发的通用接口。==NSProxy== 通常用来实现消息转发机制和惰性初始化资源。

@interface JZWeakProxy()
@property (nonatomic, weak, readonly) id target;
@end
@implementation JZWeakProxy
- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}
+ (instancetype)proxyWithTarget:(id)target {
    return [[self alloc] initWithTarget:target];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = [invocation selector];
    if([self.target respondsToSelector:sel]){
        [invocation invokeWithTarget:self.target];
    }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    NSAssert(self.target, @"目前对象失效, NSTimer必须注销");
    return [self.target methodSignatureForSelector:sel];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
    return [self.target respondsToSelector:aSelector];
}
@end

//VC
- (void)viewDidLoad{
    [super viewDidLoad];
    _timer = [NSTimer timerWithTimeInterval:1
                                         target:[JZWeakProxy proxyWithTarget:self]
                                       selector:@selector(doSomeThing)
                                       userInfo:nil
                                        repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)doSomeThing{
    NSLog(@"=====================");
}
- (void)dealloc{
    [_timer invalidate];
    _timer = nil;
    NSLog(@"%s",__func__);
}

通过使用NSProxy类,消息转发的接口,改变实例方法doSomeThing的调用者.

综合上述所说,NSTime 势必持有 JZWeakProx 实例对象, 然后结合消息转发,改变实例方法的调用者,从而实现 Controller 调用实例方法,JZWeakProxy 实例对象又不强引用 Controller实例,那么 Controller实例 能够正常释放。

注意

当 控制器实例 释放后,我们必须去是注销 NSTimer,即调用 invalidate 方法,否则抛出以下异常

Trapped uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSProxy doesNotRecognizeSelector:doSomeThing] called!'

原因很简单,因为调用者已经被释放,doSomeThing 事件没有调用实例。

Block解决循环引用
@interface NSTimer (JZBlocksSupport)
+ (NSTimer *)jz_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                       repeats:(BOOL)repeats;
@end

@implementation NSTimer (JZBlocksSupport)

+ (NSTimer *)jz_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                       repeats:(BOOL)repeats
{
    return [self scheduledTimerWithTimeInterval:interval
                                          target:self
                                        selector:@selector(jz_blockInvoke:)
                                        userInfo:[block copy]
                                         repeats:repeats];
}
+ (void)jz_blockInvoke:(NSTimer *)timer {
    void (^block)() = timer.userinfo;
    if(block) {
        block();
    }
}
@end
//调用
- (void)start {
    __weak JZClass *weakSelf = self;
    _timer = [NSTimer xx_scheduledTimerWithTimeInterval:.5
                                                 block:^{
                                                 JZClass *strongSelf = weakSelf;
                                                 [strongSelf doSomething];
                                                        }
                                               repeats:YES];
}

这里写图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]: \[runLoop addTimer:myTimer forMode:NSDefaultRunLoopMode\]; //实际上这步是不需要,scheduledTimerWithTimeInterval已经纳入当前线程运行。如果使用timerWithTimeInterval则需要。 引用\[2\]: \[\[NSRunLoop currentRunLoop\] addTimer:_timer forMode:NSDefaultRunLoopMode\]; //_timer = \[NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES\]; 引用\[3\]: GCD的定时器不受RunLoop中Mode的影响(RunLoop内部也是基于GCD实现的,可以根据源码看到), 比如滚动TableView的时候,GCD的定时器不受影响;且比NSTimer更加准时。 问题: NSTimer的mode是什么意思? 回答: NSTimer的mode是指定定时器在运行时所处的运行循环模式。在使用NSTimer时,可以通过指定mode来控制定时器在哪些运行循环模式下运行。比如在引用\[1\]和引用\[2\]中,都使用了NSDefaultRunLoopMode作为定时器的运行循环模式。这意味着定时器会在默认的运行循环模式下运行。而GCD的定时器则不受RunLoop中Mode的影响,可以在任何运行循环模式下运行,如引用\[3\]所示。 #### 引用[.reference_title] - *1* [iOS多线程的初步研究(四)-- NSTimer](https://blog.csdn.net/lengshengren/article/details/12905635)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [NSTimer 基本使用和注意事项](https://blog.csdn.net/wutengwei007/article/details/82221069)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值