NSTimer研究+一点点NSInvocation

一. 官网的理论

1. Timer必须知道的某些事

看了下Timer的官方解释,发现里面包含了某些我们必须知道的事儿.
这是官网文档 为了不失真,保持原汁原味的官方文档知识.我原文翻译下Overview,并勾勒出我认为的重点:

Timer是和run loop一起工作的.为了有效的使用timer,你必须知道run loop是如何工作的----查看RunLoop和线程编程指南.**尤其注意的是:run loop 强引用它其中的timmer,所以当你把timer加入run loop后,你无需对timmer保持强引用. **

timer不是实时机制的;只有当添加timer的那个run loop的mode在运行,并且可以检查timer的触发(我姑且把fire称作触发)时间是否达到时,timer才会被触发.因为run loop有很多输入源需要处理,所以run loop给timer的有效处理时间间隔精度控制在50-100毫秒内(PS:这一句话没读懂).如果timer的触发时间正好在run的loop的一个较长的回调上,或者run loop现在正在处理另一个mode,这个mode又不是处理timmer的那一个,timer就不会被触发了.直到下次run loop 检查timer.因此,timer实际的触发时间有可能比你schedule(计划)它触发的时间晚上一大截.参看Timer Tolerance--见本页

NSTimer和它的Core Foundation的对应品CFRunLoopTimer是"免费桥接"的,查看免费桥接获取更多免费桥接的信息.

重复VS不重复的Timer

在创建timer时,你可以创建重复或者不重复的Timer.(构造函数的repeat参数为YES 或者NO)不重复的timer只触发一次,然后会自动invalidate它自己,这样就能让它不再被触发了.相反的,重复的timer在同一个run loop里面触发,然后重新schedule它自己.

重复timer基于计划触发时间而不是实际的触发时间来计划它自己.例如,如果一个timer计划从某个时间开始触发,并每隔5秒触发一次,那么计划触发时间总是落在最开始的5秒内,即使实际的触发时间延迟了.如果触发时间已经延迟得很厉害,以至于它错过了一次或者更多的应触发时间,那么在那段时间里,timer也只会触发一次;当这次触发完成后,timer重新计划下次的触发时间.

Timer Tolerance

iOS 7和macOS 10.9之后,你可以设置timer的容忍度了.允许timer触发时的系统灵活性能促进系统优化电量节约和响应速度的能力(这句话翻译得好蹩脚).timer会在计划触发时间和计划触发时间 + tolerance的这段时间触发.timer不会在计划触发时间之前触发.对于重复timer来说,下次触发时间的计算是不考虑tolerance的,它从最开始的触发时间开始计算,以此避免时间偏移. tolerance的默认值是0,这意味着不会应用tolerance.系统保留使用tolerance在timer上的权利,但是可以忽略tolerance的值.(说白了,这个值只是一个允许容忍延迟的情况,系统可以采用,也可以不用,一般是不用的.)

接下来的这一节比较使用,我加上了些自己的理解代码:

在Run Loops中Scheduling Timer

虽然一个timer对象可以被加到1个run loop 的多个mode上去,但它一次只能在一个run loop中注册.三种创建timer的方法:

  • schedule的类方法
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

这三个带schedule的类方法,会自动创建timer,并把它schedule到当前的run loop上去,用的mode是默认的mode:NSDefaultRunLoopMode

  • timerwith类方法
+ (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;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

它们创建的timer对象没有被schedule进run loop.(你可以手动把它们进入run loop,通过调用对应的run loop的add(_:forMode:)方法)
例如:主线程run loop上添加timer

  NSTimer * timer = [NSTimer timerWithTimeInterval:5 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"oh ");
    }];

    
    [[NSRunLoop currentRunLoop] addTimer:timmer forMode:NSDefaultRunLoopMode];
  • init实例方法
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;

你可以手动把它们进入run loop,通过调用对应的run loop的add(_:forMode:)方法

一旦在run loop上schedule了timer,timer会在特定的时间触发,直到invalidate.不重复的timer会在触发完后立马自动invalidate.对于重复的timer,你必须手动调用invalidate()来invalidate它.invalidate()方法会让run loop 移除timer;当然,你在哪个run loop上加的timer,就在哪个线程上去invalidate,(PS:每个线程都有一个run loop).invalidate这个timer导致它立马不能用,然后再也不会影响run loop了.在invalidate()方法执行return前或者结束后的某个时间内,run loop会去掉对timer的强引用.一旦被invalidate后,timer就不能再用了.

当重复的timer触发后,它计划下次的触发时间:time interval的整数倍 + 最近计划触发时间,在tolerance内.如果调用selector或者invocation的时间比设定的time interval长,那么timer不会调用,只会进入下一次的触发;所以,timer是不会对错过的触发进行补偿.

子类化的注意

你不能尝试子类化NSTimer

PS:虽然前面文档说不用强引用一个timmer,但是我需要取用timmer,用于后继的invalidate,却发现run loop没有提供对外拿到timer的属性方法之类的,所以我还是要强引用,作为一个成员变量.如下:

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    [self testTimmer];
}

- (IBAction)btnClick:(id)sender {
    if ([self.timer isValid]) {
        [self.timer invalidate ];
        self.timer  = nil;
    }
}

-(void) testTimmer{
    NSTimer * timmer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1]  interval:5 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"oh ");
    }];

   // [timmer fire];
    
    [[NSRunLoop currentRunLoop] addTimer:timmer forMode:NSDefaultRunLoopMode];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end 

二.具体用法

上面是理论和简单的用法,下面说下我接触到的定时器用法,总结下.

1. NSTimer

1.1 用带schedule的方法来启动timer

无需手动把timer add到runloop中. 因为schedule的方法一共有三个形式,任选一个写一下:

//repeats为YES是重复timer,为NO是不重复timer,只会调用一次,然后自动invalidate
self.timer = [NSTimer scheduledTimerWithTimeInterval:5 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"oh");
}];

在需要的地方,(如持有timer的页面dismiss退出显示的时候).把重复timer从run loop中移除,否则timer永远都在run loop中跑呢,而且你那个持有timer的并已经dismiss的页面也没有真正从内存中退栈,(因为有timer的retain)这应该不是你想要的结果.

  • 对于不重复timer:
    在需要的地方(对vc来说一般是viewDidDisappear或viewWillDisappear,didReceiveMemoryWarning; NSObject如dealloc)销毁我们对self.timer的强引用,但是不需要invalidate,因为invalidate是让timer从run loop中移除(去掉retain),而这一步在不重复的timer中,已经自动完成了.
if ([self.timer isValid]) {
            timer = nil;
}
  • 重复的timer需要这么写:
if ([self.timer isValid]) {
        [self.timer invalidate];
        self.timer = nil;
}

1.2 用不带schedule的方式创建timer

需手动把它加入run loop


-(void) testTimmerWithoutSchedule{
    self.timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1]  interval:5 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"oh ");
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

在需要的地方销毁我们对self.timer的强引用

if ([self.timer isValid]) {
        [self.timer invalidate];
        self.timer = nil;
}

1.3. fire方法

这个方法如同其名字--立马发射!
这个方法在调用的时间上让timer立即触发.

  • 对重复timer来说,它是一次额外的触发,它的触发不会影响正常的schedule.
  • 对不重复timer来说,它一触发后,timer就被invalidate了.而不管它本来的计划时间是怎么样的.
    大家可以对timer加上 [self.timer fire], 和不加 [self.timer fire]的触发效果对比一下.

1. 4 暂停和启动

用一种取巧的方式,让timer的下一次时间为 "遥远的未来",就是暂停了:
[timer setFireDate:[NSDate distantFuture]];

再次启动,就是设置它的fire时间为马上,用:
[timer setFireDate:[NSDate date]];
或者:
[self.timer setFireDate:[NSDate distantPast]];

注意:这只适用于重复timer.不重复的timer触发一次就废了,还有啥暂停之说...

-(void) pauseTimer{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:5 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"oh");
        
        //暂停
        [timer setFireDate:[NSDate distantFuture]];

    }];

}


- (IBAction)cancelPause:(id)sender {
    
    //继续
    [self.timer setFireDate:[NSDate date]];
    //或者:
    //[self.timer setFireDate:[NSDate distantPast]];
}

1.5 非等长时间执行timer

timer的执行时间是定长的,timeInterval只能写个数字,然后坐等......
一个技巧是,把timer的初试执行时间(timeInterval)设置为很大,所以它就不会执行,然后用fireDate来控制它的执行时间,就可以不定长的执行timer了:
直接上代码:

-(void) randomTimeTimer{
    
    
    self.timer =  [NSTimer timerWithTimeInterval:MAXFLOAT
                                                            target:self selector:@selector(randomTimeFireMethod)
                                                          userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    self.timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:5];

}



-(void) randomTimeFireMethod{
    
    static int timeExecute = 0;
    NSLog(@"random call");
    
    //随机时间数组里面只放了4个元素,执行4次好了,不然用一个循环列表来做,就没有次数限制了;不然改一下timeExecute,让它逢4变1.哈哈
    if (timeExecute < 4) {
        //不定长执行
        NSTimeInterval timeInterval = [self.randomTime[timeExecute] doubleValue];
        
        timeExecute++;
        
        self.timer.fireDate = [NSDate dateWithTimeIntervalSinceNow:timeInterval];
        
    }
    
}

也就是说,timer于何时执行,你可以用fireDate实现完全的控制.根据业务需要来.

2. dispatch_source_set_timer

用起来代码量比较多,效果却和NSTimer差不多,我一般不这么用.

-(void) testTimerGCD{
    //1. 创建定时器
    dispatch_source_t timer=dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    
    //2. schedule时间
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 15ull*NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 1ull*NSEC_PER_SEC);
    
    //3. 绑定timer的响应事件
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"wakeup");
        
        
        //调用结束timer的事件,这里可在此调用,也可写到别的地方去,把局部变量timer变成成员变量,这里写只是举例
        dispatch_source_cancel(timer);
    });
    
    //绑定timer的cancel响应事件
    dispatch_source_set_cancel_handler(timer, ^{
        NSLog(@"cancel");
        //dispatch_release(timer);
    });
    
    
    //4. 最重要的一步,启动timer!
    dispatch_resume(timer);
}

三. run loop

RunLoop类声明了输入源的可编程接口.一个RunLoop对象处理的输入源有:鼠标,键盘事件,Port,NSConnection对象,以及timer.
和前几种对象相比,timer对象并不是输入,它是一种特殊类型,所以当它触发时,不会引起run loop return

RunLoop对象不是直接创建的.每一个线程对象----包含在应用的主线程----都有一个自动创建的RunLoop对象.访问当前线程的runloop,调用current方法:

[[NSRunLoop currentRunLoop] addTimer:timmer forMode:NSDefaultRunLoopMode];

四. NSInvocation

之前也没接触过这个,看到NSTimer里用到了,比较好奇.研究了下官方文档.发现它是个存储和发送"消息"的地方.

  • 一个消息的target,selector,argument,return value,都可以直接给它指派.一般前三个指派后,调用它的invoke函数执行这个消息,就可以自动获得返回值(return value)
    可以把它的target,selector,argument做任意次的修改,就能反复invoke.
  • 它不像performSelector:withObject:afterDelay:那样只能传入一个参数,可以传入多个参数.
  • NSInvocation不能处理有可变参数的情况,也不能处理参数为union的情况.
  • 只能通过它的类方法invocationWithMethodSignature:来使用它,不可以通过 alloc init的方式
  • NSInvocation没有对它使用的参数进行retain,所以为了防止参数在NSInvocation创建和invoke之间的这段时间里变成nil,要么我们自己强引用它, 要么调用NSInvocation的 这个方法,把他们retain起来.
- (void)retainArguments;

还可以用这个属性查看参数是否都被retain了:

@property (readonly) BOOL argumentsRetained;

那么在NSTimer中怎么用它呢:

-(void) testTimmer{
//    这么写也行
//    NSMethodSignature * signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
//    NSInvocation * i = [NSInvocation invocationWithMethodSignature:signature];
//    i.selector = @selector(fireMethod);
//    i.target = self;
    
    NSMethodSignature * sig = [ViewController instanceMethodSignatureForSelector:@selector(fireMethod)];
    NSInvocation * i = [NSInvocation invocationWithMethodSignature:sig];
    
    i.selector = @selector(fireMethod);
    i.target = self;

    
    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:3 invocation:i repeats:NO];
}

-(void) fireMethod{
    NSLog(@"ho");
}

介绍下NSInvocation的多参数传递.

  • 如果没有参数,如下:
-(void) testInvocation{
    NSMethodSignature * sig = [[ViewController class] instanceMethodSignatureForSelector:@selector(fireMethod)];
    NSInvocation * i = [NSInvocation invocationWithMethodSignature:sig];
    
    i.target = self;
    i.selector = @selector(fireMethod);
    
    [i invoke];
}

-(void) fireMethod{
    NSLog(@"ho");
}

  • 如果带参数:
-(void) testInvocationWithParam{
    NSMethodSignature * sig = [[ViewController class] instanceMethodSignatureForSelector:@selector(fireMethod1:str2:str3:)];
    NSInvocation * i = [NSInvocation invocationWithMethodSignature:sig];
    
    i.target = self;
    i.selector = @selector(fireMethod1:str2:str3:);
    
    //这里的Index要从2开始,以为0跟1已经被占据了,分别是self(target),selector(_cmd)
    NSString * param1 = @"param1";
     NSString * param2 = @"param2";
     NSString * param3 = @"param3";
    
    [i setArgument:¶m1 atIndex:2];
    [i setArgument:¶m2 atIndex:3];
    [i setArgument:¶m3 atIndex:4];
    
    [i invoke];
}
-(void) fireMethod1:(NSString *)str1 str2:(NSString *) str2 str3:(NSString *)str3 {
    NSLog(@"ho param: %@,%@,%@", str1, str2, str3);
}
  • 获取返回值

返回值不是手动赋值的,而是赋了参数后invoke,就能获取了,否则手动赋值了也不会被系统采用的,获取出来的还是invoke后的实际返回值.

-(void) testInvocationReturnValue{
    
    
    //创建一个函数签名,这个签名可以是任意的,但需要注意,签名函数的参数数量要和调用的一致。
    // 方法签名中保存了方法的名称/参数/返回值,协同NSInvocation来进行消息的转发
    // 方法签名一般是用来设置参数和获取返回值的, 和方法的调用没有太大的关系
    // NSInvocation中保存了方法所属的对象/方法名称/参数/返回值
    //其实NSInvocation就是将一个方法变成一个对象
    NSMethodSignature * sig = [[ViewController class] instanceMethodSignatureForSelector:@selector(addByA:b:c:)];
    NSInvocation * i = [NSInvocation invocationWithMethodSignature:sig];
    
    i.target = self;
    i.selector = @selector(addByA:b:c:);
    
    //这里的Index要从2开始,以为0跟1已经被占据了,分别是self(target),selector(_cmd)
    int param1 = 1;
    int param2 = 2;
    int param3 = 3;
    
    SEL cl = @selector(addByA:b:c:);
    
    //我看人家的代码还给前2个参数赋值了,这里不写也可以的
    ViewController * dd = self;
    [i setArgument:&dd atIndex:0];
    [i setArgument:&cl atIndex:1];
    
    [i setArgument:¶m1 atIndex:2];
    [i setArgument:¶m2 atIndex:3];
    [i setArgument:¶m3 atIndex:4];
    
    //尝试设置其return值 看是否会搅乱函数的正常逻辑?
    
    [i setReturnValue:¶m3];
    
    
    [i invoke];
    
    //取出其返回值查看 -- 结果是,设置返回值是没有用的
    int returnValue;
    
    [i getReturnValue:&returnValue];
    NSLog(@"returnValue:%i", returnValue);
}

-(int) addByA:(int)a b:(int)b c:(int) c{
    NSLog(@"param:a - %i, b - %i, c - %i,", a,b,c);
    return a + b + c;
}

参考:
http://www.jianshu.com/p/3ccdda0679c1

http://www.cnblogs.com/ios-wmm/archive/2012/08/24/2654779.html

http://blog.csdn.net/chenyong05314/article/details/12950267

http://blog.csdn.net/chenyong05314/article/details/12950267

http://www.cnblogs.com/ios-wmm/archive/2012/08/24/2654779.html

demon

你可能感兴趣的:(NSTimer研究+一点点NSInvocation)