iOS NSTimer 详解(runloop,timer销毁方式)

知识点

1、 基本使用

2、 runloop关系

3、 Timer销毁方式

关于timer的调用分为两种

  • 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.
凡是以第一种方式调用的,你需要一个runloop,才能让他正常使用。

插播:关于fire、fireDate

fire 和 fireDate 的作用基本一致,都是用来开始执行timer的,即便我们不主动调用,当timer达到要求时 即时间间隔为timerWithTimeInterval设置的值时,timer也会执行。唯一区别就是 firDate 可以指定 timer 在什么时候开始执行,而 fire 是立即执行,不设置的话就是timerWithTimeInterval后开始执行。我们可以把 fire 理解为 performSelect ,把 fireDate 理解为 performSelector afterDelay。当然,只能是当成,而不是真正意义上的“是” ,因为还涉及到了 repeat 的问题。
还有一点很重要,fire 和 fireDate 他执行的 timer action (selector 参数),代表了 timer 的一次真正意义上的执行。什么意思呢,就是说,如果repeats=NO,并且TimeInterval>0,那么执行 fire 和不执行fire,timer action 都仅仅只会执行一次,区别在于执行的时间点不一样。比如说 TimeInterval = 3 ,我调用fire了,会立即执行 timer action ,但是3秒后,并不会执行下一次,设置的TimeInterval就失去了意义。如果不调用fire,那么会在3秒后调用一次 timer action

下面一个一个方法进行分析:

一、block 回调方式 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 执行 %ld 次",timerCount);
    }];
    [timer fire];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

可以发现,timer 没有指定target,也就是说,timer 并没有强持有self。根据这个原因我们可以认定,block timer 并不会影响 controller 的生命周期。
验证:执行上述代码,查看结果

2020-07-16 11:08:32.080114+0800 BSFrameworks_Example[94630:14248608] timer block 执行 1 次
2020-07-16 11:08:33.080461+0800 BSFrameworks_Example[94630:14248608] timer block 执行 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 执行 3 次
2020-07-16 11:08:35.080898+0800 BSFrameworks_Example[94630:14248608] timer block 执行 4 次
2020-07-16 11:08:36.080533+0800 BSFrameworks_Example[94630:14248608] timer block 执行 5 次

结果显示,正如我们猜想那样,controller 在timer没销毁前释放了。但是有趣的是controller释放后,timer依然继续执行,这是为什么呢?我猜可能是因为系统要循环执行timer的selector,但是因为没有指定target,所以他把timer放在了系统全局的一个地方,以便timer的继续执行(纯个人猜测)

二、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 是什么去看下消息转发就清楚了。这种形式的timer完全可以理解为消息转发。(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];

关于 timer 的销毁

对于第二种和第三种 timer 的使用方法,他们都会指定target,在timer没有销毁前,target 是不会释放的。

既然timer的释放会影响到target的释放,那么我们肯定要优先处理timer的销毁。一般情况下 timer 的销毁我们都会在某条件下,使用如下的方式对timer进行销毁

[self.timer invalidate];
self.timer = nil;

timer销毁后,如果target将要销毁,那么target就会执行dealloc方法,也就证明了 target 销毁了。

利用消息转发,解决timer 强持self的问题

利用系统的消息转发机制,我们可以通过建立一个中间对象作为target,然后利用消息转发,将消息传递回 我们的业务类中

WechatIMG16462.png

转化成代码就是:

//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
//业务类.m

//==============================
// 属性
//==============================
/// 计时器
@property (nonatomic ,strong) NSTimer *timer;

/// 用于 解决 timer 强引用 self 的问题
@property (nonatomic ,strong) TimerTarget *timerTarget;



//==============================
// 方法
//==============================
#pragma mark - 生命周期
-(void)dealloc{
    
    NSLog(@"BSLooperView 释放");
    
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

/// 创建timer
-(void)creatTimer{
    
    [self.timer invalidate];
    self.timer = nil;
    
    if (!self.timer) {
        if (self.duration<0.5) {
            self.duration = 3;
        }
        
        /**
         * 本来要加将timer 加入 runloop中(子线程加入,启动runloppe)
         * 加入后,发现无法停止timer,暂时未找到解决方案
         * 加runloop的好处就是,如果 滚动视图 的父视图 是ScrollView
         * 那么 ScrollView 的滚动 不影响timer的执行
         * 不加入runloop会造成 scrollview在滑动的时候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 强持self导致 self 无法调用 dealloc 的问题,然后我们在 dealloc 内销毁 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

意思就是说他会在当前runloop的default mode 中 执行timer

所以我们使用的时候只需要一行代码

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

并不需要把 timer 加入到 runloop 中,因为 scheduled 的作用就是把 timer 加入到runloop中。
下面我们把 timer 放在子线程中去执行,看看啥效果

-(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];
    });
}

结果

2020-07-16 14:48:01.283390+0800 BSFrameworks_Example[95489:14357750] timer 执行

为什么 repeats = YES ,他就执行了一次呢 ?执行的这一次明显是 [self.timer fire]的作用。scheduledTimerWithTimeInterval 不是已经加入了 runloop了吗,为什么没有执行?其实很简单:对于runloop,在主线程中,系统已经帮我们开起了runloop了,但是对于子线程,是需要我们自己主动去启动runloop的,所以想要timer 正常执行还需要启动下 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 ,我们是不能主动创建Runloop的,在调用 [NSRunLoop currentRunLoop] 的时候,如果 runloop 没有,系统会自动帮我们创建,如果有,就会直接把存在的 runloop 给我们, 类似于懒加载。Runloop 与 线程 是一对一的,一个线程最多只能对应一个 Runloop 。


timer 延迟性

timer实际触发事件的时间,精度并没有那么准确,如果当前RunLoop正在执行一个复杂的连续性的运算,timer很可能会延时触发。目前苹果还为 timer 增加了 tolerance 属性,代表对 timer 误差的容忍度

CADisplayLink

相比timer来说, CADisplayLink 更加的精准

A timer object that allows your application to synchronize its drawing to the refresh rate of the display.

谷歌翻译:CADisplayLink是一个定时器,他允许您的应用程序用固定的刷新率将其图形同步绘制并展示

CADisplayLink以我们指定的模式添加到RunLoop中,通常情况下他会以60次/秒的刷新率来执行selector。对于iOS设备,他的刷新频率是固定的,但是并不是说他的刷新频率一定是一成不变的,因为他还会受到一些其他因素影响,如:CPU处于繁忙状态,并不能保证60次/s的刷新率。这样就会跳过一些次数的回调。
我们一般使用CADisplayLink用来检测屏幕是否卡顿,视频播放器的界面渲染等

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

//停止
- (void)invalidate;

用法很简单,创建时指定target和 selector然后加入到runloop中,没有 runloop 是无法使用的。销毁方法和 timer 类似

[self.link invalidate];
self.link = nil;
GCD timer

GCD timer的使用,苹果已经封装好了,直接调用即可,不需要管释放的问题

//单次 repeats = NO ,时间间隔1.0s
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC); 
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ //回调任务});

//循环 repeats = YES ,时间间隔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, ^{
    //取消回调
});
//启动定时器
dispatch_resume( timer);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343