NSTimer是iOS開發中的最多見的定時器。數組
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
複製代碼
- (void)setupNSTimer {
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(onTimerAction) userInfo:nil repeats:YES];
[timer fire];
}
複製代碼
Timer不只會持有target,也會持有userInfo對象。bash
還有使用block參數的接口:less
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
複製代碼
在iOS的Target-Action模式中, UIControl(如UIButton)對其target的持有方式是 weakRetained 的方式, 所以不會存在循環引用.ide
而NSTimer對其target持有的方式是 autorelease 方式, 即target會在其指定的runloop下一次執行時查看是否進行釋放. 若repeats參數爲YES, 則timer未釋放狀況下, target不會釋放, 於是會引發循環引用; 若repeats參數設置爲NO, 則target能夠被釋放而不會存在循環引用.函數
參考: iOS Target-Action模式下內存泄露問題深刻探究oop
NSTimer是基於RunLoop的,以scheduledTimerWithTimeInterval:開頭的方法會將NSTimer加到當前runloop的default mode上。post
而以timerWithTimeInterval:開頭的方法,則須要使用runloop的addTimer:方法,將其手動加到runloop上。動畫
所以,這裏一般有一個注意的點:即runloop的UITrackingMode下,定時器會失效。解決辦法即將定時器加到runloop的commonModes上便可。ui
NSTimer引起循環引用的本質是:this
Current RunLoop -> CFRunLoopMode -> sources數組 -> __NSCFTimer -> _NSTimerBlockTarget -> self
複製代碼
因此,必須保證NSTimer執行invalidate方法,self對象才能釋放。
即便在UIViewController的dealloc方法手動添加NSTimer的銷燬方法,也沒法解除循環引用,由於該dealloc方法根本不會調用。
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
複製代碼
一般,在UIViewController中,可在關閉界面的時候手動銷燬定時器,以解除循環引用。
對於NSTimer,如何解除循環引用,一般有幾種方式。
WeakContainer對象弱引用self對象,而後Timer的target設置爲WeakContainer對象,在WeakContainer對象中將消息轉發給target來執行便可。
@interface WeakContainer : NSObject
- (instancetype)initWithTarget:(id)target;
@end
@interface WeakContainer ()
@property (nonatomic, weak) id target;
@end
@implementation WeakContainer
- (instancetype)initWithTarget:(id)target {
self = [super init];
if (self) {
self.target = target;
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([self.target respondsToSelector:aSelector]) {
return self.target;
}
return [super forwardingTargetForSelector:aSelector];
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"doesNotRecognizeSelector %@ %@", self.target, NSStringFromSelector(aSelector));
}
@end
複製代碼
- (void)dealloc {
[self removeTimer];
}
- (void)setupTimer {
self.weakTimer = [NSTimer scheduledTimerWithTimeInterval:2
target:[[WeakContainer alloc] initWithTarget:self]
selector:@selector(onTimer)
userInfo:nil
repeats:YES];
[self.weakTimer fire];
}
- (void)removeTimer {
[self.weakTimer invalidate];
self.weakTimer = nil;
}
複製代碼
則,target對象的釋放再也不受到NSTimer的影響。
這裏,使用了一個WeakContainer,繼承自NSObject,對NSTimer的target進行弱持有。而更合適的方式,是使用NSProxy。
NSProxy implements the basic methods required of a root class, including those defined in the NSObjectProtocol protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation(_:) and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself
複製代碼
NSProxy是除了NSObject以外的另外一個基類,是一個抽象類,只能繼承它,重寫其消息轉發的方法,將消息轉發給另外一個對象。
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
複製代碼
除了重載消息轉發機制的兩個方法以外,NSProxy也沒有其餘功能了。即,使用NSProxy註定是用來轉發消息的。
@interface WeakProxy : NSProxy
- (instancetype)initWithTarget:(id)target;
@end
@interface WeakProxy ()
@property (nonatomic, weak) id target;
@end
@implementation WeakProxy
- (instancetype)initWithTarget:(id)target {
self = [WeakProxy alloc];
self.target = target;
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
複製代碼
- (void)dealloc {
[self removeTimer];
}
- (void)setupTimer {
self.weakTimer = [NSTimer scheduledTimerWithTimeInterval:2
target:[[WeakProxy alloc] initWithTarget:self]
selector:@selector(onTimer)
userInfo:nil
repeats:YES];
[self.weakTimer fire];
}
- (void)removeTimer {
[self.weakTimer invalidate];
self.weakTimer = nil;
}
複製代碼
能夠看出,兩種方式的代碼幾乎相同。只是NSProxy的特色要仔細體會。
@interface NSTimer (WeakTimer)
+ (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
block:(void(^)(void))block;
@end
@implementation NSTimer (WeakTimer)
+ (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
block:(void(^)(void))block
{
return [self scheduledTimerWithTimeInterval:ti
target:self
selector:@selector(onTimer:)
userInfo:[block copy]
repeats:yesOrNo];
}
+ (void)onTimer:(NSTimer *)timer {
void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end
複製代碼
- (void)dealloc {
[self removeTimer];
}
- (void)setupTimer {
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer weak_scheduledTimerWithTimeInterval:2 repeats:YES block:^{
[weakSelf onTimer];
}];
[self.timer fire];
}
- (void)removeTimer {
[self.timer invalidate];
self.timer = nil;
}
複製代碼
這兩種方式的實現不一樣,但本質上都要作到兩點:
CADisplayLink是以屏幕刷新頻率將內容繪製到屏幕上的定時器,適合作UI的不停重繪,動畫或視頻的渲染等。
一旦CADisplayLink以特定的模式添加到RunLoop中,每當屏幕須要刷新的時候,RunLoop就會調用CADisplayLink綁定的target上的selector方法,則target就可獲取CADisplayLink的每次調用的時間戳,用於準備下一幀顯示的數據。可用於動畫或視頻。使用CADisplayLink一樣要注意循環引用的問題。
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAction)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// displayLink.paused = YES;
// [displayLink invalidate];
// displayLink = nil;
複製代碼
相比執行,NSTimer的精確度稍低,若是NSTimer的觸發時間到了,而RunLoop處於阻塞狀態,則其觸發時間就會推遲至下一個RunLoop週期。其tolerance屬性就是用於設置能夠容忍的觸發時間的延遲範圍。
使用GCD Timer則不會有這個問題,不過用法複雜很多。
NSTimer實際上依賴於RunLoop,若RunLoop對應的任務繁重,則可能致使NSTimer執行很是不許時。且NSTimer在子線程中使用須要保證該子線程常駐,即runloop一直存在。
而GCD的定時器,是依賴於內核,不依賴於RunLoop,所以一般更加準時。
- (void)setupGCDTimer {
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
self.myGCDTimerQueue = dispatch_queue_create("com.icetime.mygcdtimer", attr);
/// 建立GCD timer
self.myGCDTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.myGCDTimerQueue);
/// 設置timer
uint64_t interval = 1 * NSEC_PER_SEC;
dispatch_source_set_timer(self.myGCDTimer, DISPATCH_TIME_NOW, interval, 0);
/// 設置timer的執行函數
dispatch_source_set_event_handler(self.myGCDTimer, ^{
NSLog(@"com.icetime.mygcdtimer");
});
/// 啓動timer
dispatch_resume(self.myGCDTimer);
}
複製代碼
// 暫停
dispatch_suspend(self.timer);
// 銷燬
dispatch_cancel(self.timer);
self.timer = nil;
複製代碼