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