一. 官網的理論
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.lxweimin.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