一. 官网的理论
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