最近一個(gè)做電商類APP的朋友說哪雕,限時(shí)優(yōu)惠頁面 UITableView 上的 Cell 全部都需要倒計(jì)時(shí)钾麸,然后他的預(yù)想做法呢更振,就是每個(gè)一個(gè) Timer 。喂走。殃饿。聽了好震驚呀,因?yàn)樗麄?APP 除了這里很多地方也需要倒計(jì)時(shí)芋肠,可是卻每個(gè)地方都一個(gè) Timer 乎芳。。帖池。 遂建議封裝個(gè)定時(shí)中心LYTimerHelper奈惑,主要的點(diǎn)其實(shí)就如下:
- 間隔
Inteval
一致的Timer
同時(shí)間只存在一個(gè)(其實(shí)根據(jù)業(yè)務(wù)一般也是 1s 倒計(jì)時(shí),所以大部分并不會(huì)創(chuàng)建太多個(gè)Timer
) - 通過
key
將執(zhí)行的操作通過block
加入對(duì)應(yīng)的Timer
睡汹,也可通過key
移除 - 每次
Timer
喚醒執(zhí)行所有的block后肴甸,都進(jìn)行判斷是否還存在需要執(zhí)行的block
,若是不存在則這個(gè)Timer
銷毀囚巴,若是存在則繼續(xù)等待下一次喚醒
所以限時(shí)優(yōu)惠頁面就很簡單了原在,使用 MVVM+RAC 輕松的就能完成,只要將頁面中的商品 model 倒計(jì)時(shí)操作加入定時(shí)中心彤叉,根據(jù)對(duì)應(yīng) model 的倒計(jì)時(shí)變化刷新對(duì)應(yīng) cell 的時(shí)間顯示即可庶柿,別的地方亦是如此
很簡單吧,那就順便聊一聊 iOS 里的定時(shí)有哪些
-
NSTimer
NSTimer
是最為常見的一種定時(shí)方式秽浇,根據(jù)文檔可以知道它的使用方式也比較簡單浮庐,文檔也表明 NSTimer
是通過添加到RunLoop中被觸發(fā)進(jìn)行工作的,橋接 CFRunLoopTimerRef
Timers work in conjunction with run loops. Run loops maintain strong references to their timers
self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
// 或者
self.timer = [NSTimer timerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
// 或者 延遲30s開始
NSTimeInterval timeInterval = [self timeIntervalSinceReferenceDate] + 30;
NSDate *newDate = [NSDate dateWithTimeIntervalSinceReferenceDate:timeInterval];
self.timer = [[NSTimer alloc] initWithFireDate:newDate interval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
醬紫其實(shí)就可以進(jìn)行定時(shí)(iOS10 提供了更加便利的 block 方法)
RunLoop 影響
上面的代碼在使用過程中柬焕,會(huì)發(fā)現(xiàn)當(dāng) UITableView
在滾動(dòng)過程中审残,定時(shí)器到了時(shí)間并未觸發(fā)
原因
A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified. Each time you run your run loop, you specify (either explicitly or implicitly) a particular “mode” in which to run. During that pass of the run loop, only sources associated with that mode are monitored and allowed to deliver their events. (Similarly, only observers associated with that mode are notified of the run loop’s progress.) Sources associated with other modes hold on to any new events until subsequent passes through the loop in the appropriate mode
說的就是同一時(shí)候 RunLoop 只運(yùn)行在一種 Mode
上,并且只有這個(gè)Mode
相關(guān)聯(lián)的源或定時(shí)器會(huì)被傳遞消息斑举,更多內(nèi)容可以查閱RunLoop
mainRunLoop
一般處于 NSDefaultRunLoopMode
搅轿,但是在滾動(dòng)或者點(diǎn)擊事件等觸發(fā)時(shí),mainRunLoop
切換至 NSEventTrackingRunLoopMode
富玷,而上面 timer
被加入的正是NSDefaultRunLoopMode
(未指明也默認(rèn)加入默認(rèn)模式)璧坟,所以滑動(dòng)時(shí)未觸發(fā)定時(shí)操作没宾。
解決
添加timer
到mainRunLoop
的NSRunLoopCommonMode
中或者子線程中,而這里加入子線程中需要注意的手動(dòng)開啟并運(yùn)行子線程的RunLoop
self.timer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(timerRuning) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonMode];
NSRunLoopCommonMode
這是一組可配置的常用模式沸柔。將輸入源與此模式相關(guān)聯(lián)也會(huì)將其與組中的每個(gè)模式相關(guān)聯(lián)。對(duì)于Cocoa應(yīng)用程序铲敛,此集合默認(rèn)包括NSDefaultRunLoopMode
褐澎,NSPanelRunLoopMode
和NSEventTrackingRunLoopMode
其它需要注意的點(diǎn)
-
Nonrepeating Timer
運(yùn)行完自行無效,以防再次觸發(fā)伐蒋,但Repeating Timer
需要手動(dòng)調(diào)用
[self.timer invalidate];
self.timer = nil;
NSTimer
在運(yùn)行期間會(huì)對(duì)target
進(jìn)行持有工三,所以切記在退出前invalidate
,否則內(nèi)存泄漏了(別再target
的dealloc里調(diào)用先鱼,噗噗)NSTimer
非實(shí)時(shí)性的
a. 一個(gè)是可能因?yàn)楫?dāng)前RunLoop
運(yùn)行的Mode
不監(jiān)聽導(dǎo)致未能觸發(fā)
b. 一個(gè)可能當(dāng)前RunLoop
做了耗時(shí)的工作俭正,使得持續(xù)時(shí)間超過了一個(gè)或若干個(gè)NSTimer
的觸發(fā)時(shí)間,NSTimer
不會(huì)進(jìn)行補(bǔ)償操作焙畔,只是此次循環(huán)檢查定時(shí)器時(shí)觸發(fā)NSTimer
掸读,但是不影響后面其它的觸發(fā)時(shí)間,因?yàn)?code>NSTimer根據(jù)一開始計(jì)劃的時(shí)間來觸發(fā)宏多,并不根據(jù)每次被觸發(fā)的實(shí)際時(shí)間點(diǎn)來計(jì)算下一次的觸發(fā)時(shí)間
c.fire
方法立即觸發(fā)時(shí)間儿惫,對(duì)于Nonrepeating Timer
運(yùn)行完自行無效,但是跟 b 點(diǎn)一樣伸但,不影響Repeating Timer
的預(yù)定周期性觸發(fā)
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fired ^_^");
}];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"a lot of work begin ^_^");
NSMutableString *countStr = [[NSMutableString alloc] init];
for (NSUInteger i = 0; i < 9999999; i++) {
[countStr appendFormat:@"i_ = %ld ", i];
}
NSLog(@"a lot of work end ^_^");
});
如 b 點(diǎn)所說肾请,大量工作耗費(fèi)較長時(shí)間,這中間并未觸發(fā)定時(shí)更胖,同時(shí)工作完成在此次循環(huán)檢查了定時(shí)器觸發(fā)了定時(shí) D铛铁,而 E 的時(shí)間點(diǎn)也依然是按一開始的時(shí)間之后 N * 1s的預(yù)計(jì)觸發(fā),而不是在 D 之后 1s
-
CADisplayLink
CADisplayLink
是基于屏幕刷新周期的定時(shí)器却妨,一般 60 frames/s饵逐,同樣也是基于RunLoop
,因此也會(huì)碰到因?yàn)檫\(yùn)行在非定時(shí)觸發(fā)的Mode
或者工作耗時(shí)導(dǎo)致的延遲(這點(diǎn)跟NSTimer
一樣)管呵,這里需要注意的是回調(diào)工作若是當(dāng)前線程大量計(jì)算梳毙,也會(huì)導(dǎo)致下一次的延遲,掉幀卡頓發(fā)生
使用也比較簡單捐下,文檔說的比較清楚
// 1.初始化
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLink:)];
// 2. 設(shè)置 - 2楨回調(diào)一次账锹,這里非時(shí)間,而是以楨為單位
self.displayLink.frameInterval = 2; //iOS10之前
self.displayLink.preferredFramesPerSecond = 30; //iOS10及之后
// 3.加入RunLoop
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
//
// 4.callback
- (void)displayLink:(CADisplayLink *)displayLink {
... ...
// 5.時(shí)間累計(jì):每次回調(diào)的間隔時(shí)間
self.accumulator += displayLink.duration * displayLink.frameInterval; //粗略計(jì)算坷襟,因?yàn)榭赡芘龅酱罅抗ぷ鞯葘?dǎo)致間隔時(shí)間bu zhu
// 或者??
if (self.timestamp == 0) {
self.timestamp = displayLink.timestamp;
}
CFTimeInterval now = displayLink.timestamp; // 獲取當(dāng)前的時(shí)間戳
self.accumulator += now - self.timestamp;
self.timestamp = now;
// 6.預(yù)計(jì)下一次時(shí)間
NSTimeInterval next = displayLink.targetTimestamp; //iOS10及之后
}
... ...
// 7.暫停
self.displayLink.paused = YES;
// 8.銷毀
[self.displayLink invalidate];
self.displayLink = nil;
CADisplayLink
因?yàn)橥狡聊凰⑿骂l率奸柬,屏幕刷新后立即回調(diào),因此很適合跟 UI 相關(guān)的定時(shí)繪制操作婴程,像進(jìn)度條廓奕、FPS等等,這樣就無須進(jìn)行多余運(yùn)算
同樣 CADisplayLink
會(huì)對(duì) target
持有,所以記得進(jìn)行釋放桌粉,以免造成內(nèi)存泄露
-
GCD
Grand Central Dispatch
簡稱 GCD
蒸绩,一套低層API,提供了簡單易用并無需直接操作線程居于隊(duì)列的并發(fā)編程铃肯,詳情查閱
GCD
功能非常強(qiáng)大患亿,今天只涉及 Dispatch Sources
中的定時(shí)器
Dispatch Sources
替換了處理系統(tǒng)相關(guān)事件的異步回調(diào)函數(shù)。配置一個(gè)dispatch source
押逼,需要指定要監(jiān)測(cè)的事件(DISPATCH_SOURCE_TYPE_TIMER
等)步藕、dispatch queue
、以及處理事件的代碼(block
或C函數(shù)
)挑格。當(dāng)事件發(fā)生時(shí)咙冗,dispatch source
會(huì)提交對(duì)應(yīng)的block
或C函數(shù)
到指定的dispatch queue
執(zhí)行
Dispatch Sources
監(jiān)聽系統(tǒng)內(nèi)核對(duì)象并處理,通過系統(tǒng)級(jí)調(diào)用漂彤,會(huì)比NSTimer
更精準(zhǔn)一些
// 1. 創(chuàng)建 dispatch source雾消,并指定檢測(cè)事件為定時(shí)
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue("LY_Timer_Queue", 0));
// 2. 設(shè)置定時(shí)器啟動(dòng)時(shí)間,間隔显歧,容差
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
// 3. 設(shè)置callback
dispatch_source_set_event_handler(timer, ^{
NSLog(@"timer fired ^_^");
});
dispatch_source_set_event_handler(timer, ^{
//取消定時(shí)器時(shí)一些操作
});
// 4.啟動(dòng)定時(shí)器-剛創(chuàng)建的source處于被掛起狀態(tài)
dispatch_resume(timer);
// 5.暫停定時(shí)器
dispatch_suspend(timer);
// 6.取消定時(shí)器
dispatch_source_cancel(timer);
timer = nil;
覺得每次創(chuàng)建代碼太多仪或,封裝下即可
需要注意的點(diǎn)
與其他
dispatch objects
一樣,dispatch sources
也是引用計(jì)數(shù)數(shù)據(jù)類型士骤,在 ARC 中無需手動(dòng)調(diào)用dispatch_release(timer)
dispatch_suspend(timer)
將timer
掛起范删,若是此時(shí)已經(jīng)在執(zhí)行 block,繼續(xù)完成此次 block拷肌,并不會(huì)立即停止dispatch source
在掛起時(shí)到旦,直接設(shè)置為nil
或者 其它新源都會(huì)造成crash,需要在activate的狀態(tài)下調(diào)用dispatch_source_cancel(timer)
后再重新創(chuàng)建巨缘。-
dispatch_source_set_timer
中設(shè)置啟動(dòng)時(shí)間添忘,dispatch_time_t
可通過兩個(gè)方法生成:dispatch_time
和dispatch_walltime
a.dispatch_time
創(chuàng)建相對(duì)時(shí)間,基于mach_absolute_time
若锁,CPU的時(shí)鐘周期數(shù)ticks
搁骑,這個(gè)tricks
在每次手機(jī)重啟之后,會(huì)重新開始計(jì)數(shù)又固,而且iPhone鎖屏進(jìn)入休眠之后tick也會(huì)暫停計(jì)數(shù),mach_absolute_time()
不會(huì)受系統(tǒng)時(shí)間影響仲器,只受設(shè)備重啟和休眠行為影響。b.
dispatch_walltime
類似創(chuàng)建絕對(duì)時(shí)間仰冠,當(dāng)設(shè)備休眠時(shí)乏冀,時(shí)間依然再走所以這兩者的區(qū)別就是,當(dāng)設(shè)備休眠時(shí)洋只,
dispatch_time
停止運(yùn)行辆沦,dispatch_walltime
繼續(xù)運(yùn)行昼捍,所以如果一個(gè)事件處理是在30分鐘之后,運(yùn)行5分鐘后肢扯,設(shè)備休眠20分鐘妒茬,兩個(gè)時(shí)間對(duì)應(yīng)的事件觸發(fā)點(diǎn)如下:
- 若是想像
NSTimer
實(shí)現(xiàn)Nonrepeating Timer
,則使用dispatch_after
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
});
-
dispatch_source
可內(nèi)部持有也可外部持有蔚晨,內(nèi)部持有可在事件處理block
中進(jìn)行有條件取消
dispatch_source_set_event_handler(timer, ^{
NSLog(@"dis timer fired ^_^");
if (條件滿足) {
dispatch_source_cancel(timer);
}
});
可以通過
dispatch_set_target_queue(timer, queue)
更改事件處理的所在隊(duì)列郊闯,修改dispatch source
是異步操作,所以不會(huì)更改已經(jīng)在執(zhí)行的事件dispatch_resume
和dispatch_suspend
調(diào)用需一一對(duì)應(yīng)蛛株,重復(fù)調(diào)用dispatch_resume
會(huì)crash
-
高精度定時(shí)
以上的幾種定時(shí)都會(huì)受限于蘋果為了保護(hù)電池和提高性能采用的策略而導(dǎo)致有延遲,像NSTimer
會(huì)有50-100毫秒的誤差育拨,若的確需要使用更高精度的定時(shí)器(誤差小于0.5毫秒)谨履,一般在多媒體操作方面有所需要,蘋果官方同樣也提供了方法熬丧,閱讀高精度定時(shí)文檔
高精度定時(shí)用到的比較少笋粟,一般視頻或者音頻相關(guān)數(shù)據(jù)流操作中需要
其實(shí)現(xiàn)原理就是使定時(shí)器的線程優(yōu)先于系統(tǒng)上的其他線程,在無多線程沖突的情況下析蝴,這定時(shí)器的請(qǐng)求會(huì)被優(yōu)先處理害捕,所以不要?jiǎng)?chuàng)建大量的實(shí)時(shí)線程,一旦某個(gè)線程都要被優(yōu)先處理闷畸,結(jié)果就是實(shí)時(shí)線程都失敗了
所以現(xiàn)在實(shí)現(xiàn)高精度定時(shí)有兩種方法:
1.使用Mach Thread API把定時(shí)器所在的線程尝盼,移到高優(yōu)先級(jí)的線程調(diào)度類即the real time scheduling class
#include <mach/mach.h>
#include <mach/mach_time.h>
#include <pthread.h>
void move_pthread_to_realtime_scheduling_class(pthread_t pthread)
{
mach_timebase_info_data_t timebase_info;
mach_timebase_info(&timebase_info);
const uint64_t NANOS_PER_MSEC = 1000000ULL;
double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;
thread_time_constraint_policy_data_t policy;
policy.period = 0;
policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work
policy.constraint = (uint32_t)(10 * clock2abs);
policy.preemptible = FALSE;
int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),
THREAD_TIME_CONSTRAINT_POLICY,
(thread_policy_t)&policy,
THREAD_TIME_CONSTRAINT_POLICY_COUNT);
if (kr != KERN_SUCCESS) {
mach_error("thread_policy_set:", kr);
exit(1);
}
}
2.使用更精確的計(jì)時(shí)API mach_wait_until()
,如下代碼使用mach_wait_until()
等待10秒
#include <mach/mach.h>
#include <mach/mach_time.h>
static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;
static mach_timebase_info_data_t timebase_info;
static uint64_t abs_to_nanos(uint64_t abs) {
return abs * timebase_info.numer / timebase_info.denom;
}
static uint64_t nanos_to_abs(uint64_t nanos) {
return nanos * timebase_info.denom / timebase_info.numer;
}
void example_mach_wait_until(int argc, const char * argv[])
{
mach_timebase_info(&timebase_info);
uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);
uint64_t now = mach_absolute_time();
mach_wait_until(now + time_to_wait);
}
以上的所有定時(shí)器佑菩,雖然提供方法各不相同盾沫,但它們的內(nèi)核代碼是相同的
-
NSTimer
平時(shí)用的比較多,一般也足夠殿漠,需要注意的就是加入的RunLoop
的Mode
或者若是子線程赴精,需要手動(dòng)啟動(dòng)并運(yùn)行RunLoop
;同時(shí)注意使用invalidate
手動(dòng)停止定時(shí)绞幌,否則引起內(nèi)存泄漏蕾哟; - 需與顯示更新同步的定時(shí),建議
CADisplayLink
莲蜘,可以省去多余計(jì)算 -
GCD
定時(shí)精度較NSTimer
高谭确,使用時(shí)只需將任務(wù)提交給相應(yīng)隊(duì)列即可,對(duì)文件資源等定期讀寫操作很方便菇夸,使用時(shí)需要??dispatch_resume
與dispatch_suspend
配套琼富,同時(shí)要給dispatch source
設(shè)置新值或者置nil,需先dispatch_source_cancel(timer)
后 - 高精度定時(shí),使用較少庄新,一般多媒體視頻流/音頻流處理相關(guān)需要
好了鞠眉,就寫到這里吧薯鼠,反正吃飯時(shí)間到了。械蹋。出皇。就醬紫