处理NSTimer的循环引用

错误用法

NSTimer 和 self会导致相互引用

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

@implementation CircleRetainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    
    if (!self.timer) {
        // 这是一种会导致循环引用的错误用法
        self.timer = [NSTimer scheduledTimerWithTimeInterval:2.f
                                                      target:self
                                                    selector:@selector(printNum)
                                                    userInfo:nil
                                                     repeats:YES];
    }
}

- (void)printNum {
    NSLog(@"zhiyun>>>>");
}

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

正确解法

我们考虑新增一个代理,让timer引用代理proxy,proxy再弱引用 self,这样的话,self 和 timer 之间就不会产生循环引用,最后我们在 self被释放后执行 invalidate 终止 timer。
同时,我们需要利用消息转发机制把 selector的执行转接到 真正的target.

额外话: 如果要求timer的执行不受滚动影响,可以指定加入的runloop的 mode 为 NSRunLoopCommonModes。

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

NSTimer+Util.h

@interface NSTimer (Util)

+ (NSTimer *)yun_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(nullable id)userInfo
                                        repeats:(BOOL)yesOrNo;
@end


NSTimer+Util.m

#import "NSTimer+Util.h"
#import "YUNTimerWeakTargetProxy.h"

@implementation NSTimer (Util)
+ (NSTimer *)yun_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                         target:(id)target
                                       selector:(SEL)selector
                                       userInfo:(nullable id)userInfo
                                        repeats:(BOOL)yesOrNo {
    
    YUNTimerWeakTargetProxy *proxy = [[YUNTimerWeakTargetProxy alloc] initWithTarget:target selector:selector];
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:ti
                                                      target:proxy
                                                    selector:selector
                                                    userInfo:userInfo
                                                     repeats:yesOrNo];
    proxy.timer = timer;
    return timer;
}

@end

YUNTimerWeakTargetProxy.h

@interface YUNTimerWeakTargetProxy : NSProxy

@property(nonatomic, weak) NSTimer *timer;

- (id)initWithTarget:(id)target selector:(SEL)sel;

@end

YUNTimerWeakTargetProxy.m

#import "YUNTimerWeakTargetProxy.h"

@interface YUNTimerWeakTargetProxy ()
@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL selector;
@property (nonatomic, strong) NSMethodSignature *sig;
@end

@implementation YUNTimerWeakTargetProxy

- (id)initWithTarget:(id)target selector:(SEL)sel {
    if (self) {
        self.target = target;
        self.selector = sel;
        self.sig = [self.target methodSignatureForSelector:self.selector];
    }
    
    return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == self.selector)
        return self.sig;
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    if (invocation.selector == self.selector) {
        if (self.target) {
            [invocation invokeWithTarget:self.target];
        } else {
            [self.timer invalidate];
            self.timer = nil;
            NSLog(@"%@ timer auto stop", NSStringFromSelector(self.selector));
        }
    } else {
        [super forwardInvocation:invocation];
    }
}

@end

这样修改过后,业务调用yun_scheduledTimerWithTimeInterval即可,不会出现循环引用

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

@implementation CircleRetainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    
    if (!self.timer) {
        self.timer = [NSTimer yun_scheduledTimerWithTimeInterval:2.f
                                                          target:self
                                                        selector:@selector(printNum)
                                                        userInfo:nil
                                                         repeats:YES];
    }
}

- (void)printNum {
    NSLog(@"zhiyun>>>>");
}

你可能感兴趣的:(处理NSTimer的循环引用)