iOS底层-定时器

CADisplayLink、NSTimer使用注意

  1. NStimer与CADisplayLink初始化示例

     #import "ZHTestViewController.h"
     @interface ZHTestViewController ()
     //@property (nonatomic,strong) CADisplayLink *playLink;
     @property (nonatomic,strong) NSTimer *timer;
     @end
        
     @implementation ZHTestViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         self.view.backgroundColor = UIColor.blueColor;
            
         /*********CADisplayLink**********/
         //每隔一段时间掉用一次方法
         //调用时间不用设置,调用时间间隔跟屏幕刷帧频率一样。
         //displayLinkWithTarget: selector: 该方法会对target捕捉,造成循环引用
     //    self.playLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
     //    [self.playLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            
         /*********NSTimer**********/
         //scheduledTimerWithTimeInterval也对target有一个强引用,导致循环引用
         self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
     }
     - (void)linkTest {
         NSLog(@"%s",__func__);
     }
     - (void)timerTest {
         NSLog(@"%s",__func__);
     }
        
     -(void)dealloc{
     //    [self.playLink invalidate];
         [self.timer invalidate];
     }
        
     @end
    
    1. 发现问题,无论是NSTimer还是CADisplayLink,back到上一个控制器时,都不能正常的停止定时器,而且当前控制器也不会销毁,说明产生了循环引用
    2. 经过分析分别是:displayLinkWithTarget: selector: scheduledTimerWithTimeInterval都对target进行了强引用,导致循环引用
    3. 对于NSTeimer可以用另外一种创建方式来解决这个问题

        //这个只能在block中使用
       __weak typeof(self) weakself = self;
       self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
           [weakself timerTest];
       }];
      
    4. 但是CADisplayLink呢? 而且NSTimer的非block方式就不能解决了吗?
    5. 循环引用分析以及解决办法如下:

      图4

      1. 解决办法
        1. 引入一个中间对象proxy,让timer的target引用为中间对象proxy
        2. 中间对象proxy的target弱引用viewcontroller
        3. 但是有个问题:timer触发时,调用的是proxy的方法,可是触发方法在viewcontroller怎么办呢?
          1. 解决办法:消息转发
  2. 解决办法:

     //中间对象proxy
     #import <Foundation/Foundation.h>
    
     @interface ZHProxy : NSObject
     +(instancetype)proxyWithTarget:(id)target;
     @end
        
     #import "ZHProxy.h"
     #import <objc/runtime.h>
        
     @interface ZHProxy ()
     @property (nonatomic,weak) id target;
     @end
        
     @implementation ZHProxy
     +(instancetype)proxyWithTarget:(id)target{
         ZHProxy *proxy = [[ZHProxy alloc] init];
         proxy.target = target;
         return  proxy;
     }
     //消息转发
     -(id)forwardingTargetForSelector:(SEL)aSelector{
         return self.target;
     }
     @end
        
     //使用
     #import "ZHTestViewController.h"
     #import "ZHProxy.h"
     @interface ZHTestViewController ()
     @property (nonatomic,strong) CADisplayLink *playLink;
     @property (nonatomic,strong) NSTimer *timer;
     @end
        
     @implementation ZHTestViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         self.view.backgroundColor = UIColor.blueColor;
         //定时器
         //每隔一段时间掉一共一次方法
         //调用时间不用设置,调用时间间隔跟屏幕刷帧频率一样。
         //displayLinkWithTarget: selector: 该方法会对target捕捉,造成循环引用
     //    self.playLink = [CADisplayLink displayLinkWithTarget:[ZHProxy proxyWithTarget:self] selector:@selector(test)];
     //    [self.playLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            
         /*********NSTimer**********/
         //scheduledTimerWithTimeInterval也对target有一个强引用,导致循环引用        
         //这个却不能使用weakself
         self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[ZHProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
     }
     - (void)linkTest {
         NSLog(@"%s",__func__);
     }
     - (void)timerTest {
         NSLog(@"%s",__func__);
     }
     -(void)dealloc{
     //    [self.playLink invalidate];
         [self.timer invalidate];
     }
        
     @end
    
    
  3. NSProxy

    1. NSProxy类

       @interface NSProxy <NSObject> {
           Class	isa;
       }
       //NSObject比较
       @interface NSObject <NSObject> {
           Class isa;
       }
      
      1. NSProxy是一个跟NSObject一样的基类,不继承任何父类
      2. 这个类专门用于做消息转发的
      3. 效率高
        1. 不需要通过像NSObject对象一样通过isa一系列查找方法来进行了
        2. 当前对象没有相应的方法,直接走消息转发
      4. 初始化不需要init
    2. 如果上面的ZHProxy继承自NSProxy,如下

       #import <Foundation/Foundation.h>
       @interface ZHProxy1 : NSProxy
       +(instancetype)proxyWithTarget:(id)target;
       @end
       #import "ZHProxy1.h"
       @interface ZHProxy1 ()
       @property (nonatomic,weak) id target;
       @end
              
       @implementation ZHProxy1
       +(instancetype)proxyWithTarget:(id)target{
           //NSProxy没有init方法
           ZHProxy1 *proxy = [ZHProxy1 alloc];
           proxy.target = target;
           return  proxy;
       }
              
       //返回方法签名
       -(NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
           return  [self.target methodSignatureForSelector:sel];
       }
       //
       -(void)forwardInvocation:(NSInvocation *)invocation{
       //    invocation.target = self.target;
           [invocation invokeWithTarget:self.target];
       }
       @end
      

GCD定时器

  1. NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
  2. 而GCD的定时器会更加准时
    1. 不依赖于runloop
    2. 直接与系统的内核挂钩
  3. 简单使用:

     #import "ZHTestViewController.h"
     @interface ZHTestViewController ()
        
     @property (nonatomic,strong) dispatch_source_t timer;
     @end
        
     @implementation ZHTestViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         self.view.backgroundColor = UIColor.blueColor;
         //队列
         dispatch_queue_t queue = dispatch_get_main_queue();
         //自动会在子线程执行
     //    dispatch_queue_t queue = dispatch_queue_create("queuegcd", DISPATCH_QUEUE_SERIAL);
            
         //设置时间
         NSTimeInterval start = 2.0; //2秒后开始执行
         NSTimeInterval interval = 1.0;//每隔1秒执行一次
         dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
         self.timer = timer;
         //设置时间
         dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC,0);
         //设置回调
         dispatch_source_set_event_handler(timer, ^{
                
             NSLog(@"=============");
         });
         //启动定时器
         dispatch_resume(timer);
     }
    
  4. 封装使用:

     #import <Foundation/Foundation.h>
     @interface ZHTimer : NSObject
     /**
      用GCD封装一个定时器
        
      @param task 任务
      @param start n秒后开始执行
      @param interval 每隔多少秒执行一次
      @param repeat 是否重复执行
      @param async 是否是异步执行
      @return 当前任务标识
      */
     +(NSString *)excue:(void (^)(void)) task start:(NSTimeInterval )start interval:(NSTimeInterval)interval repeats:(BOOL)repeat async:(BOOL)async;
        
     /**
      取消定时任务
        
      @param task 定时任务标识
      */
     +(void)cancelTask:(NSString *)task;
     @end
     #import "ZHTimer.h"
    
     @implementation ZHTimer
     static NSMutableDictionary *timers;
     static dispatch_semaphore_t seamp_;
     /**
      +initialize会被调用0次,一次,多次
         
      调用0次:表明这个类没有被使用到
      调用1次:表明只有这个类被使用到,它的子类要不然是没有被使用到,要不然就是有这个子类自己的+initialize(子类中+initialize中没有[super initialize]代码)方法
      调用多次:表明这个类或者它的多个子类被使用了,并且这多个子类没有自己的+initialize方法,子类会根据OC消息发送流程而调用了它父类的+initialize方法。
      */
     +(void)initialize{
            
         static dispatch_once_t onceToken;
         dispatch_once(&onceToken, ^{
             timers = [NSMutableDictionary dictionary];
             seamp_ = dispatch_semaphore_create(1);
         });
            
         //有可能多线程同时创建定时器,那么这个全局字典就会出现线程安全隐患
     //    dispatch_semaphore_t seamp = dispatch_semaphore_create(1);
     }
        
     + (NSString *)excue:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeat async:(BOOL)async{
         if (!task || (interval <0 && repeat)|| start <0 ) return nil;
        
         dispatch_queue_t queue = async ? dispatch_queue_create("queuegcd", DISPATCH_QUEUE_SERIAL) : dispatch_get_main_queue();
         //设置时间
         //注意这个timer要被强引用,否则就会释放
         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);
            
         //加锁
         dispatch_semaphore_wait(seamp_, DISPATCH_TIME_FOREVER);
         //定时器的唯一标识
         NSString *name = [NSString stringWithFormat:@"%zd",timers.count];
         timers[name] = timer;
         //解锁
         dispatch_semaphore_signal(seamp_);
            
         //设置回调
         dispatch_source_set_event_handler(timer, ^{
                
             task();
             if (!repeat) {//不重复任务
                 [self cancelTask:name];
             }
         });
         //启动定时器
         dispatch_resume(timer);
         return name;
     }
        
     +(void)cancelTask:(NSString *)task{
         if (!task.length) return;
         //加锁
         dispatch_semaphore_wait(seamp_, DISPATCH_TIME_FOREVER);
         if (!timers[task]) return;
         dispatch_source_cancel(timers[task]);
         [timers removeObjectForKey:task];
         //解锁
         dispatch_semaphore_signal(seamp_);
     }
     @end
        
        
     //使用
     #import "ZHTestViewController.h"
     #import "ZHTimer.h"
     @interface ZHTestViewController ()
     @property (nonatomic,copy) NSString *task;
     @end
        
     @implementation ZHTestViewController
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         self.view.backgroundColor = UIColor.blueColor;
         NSLog(@"began-----");
         NSString *task =  [ZHTimer excue:^{
             NSLog(@"--------%@",[NSThread currentThread]);
         } start:2.0 interval:1.0 repeats:NO async:YES];
         self.task = task;
     }
        
     -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         [ZHTimer cancelTask:self.task];
     }
     @end
    
    
Table of Contents