在iOS開發(fā)過程當(dāng)中序苏,我們經(jīng)常會直接或間接地使用到定時器手幢,iOS系統(tǒng)中,帶有延遲性操作的函數(shù)都是基于NSTimer忱详,CADisplayLink或者GCD定時器來實現(xiàn)的围来。本文主要也是圍繞這三種定時器展開,最后封裝一個簡單易用的定時器庫匈睁。
1监透、NSTimer定時器
1.NSTimer是基于NSRunloop的實現(xiàn)定時器,在使用NSTimer過程當(dāng)中航唆,應(yīng)該關(guān)注兩個問題
一胀蛮、直接使用NSTimer定時器,可能存在循環(huán)應(yīng)用問題糯钙。首先粪狼,NSTimer會強引用傳入的target對象, 而此時,如果target又對NSTimer產(chǎn)生強引用超营,那么就會引發(fā)循環(huán)引用問題鸳玩。
二、NSTimer回調(diào)的時間間隔可能會有存在誤差演闭。因為RunLoop每跑完一次圈再去檢查當(dāng)前累計時間是否已經(jīng)達到定時器所設(shè)置的間隔時間不跟,如果未達到,RunLoop將進入下一輪任務(wù)米碰,待任務(wù)結(jié)束之后再去檢查當(dāng)前累計時間窝革,而此時的累計時間可能已經(jīng)超過了定時器的間隔時間,故可能會存在誤差吕座。
2.針對循環(huán)引用問題虐译,我們可以使用中間類來解決。原理大致如下:
中間類繼承自NSProxy吴趴,基于消息轉(zhuǎn)發(fā)實現(xiàn)的漆诽,目的是為了提高方法調(diào)用效率。 實現(xiàn)代碼如下:
中間類.h聲明文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XBWeakProxy : NSProxy
/** weak target*/
@property (nonatomic, weak) id target;
/** init proxy by target*/
+ (instancetype)timerProxyWithTarget:(id)target;
@end
中間類.m聲明文件
#import "XBWeakProxy.h"
@implementation XBWeakProxy
+ (instancetype)timerProxyWithTarget:(id)target{
if (!target) return nil;
XBWeakProxy *proxy = [XBWeakProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
為了方便調(diào)用NSTimer,我們可以給NSTimer新增一個分類厢拭,給分類擴展類方法兰英,在擴展的方法中使用中間類來解決循環(huán)應(yīng)用問題。 同時可以利用runtime關(guān)聯(lián)技術(shù)供鸠,使用Block代替Selector回調(diào)畦贸。 代碼大致如下:
//.h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^XBTimerCallbackBlock)(NSTimer *timer);
@interface NSTimer (XbTimer)
/** 方法一,與系統(tǒng)同名方法一致楞捂, 需要手動添加到runloop中薄坏,自己控制啟動*/
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/** 方法二, 與系統(tǒng)同名方法一致寨闹,系統(tǒng)自動添加到runloop中胶坠,創(chuàng)建成功自動啟動*/
+ (NSTimer *)xb_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/** 方法三,block回調(diào)鼻忠, 不限制iOS最低版本涵但, 需要手動添加到runloop中,自己控制啟動*/
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(XBTimerCallbackBlock)block;
/** 方法四帖蔓,block回調(diào), 不限制iOS最低版本瞳脓, 系統(tǒng)自動添加到runloop中塑娇,創(chuàng)建成功自動啟動*/
+ (NSTimer *)xb_scheduledTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(XBTimerCallbackBlock)block;
@end
NS_ASSUME_NONNULL_END
//.m文件
#import "NSTimer+XBTimer.h"
#import "XBWeakProxy.h"
#import <objc/runtime.h>
@implementation NSTimer (XbTimer)
#pragma mark - Public
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo{
return [self timerWithTimeInterval:ti target:[XBWeakProxy timerProxyWithTarget:aTarget] selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
+ (NSTimer *)xb_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo{
return [self scheduledTimerWithTimeInterval:ti target:[XBWeakProxy timerProxyWithTarget:aTarget] selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
+ (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(nonnull XBTimerCallbackBlock)block{
if (!block) return nil;
NSTimer *timer = [self timerWithTimeInterval:interval target:[XBWeakProxy timerProxyWithTarget:self] selector:@selector(_blockAction:) userInfo:nil repeats:repeats];
if (!timer) return timer;
objc_setAssociatedObject(timer, @selector(_blockAction:), block, OBJC_ASSOCIATION_COPY);
return timer;
}
+ (NSTimer *)xb_scheduledTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(nonnull XBTimerCallbackBlock)block{
if (!block) return nil;
NSTimer *timer = [self scheduledTimerWithTimeInterval:interval target:[XBWeakProxy timerProxyWithTarget:self] selector:@selector(_blockAction:) userInfo:nil repeats:repeats];
if (!timer) return timer;
objc_setAssociatedObject(timer, @selector(_blockAction:), block, OBJC_ASSOCIATION_COPY);
return timer;
}
#pragma mark - Privite
+ (void)_blockAction:(NSTimer *)timer{
XBTimerCallbackBlock block = objc_getAssociatedObject(timer, _cmd);
!block?:block(timer);
}
@end
3.關(guān)于NSTimer時間誤差問題,可以使用GCD定時來代替NSTimer定時器劫侧,后面講GCD定時器部分會講到埋酬。
2、CADisplayLink定時器
CADisplayLink 依托于設(shè)備屏幕刷新頻率觸發(fā)事件烧栋,所以其觸發(fā)時間比NSTimer較準(zhǔn)確写妥,也是最適合做UI不斷刷新的事件,過渡相對流暢审姓,無卡頓感珍特。 而CADisplayLink定時器也是依賴于NSRunLoop, 所以CADisplayLink定時器也一樣會存在NSTimer的兩個問題。
針對解決循環(huán)引用問題魔吐,直接上代碼了:
//.h文件
#import <QuartzCore/QuartzCore.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^XBDisplayLinkCallbackBlock)(CADisplayLink *link);
@interface CADisplayLink (XBDisplayLink)
/** 同系統(tǒng)方法扎筒,僅解決循環(huán)引用問題*/
+ (CADisplayLink *)xb_displayLinkWithTarget:(id)target selector:(SEL)sel;
/** 同系統(tǒng)方法,自動添加到當(dāng)前runloop中酬姆,Mode: NSRunLoopCommonModes*/
+ (CADisplayLink *)xb_scheduledDisplayLinkWithTarget:(id)target selector:(SEL)sel;
/** Block callback嗜桌,auto run, runloop mode: NSRunLoopCommonModes*/
+ (CADisplayLink *)xb_scheduledDisplayLinkWithBlock:(XBDisplayLinkCallbackBlock)block;
@end
NS_ASSUME_NONNULL_END
//.m文件
#import "CADisplayLink+XBDisplayLink.h"
#import "XBWeakProxy.h"
#import <objc/runtime.h>
@implementation CADisplayLink (XBDisplayLink)
#pragma mark - Public
+ (CADisplayLink *)xb_displayLinkWithTarget:(id)target selector:(SEL)sel{
return [self displayLinkWithTarget:[XBWeakProxy timerProxyWithTarget:target] selector:sel];
}
+ (CADisplayLink *)xb_scheduledDisplayLinkWithTarget:(id)target selector:(SEL)sel{
CADisplayLink *link = [self displayLinkWithTarget:[XBWeakProxy timerProxyWithTarget:target] selector:sel];
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
return link;
}
+ (CADisplayLink *)xb_scheduledDisplayLinkWithBlock:(XBDisplayLinkCallbackBlock)block{
if (!block) return nil;
CADisplayLink *link = [self xb_displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
objc_setAssociatedObject(link, @selector(displayLinkAction:), block, OBJC_ASSOCIATION_COPY);
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
return link;
}
#pragma mark - Privite
+ (void)displayLinkAction:(CADisplayLink *)link{
XBDisplayLinkCallbackBlock block = objc_getAssociatedObject(link, _cmd);
!block?:block(link);
}
@end
3、GCD定時器
GCD定時器是這三種定時器中辞色,時間最為準(zhǔn)確的骨宠。因為GCD定時器不依賴與NSRunLoop, GCD定時器實際上是使用了dispatch源(dispatch source),dispatch源監(jiān)聽系統(tǒng)內(nèi)核對象并處理,通過系統(tǒng)級調(diào)用层亿,更加精準(zhǔn)桦卒。
以下是對GCD定時器的封裝,支持block和selector兩種回調(diào)方式
//.h
#import <Foundation/Foundation.h>
@class XBGCDTimer;
typedef void (^XBGCDTimerCallbackBlock)(XBGCDTimer *timer);
@interface XBGCDTimer : NSObject
/// Create GCDTimer, but not fire(定時器創(chuàng)建但未啟動)
/// @param start The number of seconds between timer first times callback since fire
/// @param interval The number of seconds between firings of the timer
/// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated
/// @param queue Queue for timer run and callback, default is in main queue
/// @param block Timer callback handler
+ (XBGCDTimer *)xb_GCDTimerWithSartTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
queue:(dispatch_queue_t)queue
repeats:(BOOL)repeats
block:(XBGCDTimerCallbackBlock)block;
/// Create GCDTimer and fire immdiately (定時器創(chuàng)建后馬上啟動)
/// @param start The number of seconds between timer first times callback since fire
/// @param interval The number of seconds between firings of the timer
/// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated
/// @param queue Queue for timer run and callback, default is in main queue
/// @param block Timer callback handler
+ (XBGCDTimer *)xb_scheduledGCDTimerWithSartTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
queue:(dispatch_queue_t)queue
repeats:(BOOL)repeats
block:(XBGCDTimerCallbackBlock)block;
/// Create GCDTimer, but not fire(定時器創(chuàng)建但未啟動)
/// @param target target description
/// @param selector selector description
/// @param start The number of seconds between firings of the timer
/// @param interval The number of seconds between firings of the timer
/// @param queue Queue for timer run and callback, default is in main queue
/// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated
+ (XBGCDTimer *)xb_GCDTimerWithTarget:(id)target
selector:(SEL)selector
SartTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
queue:(dispatch_queue_t)queue
repeats:(BOOL)repeats;
/// Create GCDTimer and fire immdiately (定時器創(chuàng)建后馬上啟動)
/// @param target target description
/// @param selector selector description
/// @param start The number of seconds between timer first times callback since fire
/// @param interval The number of seconds between firings of the timer
/// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated
/// @param queue Queue for timer run and callback, default is in main queue
+ (XBGCDTimer *)xb_scheduledGCDTimerWithTarget:(id)target
selector:(SEL)selector
SartTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
queue:(dispatch_queue_t)queue
repeats:(BOOL)repeats;
/** start*/
- (void)fire;
/** stop*/
- (void)invalidate;
@end
//.m
#import "XBGCDTimer.h"
#import "XBWeakProxy.h"
#import <objc/runtime.h>
@implementation XBGCDTimer
#pragma mark - Public
+ (XBGCDTimer *)xb_GCDTimerWithSartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats block:(XBGCDTimerCallbackBlock)block{
if (!block || start < 0 || (interval <= 0 && repeats)) return nil;
XBGCDTimer *gcdTimer = [[XBGCDTimer alloc] init];
// queue
dispatch_queue_t queue_t = queue ?: dispatch_get_main_queue();
// create
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue_t);
// set time
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
objc_setAssociatedObject(gcdTimer, @selector(fire), timer, OBJC_ASSOCIATION_RETAIN);
// callback
dispatch_source_set_event_handler(timer, ^{
block(gcdTimer);
if (!repeats) { // no repeats
[gcdTimer invalidate];
}
});
return gcdTimer;
}
+ (XBGCDTimer *)xb_scheduledGCDTimerWithSartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats block:(XBGCDTimerCallbackBlock)block{
XBGCDTimer *gcdTimer = [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:block];
[gcdTimer fire];
return gcdTimer;
}
+ (XBGCDTimer *)xb_GCDTimerWithTarget:(id)target selector:(SEL)selector SartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats{
XBWeakProxy *proxy = [XBWeakProxy timerProxyWithTarget:target];
return [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:^(XBGCDTimer * _Nonnull timer) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[proxy performSelector:selector];
#pragma clang diagnostic pop
}];
}
+ (XBGCDTimer *)xb_scheduledGCDTimerWithTarget:(id)target selector:(SEL)selector SartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats{
XBWeakProxy *proxy = [XBWeakProxy timerProxyWithTarget:target];
XBGCDTimer * gcdTimer = [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:^(XBGCDTimer * _Nonnull timer) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[proxy performSelector:selector];
#pragma clang diagnostic pop
}];
[gcdTimer fire];
return gcdTimer;
}
/** start*/
- (void)fire{
dispatch_source_t timer = objc_getAssociatedObject(self, _cmd);
if (timer) dispatch_resume(timer);
}
/** stop*/
- (void)invalidate{
dispatch_source_t timer = objc_getAssociatedObject(self, @selector(fire));
if (timer) dispatch_source_cancel(timer);
objc_removeAssociatedObjects(self);
}
@end
4棕所、總結(jié)
- NSTimer和CADisplayLink依賴于RunLoop闸盔,如果RunLoop的任務(wù)過于繁重,可能會導(dǎo)致NSTimer不準(zhǔn)時琳省,相比之下GCD的定時器會更加準(zhǔn)時迎吵,因為GCD不是依賴RunLoop,而是由內(nèi)核決定
- CADisplayLink和NSTimer會對target產(chǎn)生強引用针贬,如果target又對它們產(chǎn)生強引用击费,那么就會引發(fā)循環(huán)引用
如果你有什么意見和建議歡迎給我留言。
如果你對本文感興趣桦他,麻煩點個贊~~ 謝謝