iOS定時器

本文列舉iOS的各種定時相關操作的使用方法,歡迎大家補充指教。主要包括:

  • NSTimer
  • GCD定時器
  • dispatch_after
  • (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

NSTimer

大名鼎鼎的NSTimer,之所以名氣大,一個是從名字上看它就叫定時器,另一個就是使用的坑太多,也常常被拿來作為面試題,介紹相關內容的文章也比較多。我在這拋磚引玉,歡迎大家補充哈。

使用方法:

NSTimer的初始化方式有幾下幾種。我們注意到分為invocation和selector兩種調用方式,其實這兩種區別不大,一般我們用selector方式較為方便。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(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;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

invocation方式
- (void)viewDidLoad {
    [super viewDidLoad];

    NSMethodSignature  *signature = [[self class] instanceMethodSignatureForSelector:@selector(Timered:)];
    NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = @selector(Timered:);
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 invocation:invocation repeats:YES];
}

- (void)Timered:(NSTimer*)timer {
    NSLog(@"timer called");
}

selector方式:
- (void)viewDidLoad {
    [super viewDidLoad];

    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];
}

- (void)Timered:(NSTimer*)timer {
    NSLog(@"timer called");
}

scheduledTimerWith和timerWith和區別

那每種方式的調用接口又分為scheduledTimerWith和timerWith是為什么呢?這是因為NSTimer是加到runloop中執行的。看scheduledTimerWith的函數說明,創建并安排到runloop的default mode中。

Creates a timer and schedules it on the current run loop in the default mode.

如果我們調用的是timerWith接口,就需要自己加入runloop。

You must add the new timer to a run loop, using addTimer:forMode:.

NSTimer *timer  =  [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(Timered) userInfo:nil repeats:YES];

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

一堆坑:

坑一:子線程啟動定時器問題:

我們都知道iOS是通過runloop作為消息循環機制,主線程默認啟動了runloop,可是子線程沒有默認的runloop,因此,我們在子線程啟動定時器是不生效的。

解決的方式也簡單,在子線程啟動一下runloop就可以了。

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer* timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    });

坑二:runloop的mode問題:

我們注意到schedule方式啟動的timer是add到runloop的NSDefaultRunLoopMode中,這就會出現其他mode時timer得不到調度的問題。最常見的問題就是在UITrackingRunLoopMode,即UIScrollView滑動過程中定時器失效。

解決方式就是把timer add到runloop的NSRunLoopCommonModes。UITrackingRunLoopMode和kCFRunLoopDefaultMode都被標記為了common模式,所以只需要將timer的模式設置為NSRunLoopCommonModes,就可以在默認模式和追蹤模式都能夠運行。

坑三:循環引用問題:

前兩個都是小坑,因為對于大部分簡單場景,是不會踩到的。但是循環引用問題,是每個使用者都會遇到的。

究其原因,就是NSTimer的target被強引用了,而通常target就是所在的控制器,他又強引用的timer,造成了循環引用。下面是target參數的說明:

target: The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.

在這里首先聲明一下:不是所有的NSTimer都會造成循環引用。就像不是所有的block都會造成循環引用一樣。以下兩種timer不會有循環引用:

  • 非repeat類型的。非repeat類型的timer不會強引用target,因此不會出現循環引用。

  • block類型的,新api。iOS 10之后才支持,因此對于還要支持老版本的app來說,這個API暫時無法使用。當然,block內部的循環引用也要避免。

    /// - 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 *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
    
    

二次聲明:不是解決了循環引用,target就可以釋放了,別忘了在持有timer的類dealloc的時候執行invalidate。

NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(Timered:) userInfo:nil repeats:YES];

如上面代碼,這個timer并沒有被self引用,那么為什么self不會被釋放呢?因為timer被加到了runloop中,timer又強引用了self,所以timer一直存在的話,self也不會釋放。

好了,說了一大堆廢話,下面說一下如何解決循環引用的問題。解決的主要方式就是打破timer對target的強引用。

方式一:來自《Effective Objective-C》第52條:別忘了NSTimer會保留其目標對象

先上代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak id weakSelf = self;
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer *timer) {
        NSLog(@"block %@",weakSelf);
    }];
}

@implementation NSTimer(BlockTimer)
+ (NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats blockTimer:(void (^)(NSTimer *))block{
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(timered:) userInfo:[block copy] repeats:repeats];
    return timer;
}

+ (void)timered:(NSTimer*)timer {
    void (^block)(NSTimer *timer)  = timer.userInfo;
    block(timer);
}
@end

解釋:將強引用的target變成了NSTimer的類對象。類對象本身是單例的,是不會釋放的,所以強引用也無所謂。執行的block通過userInfo傳遞給定時器的響應函數timered:。循環引用被打破的結果是:

  • timer的使用者強引用timer。
  • timer強引用NSTimer的類對象。
  • timer的使用者在block中通過weak的形式使用,因此是被timer弱引用。
方式二:NSProxy的方式

建立一個proxy類,讓timer強引用這個實例,這個類中對timer的使用者target采用弱引用的方式,再把需要執行的方法都轉發給timer的使用者。

@interface ProxyObject : NSProxy
@property (weak, nonatomic) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation ProxyObject

+ (instancetype)proxyWithTarget:(id)target {
    ProxyObject* proxy = [[self class] alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.target respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.target];
    }
}

@end

@implementation ProxyTimer
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:ti target:[ProxyObject proxyWithTarget: aTarget] selector:aSelector userInfo:userInfo repeats:yesOrNo];
    return timer;
}
@end

方式三:封裝timer,弱引用target

類似NSProxy的方式,建立一個橋接timer的實例,弱引用target,讓timer強引用這個實例。

@interface NormalTimer : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic) SEL selector;
@end

@implementation NormalTimer
- (void)dealloc{
    NSLog(@"timer dealloc");
}

- (void)timered:(NSTimer*)timer{
    [self.target performSelector:self.selector withObject:timer];
}
@end

@interface NSTimer(NormalTimer)
+ (NSTimer *)scheduledNormalTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
@end

@implementation NSTimer(NormalTimer)
+ (NSTimer *)scheduledNormalTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    NormalTimer* normalTimer = [[NormalTimer alloc] init];
    normalTimer.target = aTarget;
    normalTimer.selector = aSelector;
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:ti target:normalTimer selector:@selector(timered:) userInfo:userInfo repeats:yesOrNo];
    return timer;
}
@end

GCD定時器

GCD中的Dispatch Source其中的一種類型是DISPATCH_SOURCE_TYPE_TIMER,可以實現定時器的功能。注意的是需要把timer聲明為屬性,否則,由于這種timer并不是添加到runloop中的,直接就被釋放了。

GCD定時器的好處是,他并不是加入runloop執行的,因此子線程也可以使用。也不會引起循環引用的問題。

WS(weakSelf);
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
    [weakSelf commentAnimation];
});
dispatch_resume(timer);

dispatch_after

dispatch_after內部也是使用的Dispatch Source。因此也避免了NSTimer的很多坑。

static inline void
_dispatch_after(dispatch_time_t when, dispatch_queue_t queue,
        void *ctxt, void *handler, bool block)
{
    dispatch_timer_source_refs_t dt;
    dispatch_source_t ds;
    uint64_t leeway, delta;

    if (when == DISPATCH_TIME_FOREVER) {
#if DISPATCH_DEBUG
        DISPATCH_CLIENT_CRASH(0, "dispatch_after called with 'when' == infinity");
#endif
        return;
    }

    delta = _dispatch_timeout(when);
    if (delta == 0) {
        if (block) {
            return dispatch_async(queue, handler);
        }
        return dispatch_async_f(queue, ctxt, handler);
    }
    leeway = delta / 10; // <rdar://problem/13447496>

    if (leeway < NSEC_PER_MSEC) leeway = NSEC_PER_MSEC;
    if (leeway > 60 * NSEC_PER_SEC) leeway = 60 * NSEC_PER_SEC;

    // this function can and should be optimized to not use a dispatch source
    ds = dispatch_source_create(&_dispatch_source_type_after, 0, 0, queue);
    dt = ds->ds_timer_refs;

    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    if (block) {
        _dispatch_continuation_init(dc, ds, handler, 0, 0, 0);
    } else {
        _dispatch_continuation_init_f(dc, ds, ctxt, handler, 0, 0, 0);
    }
    // reference `ds` so that it doesn't show up as a leak
    dc->dc_data = ds;
    _dispatch_trace_continuation_push(ds->_as_dq, dc);
    os_atomic_store2o(dt, ds_handler[DS_EVENT_HANDLER], dc, relaxed);

    if ((int64_t)when < 0) {
        // wall clock
        when = (dispatch_time_t)-((int64_t)when);
    } else {
        // absolute clock
        dt->du_fflags |= DISPATCH_TIMER_CLOCK_MACH;
        leeway = _dispatch_time_nano2mach(leeway);
    }
    dt->dt_timer.target = when;
    dt->dt_timer.interval = UINT64_MAX;
    dt->dt_timer.deadline = when + leeway;
    dispatch_activate(ds);
}

  1. 計算delta = _dispatch_timeout(when);如果時間到直接dispatch_async執行。
  2. dispatch_source_create創建source,并賦值給dispatch_continuation_t,dispatch_continuation_t會控制執行異步操作,將后續的時間上的操作都賦值給source的ds_timer_refs,并激活這個source。
performSelector:after

這種方式通常是用于在延時后去處理一些操作,其內部也是基于將timer加到runloop中實現的。因此也存在NSTimer的關于子線程runloop的問題。

這種調用方式的好處是可以取消。

- (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;

延時一次操作的選擇:

幾種方式都是定時器,都可以實現延時操作。綜合相比:如果只是單獨一次的延時操作,NSTimer和GCD的定時器都顯得有些笨重。performSelector方式比較合適,但是又收到了子線程runloop的限制。因此,dispatch_after是最優的選擇。

延時的取消操作::

以上幾種方式都可以實現取消操作。

  • NSTimer可以通過invalidate來停止定時器。
  • GCD的定時器可以調用dispatch_suspend來掛起。
  • performSelector:after可以通過cancelPreviousPerformRequestsWithTarget取消。
  • dispatch_after可以通過dispatch_block_cancel來取消。
    self.delayStartBlock = dispatch_block_create(0, ^{
        NSLog(@"把我取消");
    });

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(),self.delayStartBlock);

    dispatch_block_cancel(self.delayStartBlock);

鏈接:http://www.lxweimin.com/p/ca579c502894

補充CADisplayLink

-(void)startTimer {

    [self invalidateTimer];
    self.displayTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(captureView:)];
    if (@available(iOS 10.0, *)) {
        self.displayTimer.preferredFramesPerSecond = desiredFrameRate;
    } else {
        self.displayTimer.frameInterval = displayLinkFrameRate / desiredFrameRate;
    }
    [self.displayTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    self.displayTimer.paused = [UIApplication sharedApplication].applicationState == UIApplicationStateBackground;
}
-(void)invalidateTimer{
    if(self.displayTimer){
        [self.displayTimer invalidate];
        self.displayTimer = nil;
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容