知識點(diǎn)
1净蚤、 基本使用
2输硝、 runloop關(guān)系
3、 Timer銷毀方式
關(guān)于timer的調(diào)用分為兩種
-
timerWithTimeInterval
開頭 -
scheduledTimerWithTimeInterval
開頭
第一種里邊有三種方法点把,分別是
/// Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
/// - parameter: timeInterval The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)timerWithTimeInterval:(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;
蘋果給的備注寫的很清楚
Creates and returns a new NSTimer object initialized with the specified block object. This timer needs to be scheduled on a run loop (via -[NSRunLoop addTimer:]) before it will fire.
凡是以第一種方式調(diào)用的哥童,你需要一個(gè)runloop褒翰,才能讓他正常使用。
插播:關(guān)于fire错邦、fireDate
fire 和 fireDate 的作用基本一致型宙,都是用來開始執(zhí)行timer的伦吠,即便我們不主動(dòng)調(diào)用,當(dāng)timer達(dá)到要求時(shí) 即時(shí)間間隔為timerWithTimeInterval設(shè)置的值時(shí)搁嗓,timer也會執(zhí)行箱靴。唯一區(qū)別就是 firDate 可以指定 timer 在什么時(shí)候開始執(zhí)行,而 fire 是立即執(zhí)行棍矛,不設(shè)置的話就是timerWithTimeInterval后開始執(zhí)行抛杨。我們可以把 fire 理解為 performSelect ,把 fireDate 理解為 performSelector afterDelay。當(dāng)然茁帽,只能是當(dāng)成潘拨,而不是真正意義上的“是” ,因?yàn)檫€涉及到了 repeat 的問題璧亚。
還有一點(diǎn)很重要脂信,fire 和 fireDate 他執(zhí)行的 timer action (selector 參數(shù)),代表了 timer 的一次真正意義上的執(zhí)行疯搅。什么意思呢埋泵,就是說,如果repeats=NO礁蔗,并且TimeInterval>0,那么執(zhí)行 fire 和不執(zhí)行fire雁社,timer action 都僅僅只會執(zhí)行一次,區(qū)別在于執(zhí)行的時(shí)間點(diǎn)不一樣磺浙。比如說 TimeInterval = 3 徒坡,我調(diào)用fire了,會立即執(zhí)行 timer action 伦泥,但是3秒后锦溪,并不會執(zhí)行下一次,設(shè)置的TimeInterval就失去了意義跨新。如果不調(diào)用fire坏逢,那么會在3秒后調(diào)用一次 timer action
下面一個(gè)一個(gè)方法進(jìn)行分析:
一赘被、block 回調(diào)方式 timer
__block NSInteger timerCount = 0;
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
timerCount ++ ;
if (timerCount>=5) {
[timer invalidate];
timer = nil;
}
NSLog(@"timer block 執(zhí)行 %ld 次",timerCount);
}];
[timer fire];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
可以發(fā)現(xiàn)民假,timer
沒有指定target羊异,也就是說彤断,timer 并沒有強(qiáng)持有self。根據(jù)這個(gè)原因我們可以認(rèn)定平道,block timer 并不會影響 controller 的生命周期供炼。
驗(yàn)證:執(zhí)行上述代碼,查看結(jié)果
2020-07-16 11:08:32.080114+0800 BSFrameworks_Example[94630:14248608] timer block 執(zhí)行 1 次
2020-07-16 11:08:33.080461+0800 BSFrameworks_Example[94630:14248608] timer block 執(zhí)行 2 次
2020-07-16 11:08:33.600657+0800 BSFrameworks_Example[94630:14248608] BSStudyObjcController dealloc
2020-07-16 11:08:34.080959+0800 BSFrameworks_Example[94630:14248608] timer block 執(zhí)行 3 次
2020-07-16 11:08:35.080898+0800 BSFrameworks_Example[94630:14248608] timer block 執(zhí)行 4 次
2020-07-16 11:08:36.080533+0800 BSFrameworks_Example[94630:14248608] timer block 執(zhí)行 5 次
結(jié)果顯示冀墨,正如我們猜想那樣诽嘉,controller 在timer沒銷毀前釋放了疫蔓。但是有趣的是controller釋放后身冬,timer依然繼續(xù)執(zhí)行,這是為什么呢滚躯?我猜可能是因?yàn)橄到y(tǒng)要循環(huán)執(zhí)行timer的selector嘿歌,但是因?yàn)闆]有指定target,所以他把timer放在了系統(tǒng)全局的一個(gè)地方丧凤,以便timer的繼續(xù)執(zhí)行(純個(gè)人猜測)
二步脓、invocation timer
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
NSMethodSignature *signature = [self methodSignatureForSelector:@selector(timerAction)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = @selector(timerAction);
NSTimer *invocationTimer = [NSTimer timerWithTimeInterval:1 invocation:invocation repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:invocationTimer forMode:NSRunLoopCommonModes];
至于 invocation 是什么去看下消息轉(zhuǎn)發(fā)就清楚了浩螺。這種形式的timer完全可以理解為消息轉(zhuǎn)發(fā)要出。(invocation 是可以傳參的农渊,這里沒寫)
三、target selector timer
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSRunLoopCommonModes];
關(guān)于 timer 的銷毀
對于第二種和第三種 timer 的使用方法传于,他們都會指定target格了,在timer沒有銷毀前,target 是不會釋放的盛末。
既然timer的釋放會影響到target的釋放悄但,那么我們肯定要優(yōu)先處理timer的銷毀。一般情況下 timer 的銷毀我們都會在某條件下檐嚣,使用如下的方式對timer進(jìn)行銷毀
[self.timer invalidate];
self.timer = nil;
timer銷毀后嚎京,如果target將要銷毀隐解,那么target就會執(zhí)行dealloc方法,也就證明了 target 銷毀了煞茫。
利用消息轉(zhuǎn)發(fā),解決timer 強(qiáng)持self的問題
利用系統(tǒng)的消息轉(zhuǎn)發(fā)機(jī)制蚓曼,我們可以通過建立一個(gè)中間對象作為target纫版,然后利用消息轉(zhuǎn)發(fā),將消息傳遞回 我們的業(yè)務(wù)類中
轉(zhuǎn)化成代碼就是:
//TimerTarget.h文件
#pragma mark -
@interface TimerTarget : NSObject
@property (nonatomic ,weak) BSLooperView * target;
@end
//TimerTarget.m文件
@implementation TimerTarget
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
//業(yè)務(wù)類.m
//==============================
// 屬性
//==============================
/// 計(jì)時(shí)器
@property (nonatomic ,strong) NSTimer *timer;
/// 用于 解決 timer 強(qiáng)引用 self 的問題
@property (nonatomic ,strong) TimerTarget *timerTarget;
//==============================
// 方法
//==============================
#pragma mark - 生命周期
-(void)dealloc{
NSLog(@"BSLooperView 釋放");
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
}
/// 創(chuàng)建timer
-(void)creatTimer{
[self.timer invalidate];
self.timer = nil;
if (!self.timer) {
if (self.duration<0.5) {
self.duration = 3;
}
/**
* 本來要加將timer 加入 runloop中(子線程加入会涎,啟動(dòng)runloppe)
* 加入后瑞凑,發(fā)現(xiàn)無法停止timer,暫時(shí)未找到解決方案
* 加runloop的好處就是练慕,如果 滾動(dòng)視圖 的父視圖 是ScrollView
* 那么 ScrollView 的滾動(dòng) 不影響timer的執(zhí)行
* 不加入runloop會造成 scrollview在滑動(dòng)的時(shí)候timer 是暫停的(卡主)
*/
self.timerTarget = [[TimerTarget alloc]init];
self.timerTarget.target = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:self.duration target:self.timerTarget selector:@selector(looperTime) userInfo:nil repeats:YES];
}
}
這樣我們就解決了timer 強(qiáng)持self導(dǎo)致 self 無法調(diào)用 dealloc 的問題铃将,然后我們在 dealloc 內(nèi)銷毀 timer 即可
runloop 和 timer
首先說下 scheduledTimerWithTimeInterval
,在蘋果的api介紹里是這么描述的
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
/// - parameter: ti The number of seconds between firings of the timer. If seconds is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead
/// - parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
/// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
意思就是說他會在當(dāng)前runloop的default mode 中 執(zhí)行timer
所以我們使用的時(shí)候只需要一行代碼
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
并不需要把 timer 加入到 runloop 中,因?yàn)?scheduled 的作用就是把 timer 加入到runloop中劲阎。
下面我們把 timer 放在子線程中去執(zhí)行,看看啥效果
-(void)timerTest{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[self.timer fire];
});
}
結(jié)果
2020-07-16 14:48:01.283390+0800 BSFrameworks_Example[95489:14357750] timer 執(zhí)行
為什么 repeats = YES 悯仙,他就執(zhí)行了一次呢 锡垄?執(zhí)行的這一次明顯是 [self.timer fire]
的作用。scheduledTimerWithTimeInterval 不是已經(jīng)加入了 runloop了嗎货岭,為什么沒有執(zhí)行疾渴?其實(shí)很簡單:對于runloop,在主線程中搔谴,系統(tǒng)已經(jīng)幫我們開起了runloop了瞄沙,但是對于子線程慌核,是需要我們自己主動(dòng)去啟動(dòng)runloop的,所以想要timer 正常執(zhí)行還需要啟動(dòng)下 runloop
-(void)timerTest{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[self.timer fire];
[[NSRunLoop currentRunLoop]run];
});
}
順便說下 Runloop 垫桂,我們是不能主動(dòng)創(chuàng)建Runloop
的粟按,在調(diào)用 [NSRunLoop currentRunLoop]
的時(shí)候霹粥,如果 runloop 沒有后控,系統(tǒng)會自動(dòng)幫我們創(chuàng)建空镜,如果有,就會直接把存在的 runloop 給我們吴攒, 類似于懶加載。Runloop 與 線程 是一對一的署惯,一個(gè)線程最多只能對應(yīng)一個(gè) Runloop 镣隶。
timer 延遲性
timer實(shí)際觸發(fā)事件的時(shí)間,精度并沒有那么準(zhǔn)確怀酷,如果當(dāng)前RunLoop正在執(zhí)行一個(gè)復(fù)雜的連續(xù)性的運(yùn)算嗜闻,timer很可能會延時(shí)觸發(fā)。目前蘋果還為 timer 增加了 tolerance 屬性琉雳,代表對 timer 誤差的容忍度
CADisplayLink
相比timer來說, CADisplayLink 更加的精準(zhǔn)
A timer object that allows your application to synchronize its drawing to the refresh rate of the display.
谷歌翻譯:CADisplayLink是一個(gè)定時(shí)器檐束,他允許您的應(yīng)用程序用固定的刷新率將其圖形同步繪制并展示
CADisplayLink以我們指定的模式添加到RunLoop中束倍,通常情況下他會以60次/秒的刷新率來執(zhí)行selector。對于iOS設(shè)備甥桂,他的刷新頻率是固定的邮旷,但是并不是說他的刷新頻率一定是一成不變的,因?yàn)樗€會受到一些其他因素影響办陷,如:CPU處于繁忙狀態(tài),并不能保證60次/s的刷新率民镜。這樣就會跳過一些次數(shù)的回調(diào)。
我們一般使用CADisplayLink用來檢測屏幕是否卡頓植旧,視頻播放器的界面渲染等
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
//停止
- (void)invalidate;
用法很簡單离唐,創(chuàng)建時(shí)指定target和 selector然后加入到runloop中,沒有 runloop 是無法使用的。銷毀方法和 timer 類似
[self.link invalidate];
self.link = nil;
GCD timer
GCD timer的使用完沪,蘋果已經(jīng)封裝好了嵌戈,直接調(diào)用即可,不需要管釋放的問題
//單次 repeats = NO 熟呛,時(shí)間間隔1.0s
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ //回調(diào)任務(wù)});
//循環(huán) repeats = YES ,時(shí)間間隔2.0s
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 2.0 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(timer, ^{
if(指定條件){
dispatch_source_cancel(timer);
}
});
dispatch_source_set_cancel_handler(timer, ^{
//取消回調(diào)
});
//啟動(dòng)定時(shí)器
dispatch_resume( timer);