iOS开发 NSTimer定时器

前言

在iOS开发过程中,我们常用定时器分三种:NSTimer定时器、CADisplayLink定时器、GCD定时器。
本文只介绍NSTimer定时器。

NSTimer是什么

runLoop从字面的意思上看是运行中的循环,作用是保持APP的持续运行,处理APP的各种事件,比如点击事件、定时器事件、Selector事件,节省CPU资源,提高APP的性能,能够让线程在有工作的时候忙碌,在没有工作的时候休眠。

NSTimer是Foundation库提供的一个类,基于runloop实现。可以只执行一次,也可定期反复执行(设置repeat参数)。其中只执行一次时,执行后自动销毁。重复执行的,必须手动调用invalidate才能销毁。

NSTimer怎么使用

创建NSTimer

系统提供了8个创建方法:6个类创建方法,2个实例初始化方法。其中, scheduled的初始化方法将以NSDefaultRunLoopMode直接添加到当前的runloop中;不用scheduled方式初始化的,需要手动addTimer: forMode: 将timer添加到一个runloop中。

对所有创建方法参数做个说明:

  • ti(interval):定时器触发间隔时间,单位为秒,可以是小数。如果值小于等于0.0的话,系统会默认赋值0.1毫秒
  • invocation:这种形式用的比较少,大部分都是block和aSelector的形式
  • yesOrNo(rep):是否重复,如果是YES则重复触发,直到调用invalidate方法;如果是NO,则只触发一次就自动调用invalidate方法
  • aTarget(t):发送消息的目标,timer会强引用aTarget,直到调用invalidate方法
  • aSelector(s):将要发送给aTarget的消息
  • userInfo(ui):传递的用户信息。使用的话,首先aSelector须带有参数的声明,然后可以通过[timer userInfo]获取,也可以为nil,那么[timer userInfo]就为空
  • date:触发的时间,一般情况下我们都写[NSDate date],这样的话定时器会立马触发一次,并且以此时间为基准。如果没有此参数的方法,则都是以当前时间为基准,第一次触发时间是当前时间加上时间间隔ti
  • block:timer触发的时候会执行这个操作,带有一个参数,无返回值 (注:添加到runloop,参数timer是不能为空的,否则抛出异常)

下面三种创建方法,自动将timer添加到了当前runloop ,模式是NSDefaultRunLoopMode:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

下面五种创建方法,不会自动添加到runloop,还需调用- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode

+ (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:(id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;
 - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;

触发NSTimer

  1. 预约触发:在NSTimer创建完成并添加到runloop之后,它会在我们创建时预约的时间之后触发。
  2. 立即触发:- (void)fire。这种触发方式对定时器的影响分为两种情况:
    (1). 对于重复执行设置为YES的定时器,立即触发后原定时器依然生效;
    (2). 对于重复执行设置为NO的定时器,立即触发后原定时器失效。

销毁NSTimer

销毁NSTimer要调用- (void)invalidate , 这个是唯一一个可以将计时器从runloop中移除的方法。销毁的时机就是target释放,那么怎么捕获target被释放了呢?如果是控制器的话可以尝试监听pop方法的调用(nav的代理),或者viewDidDisappear方法里面也可以。如果一定要写在dealloc方法中,一定要保证不存在循环引用,因为在循环引用存在的时候,dealloc方法就不会走。

NSTimer与runloop

不论是在主线程还是子线程中创建的NSTimer都需要添加(无论是主动添加还是被动添加)到相应的runloop中。

线程和runloop是一一对应的关系,且同生共死。而一个timer也只能被添加到一个runloop,也就是说一个timer只能添加到一个线程,如果非要添加到多个runloop,则只有一个有效。

注意:主线程中的runloop是主动开启的,子线程中的runloop需要手动调用run方法([[NSRunloop currentRunloop] run])开启。

NSTimer与performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:就是在内部创建了一个NSTimer,然后会添加到当前线程的runloop中。
问:在子线程中延迟执行performSelector:withObject:afterDelay:会调用selector方法吗?
答:不会。因为NSTimer是在当前runloop中延时执行的,而子线程的runloop默认不开启,因此无法响应selector方法。需要添加[[NSRunLoop currentRunLoop] run]开启子线程的runloop,或者在主线程实现该延时执行。

注意:若想开启某线程的runloop,必须具有timer、source、observer任一事件才能开启,即执行[[NSRunLoop currentRunLoop] run]前,如果没有任何事件添加到当前runloop,那么该线程的runloop也是不会开启的。

NSTimer注意事项

  1. timer必须加入到Runloop中。
    因为Timer是也是一种资源,这种资源想起作用必须加入到runloop中。同理,如果Runloop中不包含任何资源,运行该Runloop就会立即退出。
  2. timer所在的mode只有跟当前runloop的mode相同时,才会响应timer事件。
    同一线程的Runloop在运行的时候,任意时刻只能处于其中一种mode。所以只能当runloop处于timer所在的mode的时候,timer才能得到触发事件的机会。如果不处于Timer的mode时,就无法响应Timer事件。NSRunLoopCommonModes 不是真正的模式,它属于占位模式(NSDefaultRunLoopMode、UITrackingRunLoopMode都会执行)。
  3. timer创建和销毁必须在同一线程。
  4. timer不是一种实时的机制, NSTimer回调的时间间隔可能会有存在误差。
    因为RunLoop每跑完一次圈再去检查当前累计时间是否已经达到定时器所设置的间隔时间,如果未达到,RunLoop将进入下一轮任务,待任务结束之后再去检查当前累计时间,而此时的累计时间可能已经超过了定时器的间隔时间,故可能会存在误差。
    解决办法:通过GCD定时器来代替它。
  5. timer 会对target 产生强引用,如果target又对timer产生强引用,那么就会引发循环引用。
    解决办法:
    (1)使用block。
    给NSTimer添加分类方法,在分类方法中调用 [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(repeatAction:) userInfo:timerBlock repeats:repeats],把target由之前的controller对象变成了timer对象,并且把block作为userInfo参数传递出去。不仅解决了循环引用,而且把timer的创建和事件处理位置进行了统一,提高了代码可读性。

//.h

typedef void(^TimerBlock)(void);

@interface NSTimer (CicularReference)

/// timer使用block方式添加Target-Action,解决循环引用
+ (NSTimer *)scheduledTimerWithInterval:(NSTimeInterval)interval block:(TimerBlock)timerBlock repeats:(BOOL)repeats;

@end

//.m

@implementation NSTimer (CicularReference)

+ (NSTimer *)scheduledTimerWithInterval:(NSTimeInterval)interval block:(TimerBlock)timerBlock repeats:(BOOL)repeats
{
    NSTimer *timer = [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(repeatAction:) userInfo:timerBlock repeats:repeats];
    
    return timer;
}

+ (void)repeatAction:(NSTimer *)timer
{
    TimerBlock timerBlock = timer.userInfo;
    if (timerBlock) {
        timerBlock();
    }
}

@end

//使用

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.timer
     = [NSTimer scheduledTimerWithInterval:1
                                  block:^{
        [self timerDoing];
        
    }
                                repeats:YES];
}

- (void)timerDoing
{
    NSLog(@"定时操作");
}

#pragma mark - dealloc
- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}

(2)target 使用代理对象,且target属性设置为弱指针。

方式一:继承NSObject

//.h

@interface MxProxy : NSObject

//弱指针
@property (nonatomic, weak) id target;

+ (instancetype)proxyWithTarget:(id)target;

@end

//.m

@implementation MxProxy

+ (instancetype)proxyWithTarget:(id)target
{
    MxProxy *proxy = [[MxProxy alloc] init];
    proxy.target = target;
    
    return proxy;
}

//消息转发
- (id)forwardingTargetForSelector
{
    return self.target;
}

@end

//使用

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    //proxy:NSObject
    MxProxy *proxy = [MxProxy proxyWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(timerDoing) userInfo:nil repeats:YES];
}

- (void)timerDoing
{
    NSLog(@"定时操作");
}

#pragma mark - dealloc
- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}

@end

方式二、继承NSProxy

//.h

//MfnProxy
@interface MfnProxy : NSProxy

//弱指针
@property (nonatomic,weak) id target;

+(instancetype)proxyWithTarget:(id)target;

@end

//.m

@implementation MfnProxy

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

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

-(void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}

@end

//使用

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    //proxy:MfnProxy
    MfnProxy *proxy1 = [MfnProxy proxyWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy1 selector:@selector(timerDoing) userInfo:nil repeats:YES];
}

- (void)timerDoing
{
    NSLog(@"定时操作");
}

#pragma mark - dealloc
- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}

@end
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值