知识点
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,然后利用消息转发,将消息传递回 我们的业务类中
转化成代码就是:
//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);