解決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基本一致。

以上就是完整的解決方案,如果文中出現不對的地方,歡迎各位指正。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容