在iOS開發(fā)中定時器是我們經(jīng)常遇到的需求动知,常用到的定時器表示方式有
NSTimer咪鲜、GCD
晌涕,那么它們之間有什么樣的區(qū)別呢滋捶?本文將從兩者的基本使用開始剖析它們之間的區(qū)別。
1余黎、NSTimer
1.1重窟、NSTimer簡介
NSTimer
是iOS中最基本的定時器。NSTimer
是通過RunLoop
來實現(xiàn)的惧财,在一般的情況下NSTimer作為定時器是比較準確的亲族,但是如果當前的耗時操作較多時,可能出現(xiàn)延時問題可缚。同時霎迫,因為受到RunLoop的支配,NSTimer會受到RunLoopMode
的影響帘靡。在創(chuàng)建NSTimer的時候默認是被加到defaultMode
的知给,但是如果在一個滑動的視圖中如tableview,當RunLoop的mode發(fā)生變化時,當前的NSTimer就不會工作了涩赢,這就是我們在開發(fā)中遇到的NSTimer用在tableview中戈次,當tableview滾動的時候NSTimer停止工作的原因,所以我們在創(chuàng)建NSTimer的時候?qū)⑵浼拥絉unLoop指定mode為NSRunLoopCommonModes
筒扒。
1.2怯邪、NSTimer基本使用
NSTimer的初始化方式有兩種,分別是invocation
和selector
兩種調(diào)用方式花墩,這兩種方式區(qū)別不大悬秉,但是selector
的方式更加簡便。
+ (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;
下面我們來看下這兩種方式的使用冰蘑。
1.2.1和泌、selector方式
使用selector方式初始化NSTimer比較簡單,只需要指定執(zhí)行的方法和是否循環(huán)就可以了祠肥。
- (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");
}
在上一個小節(jié)講過,NSTimer依賴于RunLoop剂桥,需要把初始化好的timer添加到RunLoop中弱睦,對于RunLoop的幾種模式在上面的代碼注釋中有說明。
這段代碼的運行結(jié)果就是每隔兩秒鐘就會打印一次“hello”
打印結(jié)果:
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] hello
1.2.1渊额、invocation方式
通過invocation方式初始化timer相對于來說會稍微復(fù)雜一些况木,最主要的是invocation參數(shù)。同樣的也需要手動將timer加入到RunLoop中旬迹。
- (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");
}
這段代碼的運行結(jié)果就是每隔兩秒鐘就會打印一次“hello”
打印結(jié)果:
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
1.2.3火惊、scheduledTimerWithTimeInterval方法
在上面列舉的API中其實有scheduledTimerWithTimeInterval
方法可以創(chuàng)建timer,這個方法和timerWithTimeInterval
的區(qū)別就在于前者會默認的將timer添加到了RunLoop奔垦,并且currentRunLoop是NSDefaultRunLoopMode
屹耐,而后者是需要開發(fā)者手動的將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");
}
這段代碼的運行結(jié)果就是每隔兩秒鐘就會打印一次“hello”
打印結(jié)果:
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照樣可以正常運行。
1.2.4 NSTimer在線程中使用
上面所列舉的例子都是在主線程中運行的犯眠,那是因為主線程默認是啟動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");
}
1.3鸯旁、NSTimer中存在的問題
1.3.1噪矛、RunLoop的mode問題
如果在一個滾動的視圖(如tableview)使用NSTimer,在視圖滾動的時候铺罢,timer會停止計時艇挨,那是因為當視圖滾動的時候RunLoop的mode是UITrackingRunLoopMode
模式。解決方式就是把timer 添加到RunLoop的NSRunLoopCommonModes
韭赘,那么UITrackingRunLoopMode
和kCFRunLoopDefaultMode
都被標記為了common
模式缩滨,就可以在默認模式和追蹤模式都能夠運行。
1.3.2泉瞻、NSTimer的循環(huán)引用
當NSTimer的target被強引用了脉漏,而target又強引用的timer,這樣就造成了循環(huán)引用瓦灶,導(dǎo)致timer無法釋放產(chǎn)生內(nèi)存泄露的問題鸠删。這也是在開發(fā)中經(jīng)常遇到的問題抱完。當然不是所有的NSTimer都會產(chǎn)生循環(huán)引用贼陶。
- repeats參數(shù)為NO的情況下,不會產(chǎn)生循環(huán)引用巧娱。
- ios10后的新的API方法
timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
也不會產(chǎn)生循環(huán)引用碉怔,但是不要忘記了在合適的地方調(diào)用invalidate
方法停止定時器的運行。
- ios10后的新的API方法
要解決NSTimer的循環(huán)引用問題就需要打破NSTimer和target之間的循環(huán)條件禁添,有如下幾種方式撮胧。
1.3.2.1、NSProxy的方式
創(chuàng)建一個中間類DSProxy繼承自NSProxy
老翘,這個類中對timer的target進行弱引用芹啥,再把需要執(zhí)行的方法都轉(zhuǎn)發(fā)給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
1.3.2.2铺峭、NSTimer封裝
這種方式其實和NSProxy的方式很類似墓怀,創(chuàng)建一個類對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
}
}
1.3.2.2傀履、block實現(xiàn)
@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
2、GCD
2.1莉炉、GCD簡介
GCD實現(xiàn)定時器功能钓账,是利用GCD中的Dispatch Source
中的一種類型DISPATCH_SOURCE_TYPE_TIMER
來實現(xiàn)的。dispatch源(Dispatch Source)監(jiān)聽系統(tǒng)內(nèi)核對象并處理絮宁,更加的精準梆暮。和NSTimer依賴于RunLoop不一樣,GCD并不依賴于RunLoop绍昂,所以即使是在滾動視圖中也不會出現(xiàn)視圖滾動時定時器不起效果的情況惕蹄。同時GCD定時器提供了定時器的啟動、暫停、回復(fù)卖陵、取消等功能遭顶,相對而言更加的貼近開發(fā)需求。
2.2泪蔫、GCD基本使用
GCD定時器調(diào)用 dispatch_source_create
方法創(chuàng)建一個source源棒旗,然后通過dispatch_source_set_timer
方法設(shè)置定時器,dispatch_source_set_event_handler
設(shè)置定時器任務(wù)撩荣,初創(chuàng)建的定時器是暫停的铣揉,需要調(diào)用dispatch_resume
方法啟動定時器,當然也可以調(diào)用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 {
/** 創(chuàng)建定時器對象
* para1: DISPATCH_SOURCE_TYPE_TIMER 為定時器類型
* para2-3: 中間兩個參數(shù)對定時器無用
* para4: 最后為在什么調(diào)度隊列中使用
*/
self.gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
/** 設(shè)置定時器
* para2: 任務(wù)開始時間
* para3: 任務(wù)的間隔
* para4: 可接受的誤差時間,設(shè)置0即不允許出現(xiàn)誤差
* 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
2.3台猴、GCD定時器的注意事項
1朽合、dispatch_resume
和dispatch_suspend
調(diào)用要成對出現(xiàn)。dispatch_suspend
嚴格上只是把timer暫時掛起饱狂,dispatch_resume
和dispatch_suspend
分別會減少和增加 dispatch 對象的掛起計數(shù)曹步。當這個計數(shù)大于 0 的時候,timer就會執(zhí)行休讳。但是Dispatch Source
并沒有提供用于檢測 source 本身的掛起計數(shù)的 API讲婚,也就是說外部不能得知一個 source 當前是不是掛起狀態(tài),那么在兩者之間需要設(shè)計一個標記變量俊柔。
2筹麸、source在suspend狀態(tài)下,如果直接設(shè)置source = nil或者重新創(chuàng)建source都會造成crash雏婶。正確的方式是在resume狀態(tài)下調(diào)用dispatch_source_cancel(source)釋放當前的source物赶。
3、dispatch_source_set_event_handler
回調(diào)是一個block尚骄,在添加到source中后會被source強引用块差,所以在這里需要注意循環(huán)引用的問題。正確的方法是使用weak+strong或者提前調(diào)用dispatch_source_cancel
取消timer倔丈。
3憨闰、NSTimer和GCD定時器的比較
- NSTimer依賴于RunLoop運行,所以在子線程中使用NSTimer需要手動啟動RunLoop需五。而GCD并不依賴于RunLoop鹉动,在子線程中可以正常使用。
- NSTimer依賴于RunLoop運行宏邮,在某種特定的環(huán)境下可能會需要RunLoop模式切換泽示。
- NSTimer會存在延時的可能性缸血,所以在定時層面準確性會有所偏差。GCD是監(jiān)聽系統(tǒng)內(nèi)核對象并處理械筛,定時更加精確捎泻。
- NSTimer的容易出現(xiàn)循環(huán)引用,GCD相對而言會好很多埋哟。當然規(guī)范編程合理設(shè)計這些都不是問題笆豁。