欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

NSTimer 基本使用和注意事项

程序员文章站 2022-04-13 20:48:38
...

NSTimer的基本使用

  • NSTimer: 一个在确定时间间隔内执行一次或多次我们指定对象方法的对象。

  • 基本使用:

两个比较常用的方法:
timerWithTimeInterval: target: selector: userInfo: repeats:;

scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:;

区别:

  • 第一个需要手动添加到Runloop 中。第二个不需要,自动就添加到了当前的Runloop 中。
第一种方式的使用:
_timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
//默认是 NSDefaultRunLoopMode
//NSRunLoopCommonModes 包含了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode,所以滑动的时候也能响应定时器
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];

第二种方式:
//默认会自动添加到 NSDefaultRunLoopMode
_timer = [NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

- (void)timerAction {
    NSLog(@" === timerAction == ");
}

NSTimer在线程中的使用

  • 在子线程上直接使用是没有反应的,因为runloop 在子线程上,需要手动去开启当前的runloop [[NSRunLoop currentRunLoop] run];
  • 在子线程上创建的定时器,必须要在子线程中销毁,不要在主线程中销毁,否者会造成runloop 资源泄露[self performSelector:@selector(invalidateTimer) withObject:nil afterDelay:3];
  • runloop 的创建方式不是通过alloc init 是通过 [NSRunLoop currentRunLoop] 来直接获取的
  • 如果当前线程中有大量的复杂操作,会导致定时器的卡住
//子线程中使用定时器
[NSThread detachNewThreadSelector:@selector(threadTimer) toTarget:self withObject:nil];

//NSTimer 在线程中的使用
- (void)threadTimer {
    NSLog(@"%@",[NSThread currentThread]);
    //直接调用是没有任何反应的
    //runloop 在子线程中是需要手动开启的
    _timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

    //子线程上的定时器,必须要在子线程中销毁,不要在主线程中销毁,否者会造成runloop 资源泄露
    [self performSelector:@selector(invalidateTimer) withObject:nil afterDelay:3];

    //手动开启runoop
    [[NSRunLoop currentRunLoop] run];

    //runloop 的创建方式不是通过alloc init 是通过 [NSRunLoop currentRunLoop] 来直接获取的
    NSLog(@" === 子线程的timer 销毁了 === ");
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //如果在当前线程有复杂的操作,会导致定时器卡住
        [self busyCalculate];
    });

//如果在当前线程有复杂的操作,会导致定时器卡住
- (void)busyCalculate {
    NSUInteger count = 0xFFFFFFF;
    CGFloat num = 0;
    for (int i = 0; i < count; i ++) {
        num = i/count;
    }
}

NSTimer在UIScrollView中的使用

  • 当在scrollView 中滑动的时候,定时器会暂停,原因是默认的Timer是在NSDefaultRunLoopMode ,但是在滑动的时候runloop是UITrackingRunLoopMode,
  • runloop 同一时刻只能在一个mode 上来运行,其他 mode 上的任务暂停。
  • 所以在Timer 中最好是设置 mode为 NSRunLoopCommonModes ,因为NSRunLoopCommonModes 包含了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode,所以滑动的时候也能响应定时器

NSTimer循环引用的问题和解决

  • NSTimer 的销毁(invalidate):
    • invalidate方法 会停止计时器的再次触发,并在RunLoop中将其移除。
    • invalidate 方法 是将NSTimer对象从RunLoop 中移除的唯一方法。
    • 调用invalidate方法会删除RunLoop对NSTimer的强引用,以及NSTimer对target和userInfo的强引用!
Description: 
//invalidate方法 会停止计时器的再次触发,并在RunLoop中将其移除。
Stops the timer from ever firing again and requests its removal from its run loop.

//invalidate 方法 是将NSTimer对象从RunLoop 中移除的唯一方法。
This method is the only way to remove a timer from an NSRunLoop object.

//调用invalidate方法会删除RunLoop对NSTimer的强引用,以及NSTimer对target和userInfo的强引用!
The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
  • NSTimer的循环引用(从NSTimer的描述和方法的描述可以看出)
    • 计时器与运行循环一起工作。RunLoop 维持着对定时器的强引用。
    • 当计时器触发后,在调用invalidated 之前会一直保持对target的强引用
//计时器与运行循环一起工作。RunLoop 维持着对定时器的强引用。
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
//当计时器触发后,在调用invalidated 之前会一直保持对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 强引用,NSTimer 又对控制器强引用,RunLoop 对NSTimer 也强引用。这样就造成了循环引用。

NSTimer 基本使用和注意事项

@interface TimerViewController ()
@property (nonatomic,strong) NSTimer *timer;
@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

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

}

- (void)timerAction {
    NSLog(@" === timerAction == ");
}

- (void)dealloc {
    [_timer invalidate];
    _timer = nil;
    NSLog(@" _timer invalidate 销毁了");
}

@end

以上代码平时我们一般都是这样使用的,但是这样dealloc 方法一般是不会走到的,这样定时器是永远都不会销毁的。

  • 解决循环引用的方法有三种

    • 一般情况下在直接在 viewWillDisappear 中手动去销毁定时器
    • 自己实现一个带block的定时器分类,实现一个不保留目标对象的定时器
    • 通过NSProxy 来处理内存泄露的问题
  • 1、在viewWillDisappear 中手动去销毁定时器

//可以手动在 viewWillDisappear 中去销毁定时器
//在控制器即将销毁的时候销毁定时器,这样定时器对控制的强引用就解除了,循环引用也解除了
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [_timer invalidate];
    _timer = nil;
}
  • 2、自己实现一个带block的定时器分类,实现一个不保留目标对象的定时器(把保留转移到了定时器的类对象身上,这样就避免了实例对象被保留。)
@interface NSTimer (RCTimer)

+ (instancetype)timerWithTimeInterval:(NSTimeInterval)ti block:(void(^)(void))block repeats:(BOOL)repeats;

@end

@implementation NSTimer (RCTimer)

+ (instancetype)timerWithTimeInterval:(NSTimeInterval)ti block:(void (^)(void))block repeats:(BOOL)repeats {

    return [NSTimer timerWithTimeInterval:ti target:self selector:@selector(timerAction:) userInfo:block repeats:repeats];
}

+ (void)timerAction:(NSTimer *)timer {
    void(^block)(void) = [timer userInfo];
    if (block) {
        block();
    }
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    //引起循环引用的主要是 targrt:self
    //自己实现一个不保留目标的定时器
    __weak typeof(self)Weakself = self;
    _timer = [NSTimer timerWithTimeInterval:1.f block:^{
        [Weakself timerAction];
    } repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
- (void)timerAction {
    NSLog(@" === timerAction == ");
}

- (void)dealloc {
      // 务必在当前线程调用invalidate方法,使得Runloop释放对timer的强引用
     [_timer invalidate];
     _timer = nil;
}
 //在iOS 10 之后系统提供了类似的block 的方法来解决循环引用的问题
    __weak typeof(self)Weakself = self;
    _timer = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
        [Weakself timerAction];
    }];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];

    //schedule方式
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
        [Weakself timerAction];
    }];
  • 3、通过NSProxy 来处理循环引用的问题(参考YYKit)
#import <Foundation/Foundation.h>

@interface RCProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
-(instancetype)initWithTarget:(id)target;
+(instancetype)proxyWithTarget:(id)target;
@end

@implementation RCProxy
-(instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}
+(instancetype)proxyWithTarget:(id)target {
    return [[RCProxy alloc] initWithTarget:target];
}
-(id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}
-(void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
-(BOOL)respondsToSelector:(SEL)aSelector {
    return [_target respondsToSelector:aSelector];
}
-(BOOL)isEqual:(id)object {
    return [_target isEqual:object];
}
-(NSUInteger)hash {
    return [_target hash];
}
-(Class)superclass {
    return [_target superclass];
}
-(Class)class {
    return [_target class];
}
-(BOOL)isKindOfClass:(Class)aClass {
    return [_target isKindOfClass:aClass];
}
-(BOOL)isMemberOfClass:(Class)aClass {
    return [_target isMemberOfClass:aClass];
}
-(BOOL)conformsToProtocol:(Protocol *)aProtocol {
    return [_target conformsToProtocol:aProtocol];
}
-(BOOL)isProxy {
    return YES;
}
-(NSString *)description {
    return [_target description];
}
-(NSString *)debugDescription {
    return [_target debugDescription];
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];    
    _timer = [NSTimer scheduledTimerWithTimeInterval:1
                                                  target:[RCProxy proxyWithTarget:self]
                                                selector:@selector(timerAction)
                                                userInfo:nil
                                                 repeats:YES];
    }
}

- (void)timerAction {
    NSLog(@" === timerAction == ");
}

- (void)dealloc {
    [_timer invalidate];
    _timer = nil;
    NSLog(@" _timer invalidate 销毁了");
}
  • NSProxy本身是一个抽象类,它遵循NSObject协议,提供了消息转发的通用接口,NSProxy通常用来实现消息转发机制和惰性初始化资源。不能直接使用NSProxy。需要创建NSProxy的子类,并实现init以及消息转发的相关方法,才可以用。
  • RCProxy继承了NSProxy,定义了一个弱引用的target对象,通过重写消息转发等方法,让target对象去处理接收到的消息。在整个引用链中,Controller对象强引用NSTimer对象,NSTimer对象强引用RCProxy对象,而RCProxy对象弱引用Controller对象,所以在YYWeakProxy对象的作用下,Controller对象和NSTimer对象之间并没有相互持有,完美解决循环引用的问题。

NSTimer 基本使用和注意事项

GCD实现定时器

  • GCD的定时器不受RunLoop中Mode的影响(RunLoop内部也是基于GCD实现的,可以根据源码看到), 比如滚动TableView的时候,GCD的定时器不受影响;且比NSTimer更加准时。
@property (nonatomic,strong) dispatch_source_t timer;

- (void)gcdTimer:(NSTimeInterval)timeInterval repeats:(BOOL)repeats {
    //获取队列
    dispatch_queue_t queue = dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建一个定时器(dispatch_source_t本质还是个OC对象,创建出来的对象需要强引用)
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //设置定时器的各种属性(什么时候开始任务,每隔多久执行一次)  GCD的时间参数,一般是纳秒(1秒 = 10的9次方纳秒)
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, timeInterval * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    //设置回调
    dispatch_source_set_event_handler(_timer, ^{
        if (!repeats) {
            dispatch_cancel(_timer);  //取消定时器
            _timer = nil;
        }else {
            [self timerAction];
        }
    });
    //启动定时器
    dispatch_resume(_timer);

    //只执行一次操作
//    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//
//    });
}