NSTimer用法與循環引用

首先介紹NSTimer的幾種創建方式

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

常用方法

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

三種方法的區別是:

  • scheduledTimerWithTimeInterval方法不僅創建了NSTimer對象,還把該NSTimer對象加入到了當前的RunLoop(默認NSDefaultRunLoopModel模式)中。
  • 前兩個方法需要使用addTimer:forMode:方法將NSTimer加入到RunLoop中。
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;

與UIScrollView使用時注意事項

在當前線程為主線程時,某些UI事件,比如UIScrollView的拖動操作,會將RunLoop切換為NSEventTrackingRunLoopModel模式,在這個過程中,默認的NSDefaultRunLoopModel模式中注冊的事件是不會被執行的。
這時可以將Timer按照NSRunLoopCommonModes模式加入到RunLoop中。
通常情況下NSDefaultRunLoopMode和UITrackingRunLoopMode都已經被加入到了common modes集合中, 所以不論runloop運行在哪種mode下, NSTimer都會被及時觸發

如何銷毀NSTimer

invalidate方法的官方介紹:

Stops the timer from ever firing again and requests its removal from its run loop.
This method is the only way to remove a timer from an NSRunLoopobject. The NSRunLoop
object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

意思是:

  • invalidate方法會停止計時器的再次觸發,并在RunLoop中將其移除。
  • invalidate方法是將NSTimer對象從RunLoop中移除的唯一方法。
  • 調用invalidate方法會刪除RunLoop對NSTimer的強引用,以及NSTimer對target和userInfo的強引用!

那為什么RunLoop會對NSTimer強引用呢?

Timers work in conjunction with run loops. Run loops maintain strong references to their timers
( 計時器與運行循環一起工作。運行循環維護對計時器的強引用)

The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
(當計時器觸發后,在調用invalidated之前會一直保持對target的強引用)

以上也解釋了下面要說的NSTimer造成循環引用的原因

循環引用造成內存泄漏

循環引用示例.png

由上可見:NSTimer強引用了self,self也強引用了NSTimer,由此造成了循環引用,同時Runloop也強引用NSTimer。

  • 下面介紹兩種情況下解決循環引用
  1. 一般情況下直接在vc的viewWillDisappear中調用以下方法即可解決
- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [self.timer invalidate];
    self.timer = nil;
}
  1. 在A--push--B,B返回A

這種情況顯然是在dealloc中調用invalidate方法,
有些人可能會想將NSTimer弱引用

@property (nonatomic, weak)NSTimer *timer;
  • 但是RunLoop強引用了timer ~,timer強引用了vc,所以dealloc不會被調用!
  • 或者target傳入weakSelf,由于在invalidate方法調用之前,timer一直強引用target,而強引用了弱引用所引用的對象,等價于強引用!

下面介紹幾種成熟的解決方案

一. 使用自定義Category用Block解決
NSTimer+ZHWeakTimer.h

@interface NSTimer (ZHWeakTimer)
+ (NSTimer *)zh_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void (^)(void))eventBlock repeats:(BOOL)repeats;
@end
NSTimer+ZHWeakTimer.m

@implementation NSTimer (ZHWeakTimer)

+ (NSTimer *)zh_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval block:(void (^)(void))eventBlock repeats:(BOOL)repeats
{
    NSTimer *timer = [self scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(zh_executeTimer:) userInfo:[eventBlock copy] repeats:repeats];
    
    return timer;
}

+ (void)zh_executeTimer:(NSTimer *)timer
{
    void (^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}

@end

定時器對象指定的target是NSTimer類對象是個單例,因此計時器是否會保留它都無所謂。這么做,循環引用依然存在,但是因為類對象無需回收,所以能解決問題。

優點:代碼簡潔,邏輯清晰
缺點:
1.需要使用weakSelf避免block循環引用
2.不再使用原生API
3.同時要為NSTimer何CADisplayLink分別引進一個Category

二. GCD自己實現Timer

直接用GCD自己實現一個定時器,YYKit直接有一個現成的類YYTimer這里不再贅述。
缺點:代價有點大,需要自己重新造一個定時器。

三. 代理NSProxy

使用工具類YYWeakProxy解決NSTimer/CADisplayLink循環引用問題!

YYWeakProxy.h
@interface YYWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
-(instancetype)initWithTarget:(id)target;
+(instancetype)proxyWithTarget:(id)target;
@end
YYWeakProxy.m
-(instancetype)initWithTarget:(id)target {
 _target = target;
 return self;
}
+(instancetype)proxyWithTarget:(id)target {
 return [[YYWeakProxy alloc] initWithTarget:target];
}
-(id)forwardingTargetForSelector:(SEL)selector {
 return _target;
}
-(void)forwardInvocation:(NSInvocation *)invocation {
 void *null = NULL;
 [invocation setReturnValue:&null];
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
 return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
-(BOOL)respondsToSelector:(SEL)aSelector {
 return [_target respondsToSelector:aSelector];
}
-(BOOL)isEqual:(id)object {
 return [_target isEqual:object];
}
-(NSUInteger)hash {
 return [_target hash];
}
-(Class)superclass {
 return [_target superclass];
}
-(Class)class {
 return [_target class];
}
-(BOOL)isKindOfClass:(Class)aClass {
 return [_target isKindOfClass:aClass];
}
-(BOOL)isMemberOfClass:(Class)aClass {
 return [_target isMemberOfClass:aClass];
}
-(BOOL)conformsToProtocol:(Protocol *)aProtocol {
 return [_target conformsToProtocol:aProtocol];
}
-(BOOL)isProxy {
 return YES;
}
-(NSString *)description {
 return [_target description];
}
-(NSString *)debugDescription {
 return [_target debugDescription];
}
@end

該方法引入一個YYWeakProxy對象,在這個對象中弱引用真正的目標對象。通過YYWeakProxy對象,將NSTimer/CADisplayLink對象弱引用目標對象。
使用方法:

 self.timer = [NSTimer scheduledTimerWithTimeInterval:1
                                                  target:[YYWeakProxy proxyWithTarget:self]
                                                selector:@selector(timeEvent)
                                                userInfo:nil
                                                 repeats:YES];
- (void)timeEvent{
}
- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;// 對象置nil是一種規范和習慣
}
為什么NSProxy的子類YYWeakProxy可以解決呢?
  • NSProxy本身是一個抽象類,它遵循NSObject協議,提供了消息轉發的通用接口,NSProxy通常用來實現消息轉發機制和惰性初始化資源。不能直接使用NSProxy。需要創建NSProxy的子類,并實現init以及消息轉發的相關方法,才可以用。
  • YYWeakProxy繼承了NSProxy,定義了一個弱引用的target對象,通過重寫消息轉發等關鍵方法,讓target對象去處理接收到的消息。在整個引用鏈中,Controller對象強引用NSTimer/CADisplayLink對象,NSTimer/CADisplayLink對象強引用YYWeakProxy對象,而YYWeakProxy對象弱引用Controller對象,所以在YYWeakProxy對象的作用下,Controller對象和NSTimer/CADisplayLink對象之間并沒有相互持有,完美解決循環引用的問題。

參考文檔

1.iOS實錄8:解決NSTimer/CADisplayLink的循環引用
2.NSTimer Class

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

推薦閱讀更多精彩內容