NSTimer 循环引用问题

1,147 阅读5分钟

1. 循环引用

问题代码

日常开发中,经常会用到NSTimer定时器,一些不正确的写法,会导致NSTimer造成循环引用,如下:

@interface TargetViewController ()
@property (nonatomic, strong) NSTimer *timer; 
@end
@implementation TargetViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector: @selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void**)timerAction {
    static uint64_t num = 0;
    NSLog(@"%s, num = %@", __func__ , @(num));
    num++;
}
- (void)dealloc {
    [self.timer invalidate];
    self.title = nil;
    NSLog(@"%s", __func__);
}
@end

这种代码必然会造成循环引用:

  • 创建timer时,将self传入target,导致timer持有self,而self又持有timer

  • 使用timerinvalidate方法可以解除timerself的持有,但是 timer 持有 self,导致 self 不可能调用 dealloc, 从而不能调用 invalidate 故此双方相互等待,造成循环引用

使用__weak 可行否

在解决 block 循环引用时,我们使用 __weak typeof(self) weakSelf = self;,那么 针对tiemer 是不是也可以这样呢?修改代码如下:

- (void)viewDidLoad {     
    [super viewDidLoad];    
    __weak typeof(self) weakSelf = elf;
    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector: @selector(timerAction) userInfo:nil repeats:YES];    
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; 
}

运行代码,依然不行,为啥呢? 查看官文 :

Xnip2022-08-05_15-13-19.png

  • target:定时器触发时指定的消息发送到的对象。计时器维护对该对象的强引用,直到它(计时器)失效

  • timerWithTimeInterval内部,使用强引用对象接收target参数,所以这里在外部定义为弱引用对象没有任何意义, 类似于这种代码:

    __weak typeof(self) weakSelf = self; // 外部
    __strong typeof(weakSelf)strongSelf = weakSelf; // 内部
    

Block的区别,Block将捕获到的弱引用对象,内部赋值给一个弱引用的临时变量,当Block执行完毕,临时变量会自动销毁,解除对外部变量的持有

2. 常规解决方法

使用 带blockAPI

使用携带Block的方法创建NSTimer,避免target的强持有

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval 
                                    repeats:(BOOL)repeats 
                                      block:(void (^)(NSTimer *timer))block;

在适当时机调用invalidate

如:在didMoveToParentViewController方法中

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

3. 打断强持有

除了常规解决方案,还可以通过打断target的强持有,解决循环引用的问题

中介者模式

- (void)viewDidLoad {
    [super viewDidLoad];

    NSObject *objc = [[NSObject alloc] init];
    class_addMethod([NSObject class], @selector(timerAction), (IMP)timerActionObjc, "v@:");
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:objc selector:@selector(timerAction) userInfo:nil repeats:YES];
}

void timerActionObjc(id obj){
    static uint64_t num = 0;
    NSLog(@"%s - %@ - %@", __func__ , @(num), obj);
    num++;
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s", **__func__** );
}
  • 创建NSObject实例对象objc,通过RuntimeNSObject增加timerAction方法,IMP指向timerActionObjc的函数地址

  • 创建NSTimer,将objc传入target参数,这样避免NSTimerself的强持有

  • 当页面退出时,由于self没有被NSTimer持有,正常调用dealloc方法

    • dealloc中,对NSTimer进行释放。此时NSTimerobjc的强持有解除,objc也跟着释放

封装自定义Timer

创建YJWeakTimer,实现自定义Timer的封装

打开YJWeakTimer.h文件,写入以下代码:

#import <Foundation/Foundation.h>
@interface YJWeakTimer : NSObject
- (instancetype)yj_initWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;
- (void)yj_invalidate;
@end

打开YJWeakTimer.m文件,写入以下代码:

#import "YJWeakTimer.h"
#import <objc/message.h>

@interface YJWeakTimer()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation YJWeakTimer
- (instancetype)yj_initWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {
    if (self == [super init]) {
        self.target     = target;
        self.aSelector  = selector;
        if ([self.target respondsToSelector:self.aSelector])
            Method method = class_getInstanceMethod([self.target class], selector);
            const char *type = method_getTypeEncoding(method);
            class_addMethod([self class], selector, (IMP)fireHomeWapper, type);
            self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:selector userInfo:userInfo repeats:repeats];
        }
    }
    return self;
}

void fireHomeWapper(YJWeakTimer *wtimer){
    if (wtimer.target) {
        void (*yj_msgSend)(void *, SEL, id) = (void *)objc_msgSend;
        yj_msgSend((__bridge void *)(wtimer.target), wtimer.aSelector, wtimer.timer);
    } else {
        [wtimer yj_invalidate];
    }
}

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

YJWeakTimer 的使用:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.weakTimer = [[YJWeakTimer alloc] yj_initWithTimeInterval:1 target:self selector: @selector(timerAction) userInfo:nil repeats:YES];
}

- (void)timerAction {
    static uint64_t num = 0;
    NSLog(@"%s, num = %@", __func__ , @(num));
    num++;
}
  • YJWeakTimer 中定义 yj_initWithTimeIntervalyj_invalidate 方法

    • YJWeakTimer 通过 weak 修饰的 target,对ViewController`进行弱持有

    • 检测target中是否能响应selector。能响应,对当前类通过Runtime API添加同名方法编号,指向自身内部fireHomeWapper的函数地址

    • 创建真正的NSTimer定时器,将控件自身的实例对象传入target,避免NSTimerViewController强持有

  • NSTimer回调时,会进入fireHomeWapper函数

    • 函数内部不负责业务处理,如果target存在,使用objc_msgSend,将消息发送给target自身下的selector方法
  • 当页面退出时,ViewController可以正常释放。但YJWeakTimerNSTimer相互持有,双方都无法释放

  • 由于双方都无法释放,NSTimer的回调会继续调用

    • 当进入fireHomeWapper函数,发现target已经不存在了,调用YJWeakTimeryj_invalidate方法,内部对NSTimer进行释放

    • NSTimer释放后,对YJWeakTimer的强持有解除,YJWeakTimer也跟着释放

NSProxy虚基类

NSProxy的作用:

  • OC不支持多继承,但是它基于运行时机制,可以通过NSProxy来实现伪多继承

  • NSProxyNSObject属于同一级别的类,也可以说是一个虚拟类,只实现了NSObject的协议部分

  • NSProxy本质是一个消息转发封装的抽象类,类似一个代理人

可以通过继承NSProxy,并重写以下两个方法实现消息转发

- (void)forwardInvocation:(NSInvocation *)invocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

NSProxy除了可以用于多继承,也可以作为切断强持有的中间人

打开YJProxy.h文件,写入以下代码:

Xnip2022-08-05_17-21-15.png

打开YJProxy.m文件,写入以下代码:

Xnip2022-08-05_17-21-47.png

YJProxy的调用使用:

- (void)viewDidLoad {
    [uper viewDidLoad];
    self.proxy = [YJProxy proxyWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction {
    static uint64_t num = 0;
    NSLog(@"%s, num = %@", __func__ , @(num));
    num++;
}
- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s", **__func__** );
}
  • YJProxy初始化方法,将传入的object赋值给弱引用对象

  • UIViewController中,创建YJProxy对象proxy。创建NSTimer对象,将proxy传入target,避免NSTimerViewController强持有

  • NSTimer回调时,触发YJProxy的消息转发方法

    • methodSignatureForSelector:设置方法签名
    • forwardInvocation:自身不做业务处理,将消息转发给object
  • 当页面退出时,ViewController可以正常释放

    • dealloc中,对NSTimer进行释放。此时NSTimerproxy的强持有解除,proxy也跟着释放

总结:

循环引用:

  • 创建NSTimer时,使用带有target参数的方法,会对传入的对象进行强引用。如果传入的是持有timer的对象,双发会相互持有,造成循环引用

  • 不能在UIViewControllerdealloc方法中释放timer。只要timer有效,UIViewControllerdealloc方法就不会执行。故此双方相互等待,谁都无法释放

  • NSTimertarget参数传入一个弱引用的self没有任何意义,因为在创建NSTimer的方法内部,使用强引用对象接收target参数

常规解决方案:

  • 使用携带Block的方法创建NSTimer,避免target的强持有

  • 根据业务需求,在适当时机调用invalidate。例如:viewWillDisappeardidMoveToParentViewController

切断target的强持有:

  • 中介者模式

  • 封装自定义Timer

  • 使用NSProxy虚基类