iOS 定时器的使用

## NSTimer 基本使用和注意事项
###  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 也强引用。这样就造成了循环引用。
![time](media/15398503044925/timer.png)

```
@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 来处理内存泄露的问题

#### 在viewWillDisappear 中手动去销毁定时器

```
//可以手动在 viewWillDisappear 中去销毁定时器
//在控制器即将销毁的时候销毁定时器,这样定时器对控制的强引用就解除了,循环引用也解除了
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [_timer invalidate];
    _timer = nil;
}
```

#### 自己实现一个带block的定时器分类 
实现一个不保留目标对象的定时器(把保留转移到了定时器的类对象身上,这样就避免了实例对象被保留。)

```
@interface NSTimer (RCTimer)

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

@end

@implementation NSTimer (RCTimer)

//block 在ARC的情况下,一般情况下是自动copy的,不管是设置为propetry strong/copy 都是一样的
//但是作为自己函数的参数:block 是不会copy的 ,系统的会自动copy的
// __NSStackBlock__
+ (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];
    //__NSMallocBlock__
    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];
    }];
```

####  通过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对象之间并没有相互持有,完美解决循环引用的问题。
 ![RCproxy](media/15398503044925/RCproxy.png)
            
### CADisplayLink 
会出现和 NSTimer 一样的问题
```
// 保证调用频率和屏幕的帧刷新频率一致 60FPS (正常情况下是一秒钟60次)
//self 强引用 link   link 强引用 self 会有循环引用问题 
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

- (void)dealloc {
   [self.link invalidate];
}
```

### 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(), ^{
//
//    });
}
```

* 使用 GCD 实现一个准时的定时器 

```

#import <Foundation/Foundation.h>

@interface RCTimer : NSObject

+ (NSString *)execTask:(void(^)(void))task
           start:(NSTimeInterval)start
        interval:(NSTimeInterval)interval
         repeats:(BOOL)repeats
           async:(BOOL)async;

+ (NSString *)execTask:target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async;

+ (void)cancelTask:(NSString *)task;

@end

#import "RCTimer.h"

@implementation RCTimer

static NSMutableDictionary *timers_;
dispatch_semaphore_t  semaphore_;

+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        timers_ = [NSMutableDictionary dictionary];
        semaphore_ = dispatch_semaphore_create(1);
    });
    
}

+ (NSString *)execTask:(void (^)(void))task
           start:(NSTimeInterval)start
        interval:(NSTimeInterval)interval
         repeats:(BOOL)repeats
           async:(BOOL)async {
    
    if (!task || start < 0 || (interval <= 0 && repeats)) { return nil;}
    
    //队列
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
   
    //创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
   
    //设置时间
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC, 0);  //误差为0
   
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    
    //定时器的唯一标识
    NSString *name = [NSString stringWithFormat:@"%zd",timers_.count];
    
    //存放到字典中
    timers_[name] = timer;
    
    dispatch_semaphore_signal(semaphore_);
    
    //设置回调 block 回调
    dispatch_source_set_event_handler(timer, ^{
        
        task();
        
        if (!repeats) {
            [self cancelTask:name];
        }
    });
    
    //启动定时器
    dispatch_resume(timer);
   
    return name;
}

+ (NSString *)execTask:(id)target
              selector:(SEL)selector
                 start:(NSTimeInterval)start
              interval:(NSTimeInterval)interval
               repeats:(BOOL)repeats
                 async:(BOOL)async {
    
    if (!target || !selector) return nil;
    
    __weak typeof(target) weaktarget = target;
    
    return [self execTask:^{
        if ([weaktarget respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [weaktarget performSelector:selector];
#pragma clang diagnostic pop
        }
    }
                    start:start
                 interval:interval
                  repeats:repeats
                    async:async];
}

+ (void)cancelTask:(NSString *)task {
    
    if (task.length == 0) {return;}
    
    dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
    
    dispatch_source_t timer = timers_[task];
    if (timer) {
        dispatch_source_cancel(timer);
        
        [timers_ removeObjectForKey:task];
    }
    
    dispatch_semaphore_signal(semaphore_);
}
@end
```

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值