解决NSTimer的循环引用

解决NSTimer的循环引用

一、循环引用的原因

一般我们使用NSTimer,都是设置成控制器的属性@property (strong, nonatomic) NSTimer *timer;,控制器强引用timer。而timer对添加的target(一般是控制器本身,也就是self)也是强引用关系,这样引用关系就变成了self => timer => self,形成了循环引用。可能有些人认为用 __weak修饰self方式打破循环引用__weak typeof(self) weakSelf = self,其实这样做是无效的,timer对target是强引用,而__weak weakSelf只是影响它所引用的对象retainCount,并不影响timer对target的retainCount。另外,即使self对timer没有持有关系,由于runloop对timer有持有关系,则 runloop => timer => target(self控制器),可以看到self的引用计数除非主动断开timer对self的强引用,否则不可能为0。

二、探索解决思路

要想打破循环引用,就要破掉其中一个的引用关系使之不能形成循环,首先self => timer的引用关系必然是强引用,那就要针对timer => target的引用下手了。一个比较容易想到的思路是自定义一个类A,A中设置一个weak属性@property (weak, nonatomic) id target;,我们给timer设置target的时候,首先创建Aclass的实例a,然后设置a.target = self,最后将这个a作为timer的target,整个的引用关系就变成 self => timer => a --> self(注意这里用-->表示弱引用),可以看到没有形成循环,此思路可行。

接下来的问题就在于如何让self接收timer的回调而不是a对象。好在oc中有runtime,可以通过消息转发的方式将消息转发到我们指定的对象上(self,timer所在控制器)。

简单说下消息转发机制,首先我们给一个对象obj发送一个消息@selector(msg),但是这个对象并没有msg方法,程序不会立马抛出异常,而是有三步消息转发的机会。

第一步:

  • +(BOOL)resolveInstanceMethod:(SEL)sel
  • +(BOOL)resolveClassMethod:(SEL)sel

第二步:

  • (id)forwardingTargetForSelector:(SEL)aSelector

第三步:

  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
  • (void)forwardInvocation:(NSInvocation *)anInvocation

所以我们要想讲timer的回调转发到self上可以在第二步return a.target,或者在第三步拿到target方法签名,交由target去执行方法。

三、解决方案

这里介绍一个OC专门用来消息转发的类NSProxy,这个类不是继承自NSObject,仅仅是用来做消息转发的,注意这个类没有实现init方法,我们只需alloc就行。

另外对于NStimer,需要注意的是它是类簇的方式实现的,我们不能直接继承NSTimer,所以这里采用继承NSObject,内部持有一个NSTimer实例的方式。

1、首先创建一个消息转发类

@interface KJWeakProxy : NSProxy

@property (weak, nonatomic) id target;

+ (instancetype)weakProxyWithTarget:(id)target;
- (instancetype)initWeakProxyWithTarget:(id)target;
@end

@implementation KJWeakProxy

+ (instancetype)weakProxyWithTarget:(id)target {
    KJWeakProxy *proxy = [[self alloc] initWeakProxyWithTarget:target];
    return proxy;
}
- (instancetype)initWeakProxyWithTarget:(id)target {
    self = [KJWeakProxy alloc];
    self.target = target;
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    void *nullValue = NULL;
    [invocation setReturnValue:&nullValue];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

@end

主要是消息转发的步骤,直接通过消息转发的第二步,将方法转交给target去执行。注意我下面还针对消息转发第三步做了一些事情,因为target是weak引用,所以在forwardingTargetForSelector中可能会返回nil,此时走消息转发第三步。为了防止触发doesNotRecognizeSelector,在methodSignatureForSelector中返回NSObject实例的init方法签名,并在forwardInvocation中设置返回值为nil。

2、自定义timer类

.h文件:

@interface KJTimer : NSObject

@property (copy) NSDate *fireDate;
@property (readonly) NSTimeInterval timeInterval;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep runloopMode:(NSRunLoopMode)mode;

+ (instancetype)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo runloopMode:(NSRunLoopMode)mode;
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo runloopMode:(NSRunLoopMode)mode;

- (void)fire;
- (void)invalidate;

@end

仿照NSTimer的API创建实例对象,初始化方法增加一个runloopMode参数,方便timer运行在不同mode下。

.m文件:

@interface KJTimer ()

@property (strong, nonatomic) NSTimer *timer;


@end

@implementation KJTimer

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

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep runloopMode:(NSRunLoopMode)mode {
    if (self = [super init]) {
        KJWeakProxy *weakProxy = [KJWeakProxy weakProxyWithTarget:t];
        
        _timer = [[NSTimer alloc] initWithFireDate:date interval:ti target:weakProxy selector:s userInfo:ui repeats:rep];
        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:mode];
    }
    
    return self;
}

+ (instancetype)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo runloopMode:(NSRunLoopMode)mode {
    KJTimer *timer = [[KJTimer alloc] initWithFireDate:[NSDate distantFuture] interval:ti target:aTarget selector:aSelector userInfo:userInfo repeats:yesOrNo runloopMode:mode];
    return timer;
}
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo runloopMode:(NSRunLoopMode)mode {
    KJTimer *timer = [[KJTimer alloc] initWithFireDate:[NSDate distantPast] interval:ti target:aTarget selector:aSelector userInfo:userInfo repeats:yesOrNo runloopMode:mode];
    return timer;
    
}

- (void)fire {
    [self.timer setFireDate:[NSDate distantPast]];
}
- (NSDate *)fireDate {
    return self.timer.fireDate;
}
- (void)setFireDate:(NSDate *)date {
    [self.timer setFireDate:date];
}
- (NSTimeInterval)timeInterval {
    return self.timer.timeInterval;
}
- (void)invalidate {
    [self.timer invalidate];
     self.timer = nil;
}
@end

在初始化方法中,直接将timer添加到当前runloop中,另外需要注意在dealloc中[self.timer invalidate],将timer从当前runloop移除,其余的使用方式跟NSTimer基本一致。

以上就是完整的解决方案,如果文中出现不对的地方,欢迎各位指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容