在iOS開發中定時器是咱們常常遇到的需求,經常使用到的定時器表示方式有
NSTimer、GCD
,那麼它們之間有什麼樣的區別呢?本文將從二者的基本使用開始剖析它們之間的區別。ios
NSTimer
是iOS中最基本的定時器。NSTimer
是經過RunLoop
來實現的,在通常的狀況下NSTimer做爲定時器是比較準確的,可是若是當前的耗時操做較多時,可能出現延時問題。同時,由於受到RunLoop的支配,NSTimer會受到RunLoopMode
的影響。在建立NSTimer的時候默認是被加到defaultMode
的,可是若是在一個滑動的視圖中如tableview,當RunLoop的mode發生變化時,當前的NSTimer就不會工做了,這就是咱們在開發中遇到的NSTimer用在tableview中,當tableview滾動的時候NSTimer中止工做的緣由,因此咱們在建立NSTimer的時候將其加到RunLoop指定mode爲NSRunLoopCommonModes
。編程
NSTimer的初始化方式有兩種,分別是invocation
和selector
兩種調用方式,這兩種方式區別不大,可是selector
的方式更加簡便。bash
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (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;
複製代碼
下面咱們來看下這兩種方式的使用。markdown
使用selector方式初始化NSTimer比較簡單,只須要指定執行的方法和是否循環就能夠了。async
- (void)selectorType { NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES]; // NSDefaultRunLoopMode模式,切換RunLoop模式,定時器中止工做. // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; // UITrackingRunLoopMode模式,切換RunLoop模式,定時器中止工做. // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; // common modes的模式,如下三種模式的組合模式 NSDefaultRunLoopMode & NSModalPanelRunLoopMode & NSEventTrackingRunLoopMode [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } - (void)timerTest { NSLog(@"hello"); } 複製代碼
在上一個小節講過,NSTimer依賴於RunLoop,須要把初始化好的timer添加到RunLoop中,對於RunLoop的幾種模式在上面的代碼註釋中有說明。 這段代碼的運行結果就是每隔兩秒鐘就會打印一次「hello」oop
打印結果: 2020-03-16 17:55:24.123435+0800 ThreadDemo[3845:9977585] hello 2020-03-16 17:55:26.122417+0800 ThreadDemo[3845:9977585] hello 2020-03-16 17:55:28.123599+0800 ThreadDemo[3845:9977585] hello 2020-03-16 17:55:30.122504+0800 ThreadDemo[3845:9977585] helloatom
經過invocation方式初始化timer相對於來講會稍微複雜一些,最主要的是invocation參數。一樣的也須要手動將timer加入到RunLoop中。spa
- (void)invocationType { // 獲取到方法的簽名 NSMethodSignature *signature = [[self class]instanceMethodSignatureForSelector:@selector(timerTest)]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; invocation.target = self; invocation.selector = @selector(timerTest); NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 invocation:invocation repeats:YES]; [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes]; } - (void)timerTest { NSLog(@"hello"); } 複製代碼
這段代碼的運行結果就是每隔兩秒鐘就會打印一次「hello」線程
打印結果: 2020-03-16 22:54:48.964318+0800 ThreadDemo[6400:10171057] hello 2020-03-16 22:54:50.964530+0800 ThreadDemo[6400:10171057] hello 2020-03-16 22:54:52.964403+0800 ThreadDemo[6400:10171057] hello 2020-03-16 22:54:54.964780+0800 ThreadDemo[6400:10171057] hello設計
在上面列舉的API中其實有scheduledTimerWithTimeInterval
方法能夠建立timer,這個方法和timerWithTimeInterval
的區別就在於前者會默認的將timer添加到了RunLoop,而且currentRunLoop是NSDefaultRunLoopMode
,然後者是須要開發者手動的將timer添加到RunLoop中。
- (void)scheduledTimer { // NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES]; NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(timerTest)]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; invocation.target = self; invocation.selector = @selector(timerTest); NSTimer *timer2 = [NSTimer scheduledTimerWithTimeInterval:2.0 invocation:invocation repeats:YES]; } - (void)timerTest { NSLog(@"hello"); } 複製代碼
這段代碼的運行結果就是每隔兩秒鐘就會打印一次「hello」
打印結果: 2020-03-16 23:05:30.717027+0800 ThreadDemo[6581:10181270] hello 2020-03-16 23:05:32.715849+0800 ThreadDemo[6581:10181270] hello 2020-03-16 23:05:34.716522+0800 ThreadDemo[6581:10181270] hello
如上代碼所示,並無將timer添加到RunLoop,timer照樣能夠正常運行。
上面所列舉的例子都是在主線程中運行的,那是由於主線程默認是啓動RunLoop的,可是在線程是沒有默認開啓RunLoop的,因此當在子線程中使用NSTimer的時候就須要手動開啓RunLoop了。
- (void)timerInThread { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop]run]; }); } - (void)timerTest { NSLog(@"hello"); } 複製代碼
若是在一個滾動的視圖(如tableview)使用NSTimer,在視圖滾動的時候,timer會中止計時,那是由於當視圖滾動的時候RunLoop的mode是UITrackingRunLoopMode
模式。解決方式就是把timer 添加到RunLoop的NSRunLoopCommonModes
,那麼UITrackingRunLoopMode
和kCFRunLoopDefaultMode
都被標記爲了common
模式,就能夠在默認模式和追蹤模式都可以運行。
當NSTimer的target被強引用了,而target又強引用的timer,這樣就形成了循環引用,致使timer沒法釋放產生內存泄露的問題。這也是在開發中常常遇到的問題。固然不是全部的NSTimer都會產生循環引用。
timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
也不會產生循環引用,可是不要忘記了在合適的地方調用invalidate
方法中止定時器的運行。要解決NSTimer的循環引用問題就須要打破NSTimer和target之間的循環條件,有以下幾種方式。
建立一箇中間類DSProxy繼承自NSProxy
,這個類中對timer的target進行弱引用,再把須要執行的方法都轉發給timer的target。
@interface DSProxy : NSProxy @property (weak, nonatomic) id target; + (instancetype)proxyWithTarget:(id)target; @end @implementation DSProxy + (instancetype)proxyWithTarget:(id)target { DSProxy* proxy = [[self class] alloc]; proxy.target = target; return proxy; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation{ SEL sel = [invocation selector]; if ([self.target respondsToSelector:sel]) { [invocation invokeWithTarget:self.target]; } } @interface ProxyTimer : NSObject + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats; @end @implementation ProxyTimer + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats{ NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:[DSProxy proxyWithTarget: target] selector:selector userInfo:userInfo repeats:repeats]; return timer; } @end 複製代碼
這種方式其實和NSProxy的方式很相似,建立一個類對NSTimer進行封裝,將taget弱引用,
@interface DSTimer : NSObject @property (nonatomic, weak) id target; @property (nonatomic) SEL selector; + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats; @end + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(nullable id)userInfo repeats:(BOOL)repeats { DSTimer *dsTimer = [[DSTimer alloc] init]; dsTimer.target = target; dsTimer.selector = selector; NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:dsTimer selector:@selector(timered:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes]; return timer; } - (void)timered:(NSTimer *)timer { if ([self.target respondsToSelector:self.selector]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.target performSelector:self.selector withObject:timer]; #pragma clang diagnostic pop } } 複製代碼
@interface NSTimer (DSTimer) + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block; @end @implementation NSTimer (DSTimer) + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block { NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(timered:) userInfo:[block copy] repeats:repeats]; [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes]; return timer; } + (void)timered:(NSTimer *)timer { void (^ block)(NSTimer *timer) = timer.userInfo; block(timer); } @end 複製代碼
GCD實現定時器功能,是利用GCD中的Dispatch Source
中的一種類型DISPATCH_SOURCE_TYPE_TIMER
來實現的。dispatch源(Dispatch Source)監聽系統內核對象並處理,更加的精準。和NSTimer依賴於RunLoop不同,GCD並不依賴於RunLoop,因此即便是在滾動視圖中也不會出現視圖滾動時定時器不起效果的狀況。同時GCD定時器提供了定時器的啓動、暫停、回覆、取消等功能,相對而言更加的貼近開發需求。
GCD定時器調用 dispatch_source_create
方法建立一個source源,而後經過dispatch_source_set_timer
方法設置定時器,dispatch_source_set_event_handler
設置定時器任務,初建立的定時器是暫停的,須要調用dispatch_resume
方法啓動定時器,固然也能夠調用dispatch_suspend
或者dispatch_source_cancel
中止定時器。
下面是對於GCD的簡單封裝。
typedef enum : NSUInteger { Status_Running, Status_Pause, Status_Cancle, } TimerStatus; @interface GCDTimer () @property (nonatomic, strong) dispatch_source_t gcdTimer; @property (nonatomic, assign) TimerStatus currentStatus; @end @implementation GCDTimer - (void)scheduledTimerWithTimeInterval:(NSTimeInterval)interval runNow:(BOOL)runNow afterTime:(NSTimeInterval)afterTime repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block { /** 建立定時器對象 * para1: DISPATCH_SOURCE_TYPE_TIMER 爲定時器類型 * para2-3: 中間兩個參數對定時器無用 * para4: 最後爲在什麼調度隊列中使用 */ self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); /** 設置定時器 * para2: 任務開始時間 * para3: 任務的間隔 * para4: 可接受的偏差時間,設置0即不容許出現偏差 * Tips: 單位均爲納秒 */ dispatch_time_t when; if (runNow) { when = DISPATCH_TIME_NOW; } else { when = dispatch_walltime(NULL, (int64_t)(afterTime * NSEC_PER_SEC)); } dispatch_source_set_timer(self.gcdTimer, dispatch_time(when, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0); dispatch_source_set_event_handler(self.gcdTimer, ^{ if (!repeats) { dispatch_source_cancel(self.gcdTimer); } block(); }); dispatch_resume(self.gcdTimer); self.currentStatus = Status_Running; } - (void)pauseTimer { if (self.currentStatus == Status_Running && self.gcdTimer) { dispatch_suspend(self.gcdTimer); self.currentStatus = Status_Pause; } } - (void)resumeTimer { if (self.currentStatus == Status_Pause && self.gcdTimer) { dispatch_resume(self.gcdTimer); self.currentStatus = Status_Running; } } - (void)stopTimer { if (self.gcdTimer) { dispatch_source_cancel(self.gcdTimer); self.currentStatus = Status_Cancle; self.gcdTimer = nil; } } @end 複製代碼
一、dispatch_resume
和dispatch_suspend
調用要成對出現。dispatch_suspend
嚴格上只是把timer暫時掛起,dispatch_resume
和dispatch_suspend
分別會減小和增長 dispatch 對象的掛起計數。當這個計數大於 0 的時候,timer就會執行。可是Dispatch Source
並無提供用於檢測 source 自己的掛起計數的 API,也就是說外部不能得知一個 source 當前是否是掛起狀態,那麼在二者之間須要設計一個標記變量。 二、source在suspend狀態下,若是直接設置source = nil或者從新建立source都會形成crash。正確的方式是在resume狀態下調用dispatch_source_cancel(source)釋放當前的source。 三、dispatch_source_set_event_handler
回調是一個block,在添加到source中後會被source強引用,因此在這裏須要注意循環引用的問題。正確的方法是使用weak+strong或者提早調用dispatch_source_cancel
取消timer。