iOS NSTimer 詳解(runloop,timer銷毀方式)

知識點(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ù)類中

WechatIMG16462.png

轉(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);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市九府,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌侄旬,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宣羊,死亡現(xiàn)場離奇詭異仇冯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)赞枕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門炕婶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來莱预,“玉大人,你說我怎么就攤上這事依沮。” “怎么了宋渔?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵辜限,是天一觀的道長。 經(jīng)常有香客問我氧急,道長毫深,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任钉寝,我火速辦了婚禮闸迷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘稿黍。我一直安慰自己,他們只是感情好言沐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布酣栈。 她就那樣靜靜地躺著,像睡著了一般起便。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妙痹,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天鼻疮,我揣著相機(jī)與錄音,去河邊找鬼判沟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛吧秕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播砸彬,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼疗涉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绽淘?” 一聲冷哼從身側(cè)響起闹伪,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎杀怠,沒想到半個(gè)月后厅克,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡硕旗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年女责,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墙基。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡软族,死狀恐怖立砸,靈堂內(nèi)的尸體忽然破棺而出痘拆,到底是詐尸還是另有隱情氮墨,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布规揪,位于F島的核電站,受9級特大地震影響字支,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜堕伪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一栗菜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧富俄,春花似錦、人聲如沸霍比。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浅妆。三九已至,卻和暖如春狂打,著一層夾襖步出監(jiān)牢的瞬間混弥,已是汗流浹背对省。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工晾捏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人劳秋。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓胖齐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親呀伙。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345