說到定時器, 我們使用最多的就是NSTimer 和 GCD 了, 還有另外一個高級的定時器 CADisplayLink;
一. NSTimer
NSTimer的初始化方法有以下幾種:
會自動啟動, 并加入MainRunloop 的NSDefaultRunLoopMode 中:
注意: 這里的自動啟動, 并不是馬上就會啟動, 而是會延遲大概一個interval的時間:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
參數(shù):
- internal : 時間間隔, 多久調用一次
- repeats: 是否重復調用
- block: 需要重復做的事情
使用:
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
static NSInteger num = 0;
NSLog(@"%ld", (long)num);
num++;
if (num > 4) {
[timer invalidate];
NSLog(@"end");
}
}];
NSLog(@"start");
這時, 控制臺的輸出:
2016-12-29 16:29:53.901 定時器[11673:278678] start
2016-12-29 16:29:54.919 定時器[11673:278678] 0
2016-12-29 16:29:55.965 定時器[11673:278678] 1
2016-12-29 16:29:56.901 定時器[11673:278678] 2
2016-12-29 16:29:57.974 定時器[11673:278678] 3
2016-12-29 16:29:58.958 定時器[11673:278678] 4
2016-12-29 16:29:58.959 定時器[11673:278678] end
可以看出, 這里的internal設置為1s, 大概延遲了1s才開始執(zhí)行block里的內(nèi)容;
這里的停止定時器, 我直接在block里進行的, 如果使用一個全局變量來再其他地方手動停止定時器,需要這樣進行:
[self.timer invalidate];
self.timer = nil;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo
參數(shù):
- ti: 重復執(zhí)行時間間隔
- invocation: NSInvocation實例, 其用法見NSInvocation的基本用法
- yesOrNo: 是否重復執(zhí)行
示例:
// NSInvocation形式
- (void)timer2 {
NSMethodSignature *method = [ViewController instanceMethodSignatureForSelector:@selector(invocationTimeRun:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:method];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];
// 設置方法調用者
invocation.target = self;
// 這里的SEL需要和NSMethodSignature中的一致
invocation.selector = @selector(invocationTimeRun:);
// 設置參數(shù)
// //這里的Index要從2開始,以為0跟1已經(jīng)被占據(jù)了,分別是self(target),selector(_cmd)
// 如果有多個參數(shù), 可依次設置3 4 5 ...
[invocation setArgument:&timer atIndex:2];
[invocation invoke];
NSLog(@"start");
}
- (void)invocationTimeRun:(NSTimer *)timer {
static NSInteger num = 0;
NSLog(@"%ld---%@", (long)num, timer);
num++;
if (num > 4) {
[timer invalidate];
}
}
輸出:
2016-12-29 16:52:54.029 定時器[12089:289673] 0---<__NSCFTimer: 0x60000017d940>
2016-12-29 16:52:54.029 定時器[12089:289673] start
2016-12-29 16:52:55.104 定時器[12089:289673] 1---<__NSCFTimer: 0x60000017d940>
2016-12-29 16:52:56.095 定時器[12089:289673] 2---<__NSCFTimer: 0x60000017d940>
2016-12-29 16:52:57.098 定時器[12089:289673] 3---<__NSCFTimer: 0x60000017d940>
2016-12-29 16:52:58.094 定時器[12089:289673] 4---<__NSCFTimer: 0x60000017d940>
可以看出, 這里定時器是立馬就執(zhí)行了, 沒有延遲;
此方法可以傳遞多個參數(shù), 下面是傳遞兩個參數(shù)的示例:
// NSInvocation形式
- (void)timer2 {
NSMethodSignature *method = [ViewController instanceMethodSignatureForSelector:@selector(invocationTimeRun:des:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:method];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];
// 設置方法調用者
invocation.target = self;
// 這里的SEL需要和NSMethodSignature中的一致
invocation.selector = @selector(invocationTimeRun:des:);
// 設置參數(shù)
// //這里的Index要從2開始,以為0跟1已經(jīng)被占據(jù)了,分別是self(target),selector(_cmd)
// 如果有多個參數(shù), 可依次設置3 4 5 ...
[invocation setArgument:&timer atIndex:2];
// 設置第二個參數(shù)
NSString *dsc = @"第二個參數(shù)是字符串";
[invocation setArgument:&dsc atIndex:3];
[invocation invoke];
NSLog(@"start");
}
- (void)invocationTimeRun:(NSTimer *)timer des:(NSString *)dsc {
static NSInteger num = 0;
NSLog(@"%ld---%@--%@", (long)num, timer, dsc);
num++;
if (num > 4) {
[timer invalidate];
}
}
輸出:
2016-12-29 16:57:45.087 定時器[12183:292324] 0---<__NSCFTimer: 0x60000016dbc0>--第二個參數(shù)是字符串
2016-12-29 16:57:45.088 定時器[12183:292324] start
2016-12-29 16:57:46.161 定時器[12183:292324] 1---<__NSCFTimer: 0x60000016dbc0>--第二個參數(shù)是字符串
2016-12-29 16:57:47.161 定時器[12183:292324] 2---<__NSCFTimer: 0x60000016dbc0>--第二個參數(shù)是字符串
2016-12-29 16:57:48.150 定時器[12183:292324] 3---<__NSCFTimer: 0x60000016dbc0>--第二個參數(shù)是字符串
2016-12-29 16:57:49.159 定時器[12183:292324] 4---<__NSCFTimer: 0x60000016dbc0>--第二個參數(shù)是字符串
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
參數(shù):
- ti: 時間間隔
- aTarget: 調用者
- aSelector: 執(zhí)行的方法
- userInfo: 參數(shù)
- yesOrNo: 是否重復執(zhí)行
示例:
- (void)timer3 {
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(targetRun:) userInfo:@"這是攜帶的參數(shù)" repeats:YES];
NSLog(@"start");
}
- (void)targetRun:(NSTimer *)timer {
static NSInteger num = 0;
NSLog(@"%ld---%@--%@", (long)num, timer, timer.userInfo);
num++;
if (num > 4) {
[timer invalidate];
}
}
輸出:
2016-12-29 17:05:11.590 定時器[12328:296879] start
2016-12-29 17:05:12.655 定時器[12328:296879] 0---<__NSCFTimer: 0x608000162700>--這是攜帶的參數(shù)
2016-12-29 17:05:13.661 定時器[12328:296879] 1---<__NSCFTimer: 0x608000162700>--這是攜帶的參數(shù)
2016-12-29 17:05:14.664 定時器[12328:296879] 2---<__NSCFTimer: 0x608000162700>--這是攜帶的參數(shù)
2016-12-29 17:05:15.651 定時器[12328:296879] 3---<__NSCFTimer: 0x608000162700>--這是攜帶的參數(shù)
2016-12-29 17:05:16.650 定時器[12328:296879] 4---<__NSCFTimer: 0x608000162700>--這是攜帶的參數(shù)
下面這三種方式創(chuàng)建定時器的用法, 和上面相應的方法類似, 需要注意的是, 這樣創(chuàng)建的定時器, 并不會執(zhí)行, 需要我們手動來開啟定時器;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo
開啟的方式是, 將當前定時器添加到RunLoop中:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
下面給出一個示例:
- (void)timer4 {
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
static NSInteger num = 0;
NSLog(@"%ld", (long)num);
num++;
if (num > 4) {
[timer invalidate];
timer = nil;
NSLog(@"end");
}
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
NSLog(@"start");
}
輸出:
2016-12-29 17:12:13.955 定時器[12498:301751] start
2016-12-29 17:12:15.013 定時器[12498:301751] 0
2016-12-29 17:12:16.018 定時器[12498:301751] 1
2016-12-29 17:12:17.011 定時器[12498:301751] 2
2016-12-29 17:12:18.024 定時器[12498:301751] 3
2016-12-29 17:12:19.023 定時器[12498:301751] 4
2016-12-29 17:12:19.023 定時器[12498:301751] end
定時器基本的創(chuàng)建方式就這些了, 還可以設置其他的屬性, 例如開啟時間, 這些直接參考其API 進行設置即可;
注意: 以上實例中, 我沒有使用全局的NSTimer 對象, 如果設置全局變量, 或者設置為屬性, 在停止定時器的時候要手動置為nil, 即:
[timer invalidate];
timer = nil;
二. GCD
dispatch_after : 延遲執(zhí)行一次
dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block)
示例:
- (void)gcdTimer {
// 延遲2s
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC);
dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
NSLog(@"延遲2s后執(zhí)行");
});
NSLog(@"start");
}
重復執(zhí)行的定時器
void
dispatch_source_set_timer(dispatch_source_t source,
dispatch_time_t start,
uint64_t interval,
uint64_t leeway)
參數(shù):
- source: 定時器
- start: 開始時間, 當我們使用 dispatch_time 或者 DISPATCH_TIME_NOW 時,系統(tǒng)會使用默認時鐘來進行計時。然而當系統(tǒng)休眠的時候,默認時鐘是不走的,也就會導致計時器停止。使用 dispatch_walltime 可以讓計時器按照真實時間間隔進行計時;
- interval: 間隔(如果設置為 DISPATCH_TIME_FOREVER 則只執(zhí)行一次)
- leeway: 允許的誤差范圍; 計時不可能是百分百精確的, 即使設置為0, 也不是百分百精確的, 所以可以設置合理的允許誤差, 單位: 納秒(NSEC_PER_SEC)
相關內(nèi)容, 可參考文章: Dispatch Source Timer 的使用以及注意事項
// 重復執(zhí)行的定時器
- (void)gcdTimer1 {
// 獲取全局隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 創(chuàng)建定時器
dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 開始時間
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
// dispatch_time_t start = dispatch_walltime(NULL, 0);
// 重復間隔
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
// 設置定時器
dispatch_source_set_timer(_timer, start, interval, 0);
// 設置需要執(zhí)行的事件
dispatch_source_set_event_handler(_timer, ^{
//在這里執(zhí)行事件
static NSInteger num = 0;
NSLog(@"%ld", (long)num);
num++;
if (num > 4) {
NSLog(@"end");
// 關閉定時器
dispatch_source_cancel(_timer);
}
});
// 開啟定時器
dispatch_resume(_timer);
NSLog(@"start");
}
輸出:
2016-12-30 10:15:01.114 定時器[3393:99474] start
2016-12-30 10:15:02.187 定時器[3393:99796] 0
2016-12-30 10:15:03.114 定時器[3393:99796] 1
2016-12-30 10:15:04.186 定時器[3393:99796] 2
2016-12-30 10:15:05.188 定時器[3393:99796] 3
2016-12-30 10:15:06.188 定時器[3393:99796] 4
2016-12-30 10:15:06.188 定時器[3393:99796] end
這里的開始時間設置了1s的間隔, 所以1s之后才開始執(zhí)行,可以設置使用DISPATCH_TIME_NOW來立馬執(zhí)行;
注意:
這里的開始時間(start)可以使用下面的方式的來設置:
dispatch_time_t start = dispatch_walltime(NULL, 0);
或者直接設置為: DISPATCH_TIME_NOW
關于dispatch_walltime 和dispatch_time 的區(qū)別, 上面也有提及,也可參考stackOverflow上的這個回答; 主要區(qū)別就是前者在系統(tǒng)休眠時還會繼續(xù)計時, 而后者在系統(tǒng)休眠時就停止計時, 待系統(tǒng)重新激活時, 接著繼續(xù)計時;
停止計時器:
停止GCD定時器的方式, Dispatch Source Timer 的使用以及注意事項中有提及, 主要有以下兩種:
// 關閉定時器
// 完全銷毀定時器, 重新開啟的話需要重新創(chuàng)建
// 全局變量, 關閉后需要置為nil
dispatch_source_cancel(_timer);
// 暫停定時器
// 可使用dispatch_resume(_timer)再次開啟
// 全局變量, 暫停后不能置為nil, 否則不能重新開啟
dispatch_suspend(_timer);
三. CADisplayLink
CADisplayLink默認每秒運行60次,通過它的** frameInterval** 屬性改變每秒運行幀數(shù),如設置為2,意味CADisplayLink每隔一幀運行一次,有效的邏輯每秒運行30次
屏幕刷新時調用:CADisplayLink是一個能讓我們以和屏幕刷新率同步的頻率將特定的內(nèi)容畫到屏幕上的定時器類。CADisplayLink以特定模式注冊到runloop后,每當屏幕顯示內(nèi)容刷新結束的時候,runloop就會向CADisplayLink指定的target發(fā)送一次指定的selector消息, CADisplayLink類對應的selector就會被調用一次。所以通常情況下,按照iOS設備屏幕的刷新率60次/秒
延遲:iOS設備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會在每次刷新結束都被調用,精確度相當高。但如果調用的方法比較耗時,超過了屏幕刷新周期,就會導致跳過若干次回調調用機會。
如果CPU過于繁忙,無法保證屏幕60次/秒的刷新率,就會導致跳過若干次調用回調方法的機會,跳過次數(shù)取決CPU的忙碌程度。
使用場景:從原理上可以看出,CADisplayLink適合做界面的不停重繪,比如視頻播放的時候需要不停地獲取下一幀用于界面渲染。
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel
參數(shù):
- target: 調用者
- sel: 執(zhí)行的方法
示例:
- (void) displayLink {
CADisplayLink *display = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayRun:)];
// 大概1s執(zhí)行一次
// 取值范圍 1--100, 值越大, 頻率越高
display.preferredFramesPerSecond = 2;
[display addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)displayRun:(CADisplayLink *)link {
static NSInteger num = 0;
NSLog(@"%ld", (long)num);
num++;
if (num > 4) {
[link invalidate];
NSLog(@"end");
}
}
這里的示例不太恰當, 不應該在這種場合使用,
另外, 我們可以使用他的 paused 屬性, 來使其暫停, 或繼續(xù):
// 暫停
display.paused = YES;
// 繼續(xù)
display.paused = NO;