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:&param1 atIndex:2];
    [i setArgument:&param2 atIndex:3];
    [i setArgument:&param3 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:&param1 atIndex:2];
    [i setArgument:&param2 atIndex:3];
    [i setArgument:&param3 atIndex:4];
    
    //嘗試設置其return值 看是否會攪亂函數的正常邏輯?
    
    [i setReturnValue:&param3];
    
    
    [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

demon

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內容