解决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基本一致。
以上就是完整的解决方案,如果文中出现不对的地方,欢迎各位指正。