NSTimer循环引用原因及解决方案

3,037 阅读4分钟

前段时间在群里和同事讨论起NSTimer循环引用的问题,当时胡诌了一顿,说的不是很明白,也不清楚自己的理解是否有问题,所以专门研究了一下,提供给大家

NSTimer创建

  • 第一种
self.timer = [NSTimer timerWithTimeInterval:1
                                     target:self
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];
  • 如果在主线程里创建,需要修改下Mode为NSRunLoopCommonModes,不然,当滚动事件发生时,会导致NSTimer不执行,主线程的RunLoop是默认开启的,所以不需要[[NSRunLoop currentRunLoop] run]。
  • 如果在子线程里创建,且当前线程里无滚动事件,则不需要修改Mode,子线程的RunLoop默认不开启的,需要手动加入Runloop
  • 第二种
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                              target:self
                                            selector:@selector(fireHome)
                                            userInfo:nil
                                             repeats:YES];

循环引用分析

说到循环引用,大家都会想到有weakself替换self去解决,比如上面的写法改为

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1
                                     target:weakSelf
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];

同事: 为什么timer都弱引用了target,还是释放不了

  • self 强持有 timer 我们都能直接看出来,那么timer是什么时候强持有 self的呢?

截屏2021-10-08 下午4.33.01.png

看苹果官方文档可知:target方法中,timerself对象进行了强持有,因此造成了循环引用。

  • 但是当我们按照惯例用weakSelf去打破强引用的时候,发现weakSelf没有打破循环引用,timer仍然在运行。

  • 即 self -> timer -> weakSelf -> self

来看看__weak typeof(self) weakSelf = self;做了什么 869753-26f8fb55fde27308.webp

从上图可知,weakSelfself两个指针地址不同但内存空间地址相同,也就是两个对象同时持有同一个内存空间.相当于NStimer间接的持有了self,所以weakSelf并没有打破循环关系

** 同事: 不使用属性或者成员变量切断当前类对timer的强引用,可以吧

NSTimer *timer = [NSTimer timerWithTimeInterval:1
                                     target:weakSelf
                                   selector:@selector(fireHome)
                                   userInfo:nil
                                    repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer
                                  forMode:NSDefaultRunLoopMode];

timer还是释放不了,原因如下

1335883-872d4c69c289a08f.webp

主线程的Runloop在程序运行期间是不会销毁的,它比self的生命周期都长,也就是runloop引用着timer,timer就不会销毁,timer引用着target,target也不会销毁.从runloop引用着timer这个思路来想,要打破循环,只能从timer引用target这个层面来打破

解决方案

通过上面的分析,我们知道需要打破的地方就是timer对self的强引用 截屏2021-10-08 下午5.28.56.png

方法如下:

1. 使用更新后的API

在iOS 10以后系统,苹果针对NSTimer进行了优化,使用Block回调方式,解决了循环引用问题。

  [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
       // Do some things
    }];

2. 在当前页面消失的时候释放timer对象

使用(void)didMoveToParentViewController:(UIViewController *)parent方法,在这个方法里清掉定时器,就会释放Timer对象,也就解决了强引用。即调用dealloc方法了。

//生命周期  移除childVC的时候
- (void)didMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
        [self.myTimer invalidate];
        self.myTimer = nil;
    }
}

缺点: 针对PresentVC不适应

3. 中间件方法

//定义个中间件属性
@property (nonatomic, strong) id target;

  _target = [NSObject new];
  class_addMethod([_target class], @selector(testTimer), (IMP)timerIMP, "v@:");
    //这里换成_target  不用self了。  这就没有了循环引用了
  self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_target selector:@selector(testTimer) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:self.myTimer forMode:NSDefaultRunLoopMode];

void timerIMP(id self, SEL _cmd) {
    NSLog(@"Do some things");
}

//停止Timer
- (void)dealloc {
    [self.myTimer invalidate];
    self.myTimer = nil;

    NSLog(@"Timer dealloc");
}

4. 使用NSProxy类

新建一个类TimerProxy,继承NSProxy,设置一个属性

//注意这里要使用weak
@property (nonatomic, weak) id target;

实现方法转发

/** 方法签名 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
/** 消息转发 */
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

在你需要的地方,然后导入TimerProxy头文件使用
@property (nonatomic, strong) TimerProxy *timerProxy;
    _timerProxy = [TimerProxy alloc];//注意这里只有alloc方法
    //self弱引用所以可以被释放
    _timerProxy.target = self;
    self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_timerProxy selector:@selector(testTimer) userInfo:nil repeats:YES];

5. 自定义Timer类

  1. 自定义一个类CustomTimer,定义两个属性,包括timer和target,timer就是真实的NSTimer对象,这里的target不在是timer的target,而是我们外面真正实现业务的对象
@property (nonatomic, strong) NSTimer *timer;
//target使用weak修饰
@property (nonatomic, weak) id target;
  1. self作为timer的target,实现timer回调方法handler:,同时创建一个NSInvocation对象,用于外部真正target的方法调用,invocation通过userInfo参数传递
- (instancetype)initWithTimeInterval:(NSTimeInterval)interval
                                   target:(id)target
                                 selector:(SEL)selector
                                  repeats:(BOOL)repeat
{
    self = [super init];
    if (self) {
        NSMethodSignature *methodSignature = [target methodSignatureForSelector:selector];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
        invocation.selector = selector;
        invocation.target = target;
        self.target = target;
        self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(handler:) userInfo:invocation repeats:repeat];
    }
    return self;
}
  1. 处理回调 在handler:中判断弱引用属性target是否为空,不为空则通过invocation调用target方法,如果为空则释放timer
- (void)handler:(NSTimer *)timer
{
    NSInvocation *invocation = [timer userInfo];
    if (self.target) {
        [invocation invoke];
    }else{
        [self invalidate];
    }
}

- (void)invalidate
{
    [self.timer invalidate];
    self.timer = nil;
}

6. 类似系统API,使用block方式

使用NSTimer的Category加block的方式

@implementation NSTimer (BlockTimer)

+ (NSTimer *)bt_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void(^)(NSTimer *timer))block{
    
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(handler:) userInfo:[block copy] repeats:repeats];
}

+ (void)handler:(NSTimer *)timer{
   
    void (^block)(NSTimer *timer) = timer.userInfo;
    if (block) {
        block(timer);
    }
}
@end