本文約100行代碼秽荞,讀完大概用時5-10分鐘,理解的話看個人知識掌握程度抚官。
App在開發(fā)的過程中扬跋,經常會遇到倒計時等等與時間計算有關的需求,這時就需要我們去使用定時器了凌节,本篇我們就來盤點盤點iOS中的三大定時器:NSTimer钦听、dispatch_source_t和CADisplayLink。
一倍奢、NSTimer
1.NSTimer的介紹
NSTimer應該是新手最耳熟能詳?shù)亩〞r器了朴上,通過Apple開發(fā)文檔的描述 A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.
我們可以看到它是通過間隔一定的時間,向目標對象發(fā)送指定的消息(OC中調用方法在底層就是發(fā)送消息)來實現(xiàn)定時器的功能的卒煞。NSTimer在使用的過程中其實是有很多小細節(jié)需要注意的痪宰,下面都會講到。
2.NSTimer的方法
NSTimer有3個timerWith類方法(初始化):
/// @param ti 定時器的時間間隔
/// @param invocation 方法調用
/// @param yesOrNo 是否重復執(zhí)行
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
/// @param ti 定時器的時間間隔
/// @param aTarget 目標對象(一般是self)
/// @param aSelector 方法調用
/// @param yesOrNo 是否重復執(zhí)行
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/// @param interval 定時器的時間間隔
/// @param repeats 是否重復執(zhí)行
/// @param block 方法調用(代碼塊的形式)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
這3種類方法需要你手動將timer對象添加到runloop中畔裕;
// 在主runloop上添加 [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
和3個scheduledTimer類方法(初始化):
/// @param ti 定時器的時間間隔
/// @param invocation 方法調用
/// @param yesOrNo 是否重復執(zhí)行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
/// @param ti 定時器的時間間隔
/// @param aTarget 目標對象(一般是self)
/// @param aSelector 方法調用
/// @param yesOrNo 是否重復執(zhí)行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
/// @param interval 定時器的時間間隔
/// @param repeats 是否重復執(zhí)行
/// @param block 方法調用(代碼塊的形式)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
這3種類方法需要會自動將timer對象添加到當前runloop(默認是主runloop)中衣撬,并且mode為NSDefaultRunLoopMode;
以及2個initWith實例方法(初始化):
/// 需要手動將timer對象添加到runloop中
/// @param date 開始執(zhí)行的日期
/// @param interval 定時器的時間間隔
/// @param repeats 是否重復執(zhí)行
/// @param block 方法調用(代碼塊的形式)
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
/// 需要手動將timer對象添加到runloop中
/// @param date 開始執(zhí)行的日期
/// @param ti 定時器的時間間隔
/// @param t 目標對象(一般是self)
/// @param s 方法調用
/// @param ui 可自定義的參數(shù)
/// @param rep 是否重復執(zhí)行
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;
以上8個初始化方法扮饶,scheduledTimer
方法會自動添加timer到當前runloop具练,其他則不會
使用timerWith
和initWith
時需要手動添加timer到runloop
2個常用的實例方法:
// 開始執(zhí)行
- (void)fire;
// 銷毀
- (void)invalidate;
fire
會立即調用方法,在執(zhí)行完后甜无,如果不是重復的timer扛点,會立即 invalidate ;
invalidate
會停止重復的timer(不重復的執(zhí)行一次后會自動invalidate)岂丘,并將其從runloop中remove陵究。
3.NSTimer的使用示例
3.1 在一個普通的VC中使用:
@property (nonatomic, strong) NSTimer *timer; //作為屬性一般使用strong修飾,因為timer是一個對象元潘,需要被持有者強引用以防提前釋放
// 這里使用weakSelf來避免循環(huán)引用從而導致內存泄漏
__weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf changeLabelText];
}];
值得注意的是畔乙,當我們使用 target: selector:
的方式時,target后面使用weakSelf 并不能避免循環(huán)引用翩概,此時timer依然會對self進行強引用牲距,會導致內存泄漏,下面的代碼是錯誤的:
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(changeLabelText) userInfo:nil repeats:YES];
如果此時頁面上有scrollView或者tableView等在滑動時钥庇,需要手動更改timer的mode:
/* 當scrollView滾動的時候牍鞠,當前的 MainRunLoop 會處于 UITrackingRunLoopMode 的模式下,
在這個模式下评姨,是不會處理 NSDefaultRunLoopMode 的任務的 */
[[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
3.2 立即執(zhí)行timer的方法調用:
// 執(zhí)行fire方法后难述,會立即執(zhí)行本來需要時間間隔后才執(zhí)行的指定方法調用
// 對于重復的timer萤晴,它是一次額外的操作,并且不會打破正常的schedule
// 對于不重復的timer胁后,它一觸發(fā)完后店读,timer就被invalidate,就不管原來設定的時間間隔了
[_timer fire];
3.3 timer的釋放與銷毀:
if ([_timer isValid]) {
[_timer invalidate]; //對于不重復的timer可以不寫攀芯,因為不重復的timer在執(zhí)行完后就自動invalidate了
_timer = nil;
}
PS:看到很多文章說timer的銷毀不能放在dealloc
中屯断,要放在- (void)viewDidDisappear:(BOOL)animated
中,因為在dealloc中并不會去執(zhí)行侣诺。這種說法一般是不對的殖演,首先,dealloc中不會去執(zhí)行大概率是出現(xiàn)了循環(huán)引用年鸳,此時VC仍然被timer強引用趴久,導致VC沒法dealloc
,那么timer當然不會去執(zhí)行銷毀搔确;其次彼棍,viewDidDisappear
時去銷毀那么你在跳往下一級頁面而不是返回上一級頁面的時候,此時當前頁面一般是要繼續(xù)存在的妥箕,這么做就將當前頁面的timer銷毀了滥酥,肯定是不對的,正確的做法是使用上面timer的block API + weakSelf來避免循環(huán)使用畦幢。
4.NSTimer的更多使用技巧
4.1 暫停和啟動:
// 讓timer的fire時間為“遙遠的未來”,那么它就“暫屠虏酰”了
[_timer setFireDate:[NSDate distantFuture]];
// 讓timer的fire時間為“馬上”或“遠古”宇葱,那么它就啟動了
[self.timer setFireDate:[NSDate date]];
[self.timer setFireDate:[NSDate distantPast]];
4.2 非固定時間間隔執(zhí)行timer
- (void)randomTimeTimer {
// 此處先將timer的timeInterval設置為無窮大,這樣它便不會執(zhí)行
_timer = [NSTimer timerWithTimeInterval:MAXFLOAT target:self selector:@selector(randomTimeFireMethod) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer: _timer forMode:NSDefaultRunLoopMode];
}
- (void)randomTimeFireMethod {
static int timeExecute = 0;
// 這里的4是你想要timer執(zhí)行的次數(shù)
if (timeExecute < 4) {
// 不定長執(zhí)行
NSTimeInterval timeInterval = [self.randomTime[timeExecute] doubleValue];
timeExecute++;
// 使用fireDate來控制timer以達到不定長執(zhí)行
_timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:timeInterval];
}
}
OC中并沒有NSTimer的暫停刊头、啟動和非固定時間間隔的方法黍瞧,我們可以使用這種奇思妙想來達到這個目的。
4.3 在子線程中使用NSTimer
5.NSTimer的注意事項
5.1 為什么NSTimer的銷毀需要invalidate
+ =nil
原杂?
在OC中印颤,一般我們銷毀強引用,會直接使用 =nil
穿肄,但是NSTimer不可以年局。我們來看看ARC中的NSTimer創(chuàng)建到銷毀的過程中具體的引用關系變化:
-
VC創(chuàng)建NSTimer后,此時VC對timer強引用咸产,再之后timer加入到runloop后矢否,系統(tǒng)也會強引用timer
-
=nil
后,VC解除了對timer的強引用脑溢,但此時系統(tǒng)依然對timer有強引用
- 調用
invalidate
方法后僵朗,系統(tǒng)解除對timer的強引用
綜合以上,我們需要對timerinvalidate
+=nil
,才能真正的銷毀NSTimer验庙。
5.2 NSTimer不是實時的 / NSTimer可以設置Tolerance(容忍度)顶吮。
- NSTimer加入的runloop正好處在一個耗時的周期內;
- NSTimer添加的runloopMode不是當前runloop所處的mode時粪薛,如
NSDefaultRunLoopMode
的NSTimer在頁面滑動時暫停云矫; - Tolerance大概是避免NSTimer在runloop中的不實時帶來的時間偏移的(實際開發(fā)中使用較少,暫時沒怎么研究)汗菜。
二让禀、dispatch_source_t
1. dispatch_source_t的介紹
dispatch_source_t
是眾多DISPATCH_SOURCE
種類的一種
針對NSTimer受runloop的影響而不精準的問題,dispatch_source_t是一種相對精準的計時器陨界,并且它天生就可以使用GCD在子線程中執(zhí)行巡揍,解決NSTimer在主線程中導致卡頓的問題,但是它的缺點也比較明顯菌瘪,就是代碼量相對比較多一點腮敌。
2. dispatch_source_t的使用
定義屬性:
// 此處也用strong修飾,雖然沒有 * 俏扩,但是dispatch_source_t也是對象糜工,和普通的對象一樣,strong防止提前釋放
@property (nonatomic, strong) dispatch_source_t timer;
初始化和設定:
- (void)initTimer {
if (!_gcdTimer) {
// 創(chuàng)建隊列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 初始化timer(設定source_type录淡,以及隊列)
_gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設定timer的開始時間
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC));
// 如果timer的間隔時間比較大捌木,那么可以使用dispatch_walltime來創(chuàng)建start,可以避免誤差
dispatch_time_t start_0 = dispatch_walltime(0, 0);
// 設定timer的固定時間間隔
uint64_t interval = (uint64_t)(1 * NSEC_PER_SEC);
// 設置timer嫉戚,最后一個參數(shù)為leeway刨裆,是用來設置定時器的“期望精度值”,系統(tǒng)會根據這個值延遲或提前觸發(fā)定時器
dispatch_source_set_timer(_gcdTimer, start, interval, 0);
// 設定timer的方法調用
dispatch_source_set_event_handler(_gcdTimer, ^{
// 如果timer的方法調用是UI方面相關的操作彬檀,需要在主線程中執(zhí)行(線程間通信)
dispatch_async(dispatch_get_main_queue(), ^{
[self changeLabelText];
});
});
// 開啟定時器
dispatch_resume(_gcdTimer);
}
}
dispatch_source_create
方法參數(shù)詳細說明
- 第一個參數(shù):dispatch_source_type_t type為設置GCD源方法的類型帆啃,前面已經列舉過了。
- 第二個參數(shù):uintptr_t handle Apple的API介紹說窍帝,暫時沒有使用努潘,傳0即可。
- 第三個參數(shù):unsigned long mask Apple的API介紹說坤学,使用DISPATCH_TIMER_STRICT疯坤,會引起電量消耗加劇,畢竟要求精確時間拥峦,所以一般傳0即可贴膘,視業(yè)務情況而定。
- 第四個參數(shù):dispatch_queue_t _Nullable queue 隊列略号,將定時器事件處理的Block提交到哪個隊列,可以傳Null刑峡,默認為全局隊列洋闽。
開啟定時器:
dispatch_resume(_gcdTimer);
暫停定時器:
// 暫停
- (void)pauseTimer {
if (_gcdTimer) {
dispatch_suspend(_gcdTimer);
}
}
// 暫停后的重新開啟
dispatch_resume(_gcdTimer);
銷毀定時器:
dispatch_source_cancel(_gcdTimer);
_gcdTimer = nil;
3. dispatch_source_t的注意事項
timer被dispatch_suspend
后是不能釋放的,否則會引起崩潰突梦。因為OC中并沒有dispatch_source_t的暫停和開啟狀態(tài)的記錄诫舅,所以如果我們用到了它的暫停和開啟,則我們必須手動記錄宫患,有dispatch_suspend
則必有dispatch_resume
刊懈。
4. dispatch_source_t的優(yōu)缺點
優(yōu)點:
- 性能更好,相對更精確;
- 自帶暫停娃闲、繼續(xù);
- 天生適合在子線程中使用虚汛;
- 不需要加入到runloop中,也不需要管runloop的mode皇帮。
缺點:
- 每次
dispatch_resume
都會先執(zhí)行一次卷哩; - 本質上也不是完全精確;
- 代碼量較多属拾。
三将谊、CADisplayLink
1. CADisplayLink的介紹
CADisplayLink是OC中精度最高的定時器,它是根據設備的屏幕刷新頻率來執(zhí)行操作渐白,因此它的使用場景也相對當一尊浓,比較適合用來做UI的繪制、自定義的動畫引擎以及視頻播放的渲染纯衍。
2. CADisplayLink的方法和相關屬性
1個初始化類方法:
/// @param target 目標對象(一般是self)
/// @param sel 方法調用
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
3個實例方法
/// 將CADisplayLink對象添加到runloop中并指定mode
/// @param runloop 加入的runloop
/// @param mode 指定runloopMode
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
/// 將CADisplayLink對象從runloop指定的mode中移除
/// @param runloop 被移除CADisplayLink對象的runloop
/// @param mode 指定的runloopMode
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
// 將CADisplayLink對象從runloop所有mode中移除
- (void)invalidate;
兩個移除方法的區(qū)別
removeFromRunLoop
會將其從指定的runloop的指定mode中移除栋齿,此方法在runloop或者mode任一不匹配的情況下都無效,而且remove時需要進行判斷托酸,如果指定的mode中不存在褒颈,那么將會引起crash,原因是重復over-release
励堡;
invalidate
是從runloop的所有模式中移除,并取消和target的關聯(lián)關系堡掏,此方法可以多次調用应结,不會引起crash。
3個只讀屬性:
// 當前屏幕上顯示幀率的時間戳
@property(readonly, nonatomic) CFTimeInterval timestamp;
// 定時器的時間間隔
@property(readonly, nonatomic) CFTimeInterval duration;
// 客戶端針對其渲染的下一個時間戳
@property(readonly, nonatomic) CFTimeInterval targetTimestamp;
3個讀寫屬性:
// 是否暫停泉唁,設置了暫停后定時器將暫停鹅龄,直到設置為false的時候再執(zhí)行
@property(getter=isPaused, nonatomic) BOOL paused;
// 從iOS10開始已廢棄,不要去使用
@property(nonatomic) NSInteger frameInterval;
// 每秒刷新次數(shù)(幀率)
@property(nonatomic) NSInteger preferredFramesPerSecond;
3. CADisplayLink使用示例
// 創(chuàng)建
- (void)initDisplaylink {
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(changeLabelText)];
_displayLink.preferredFramesPerSecond = 0; //每秒刷新次數(shù)亭畜,設置為0時就是默認屏幕的最大刷新幀率
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
// 銷毀
[_displayLink invalidate];
_displayLink = nil;
4. CADisplayLink的特性
CADisplayLink
不能夠繼承-
修改幀率
CADisplayLink的實際幀率是由屏幕最大幀率(maximumFramesPerSecond
)和參數(shù)preferredFramesPerSecond
一起決定的扮休,規(guī)則為:如果屏幕最大幀率是60,實際幀率只能是15拴鸵、20玷坠、30蜗搔、60中的一種;如果設置大于60的值八堡,屏幕實際幀率為60樟凄。如果設置的是26~35之間的值,實際幀率是30兄渺;如果設置為0缝龄,會使用最高幀率。 - 在添加進runloop時應當選擇高優(yōu)先級的挂谍,以保證動畫的流暢
5. CADisplayLink防止循環(huán)引用
上面NSTimer在防止循環(huán)引用時使用了NSTimer本身提供的block方法而非傳入target的方式叔壤,但是CADisplayLink本身沒有提供block方法,只有傳入target的方式口叙,那么我們怎么避免循環(huán)引用呢炼绘?
首先我們來看一種錯誤的做法:
__weak typeof(self) weakSelf = self;
_displayLink = [CADisplayLink displayLinkWithTarget:weakSelf selector:@selector(changeLabelText:)];
_displayLink.preferredFramesPerSecond = 10;
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
然后再看另一種錯誤的做法:
// 將displayLink屬性聲明為weak
@property (nonatomic, weak) CADisplayLink *displayLink;
// 初始化
CADisplayLink *temp = [CADisplayLink displayLinkWithTarget:self selector:@selector(changeLabelText:)];
temp.preferredFramesPerSecond = 10;
[temp addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
_displayLink = temp;
以上兩種方法都是錯誤的,在頁面銷毀時庐扫,它們無一例外的定時器都沒有被銷毀饭望,依然在工作,原因在于此時runloop對定時器依然有強引用形庭。
此時正確的做法有兩種:
1.使用NSProxy
創(chuàng)建一個繼承自NSProxy
的新類GQProxy
// .h
@interface GQProxy : NSProxy
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
// .m
#import "GQProxy.h"
@implementation GQProxy
+ (instancetype)proxyWithTarget:(id)target {
GQProxy *proxy = [GQProxy alloc];
proxy.target = target;
return proxy;
}
//返回方法簽名
-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
-(void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
在初始化定時器時
_displayLink = [CADisplayLink displayLinkWithTarget:[GQProxy proxyWithTarget:self] selector:@selector(changeLabelText:)];
_displayLink.preferredFramesPerSecond = 10;
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
這樣就可以在避免循環(huán)引用了铅辞,推薦NSTimer也使用這種方法。
2.使用category擴展block方法
新建一個分類 CADisplayLink+GQTool
// .h
#import <QuartzCore/QuartzCore.h>
typedef void(^GQExecuteDisplayLinkBlock) (CADisplayLink *displayLink);
@interface CADisplayLink (GQTool)
@property (nonatomic,copy) GQExecuteDisplayLinkBlock executeBlock;
+ (CADisplayLink *)displayLinkWithExecuteBlock:(GQExecuteDisplayLinkBlock)block;
@end
// .m
#import "CADisplayLink+GQTool.h"
#import <objc/runtime.h>
@implementation CADisplayLink (GQTool)
- (void)setExecuteBlock:(GQExecuteDisplayLinkBlock)executeBlock{
objc_setAssociatedObject(self, @selector(executeBlock), [executeBlock copy], OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (GQExecuteDisplayLinkBlock)executeBlock{
return objc_getAssociatedObject(self, @selector(executeBlock));
}
+ (CADisplayLink *)displayLinkWithExecuteBlock:(GQExecuteDisplayLinkBlock)block{
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(gq_executeDisplayLink:)];
displayLink.executeBlock = [block copy];
return displayLink;
}
+ (void)gq_executeDisplayLink:(CADisplayLink *)displayLink{
if (displayLink.executeBlock) {
displayLink.executeBlock(displayLink);
}
}
@end
在初始化定時器時
__weak typeof(self) weakSelf = self;
_displayLink = [CADisplayLink displayLinkWithExecuteBlock:^(CADisplayLink * _Nonnull displayLink) {
[weakSelf changeLabelText];
}];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
這樣也可以避免定時器對VC的強引用萨醒,但本質上只是將定時器的target從控制器換成了定時器本身的類斟珊,還是存在循環(huán)引用,只不過對我們的系統(tǒng)沒有影響了富纸。所以推薦使用NSProxy這種方法囤踩。
以上就是關于iOS中三種定時器的詳細介紹,原創(chuàng)不易晓褪,如果您覺得這篇文章對您有用的話堵漱,就順手點個贊+關注吧。