本文是投稿文章,作者:yohunl
目錄
我們常用NSTimer的方式
上面的NSTimer無論采用何種方式都是在主線程上跑的那麼怎麼在非主線程中跑一個NSTimer呢
GCD的方式
一次性的timer方式的GCD模式
另一種dispatch_after方式的定時器
利用GCD的弱引用型的timer
使用NSTimer方式創建的Timer使用時候需要注意
參考文檔
我們常用NSTimer的方式
如下代碼所示,是我們最常見的使用timer的方式
@property (nonatomic , strong) NSTimer *animationTimer;self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:(self.animationDuration = animationDuration) target:self selector:@selector(animationTimerDidFired:) userInfo:nil repeats:YES];123456
當使用NSTimer的scheduledTimerWithTimeInterval方法時。事實上此時Timer會被加入到當前線程的Run Loop中,且模式是默認的NSDefaultRunLoopMode。而如果當前線程就是主線程,也就是UI線程時,某些UI事件,比如UIScrollView的拖動操作,會將Run Loop切換成NSEventTrackingRunLoopMode模式,在這個過程中,默認的NSDefaultRunLoopMode模式中注冊的事件是不會被執行的。也就是說,此時使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不會執行。
我們可以通過添加一個UICollectionView,然後滑動它後打印定時器方法
2016-01-27 11:41:59.770 TimerAbout[89719:1419729] enter timer 2016-01-27 11:42:00.339 TimerAbout[89719:1419729] enter timer 2016-01-27 11:42:01.338 TimerAbout[89719:1419729] enter timer 2016-01-27 11:42:02.338 TimerAbout[89719:1419729] enter timer 2016-01-27 11:42:03.338 TimerAbout[89719:1419729] enter timer 2016-01-27 11:42:15.150 TimerAbout[89719:1419729] enter timer 2016-01-27 11:42:15.338 TimerAbout[89719:1419729] enter timer
從中可以看到,當UICollectionView滑動時候,定時器方法並沒有打印(從03.338到15.150)
為了設置一個不被UI干擾的Timer,我們需要手動創建一個Timer,然後使用NSRunLoop的addTimer:forMode:方法來把Timer按照指定模式加入到Run Loop中。這裡使用的模式是:NSRunLoopCommonModes,這個模式等效於NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的結合,官方參考文檔
還是上面的例子,換為:
self.animationTimer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(animationTimerDidFired:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.animationTimer forMode:NSRunLoopCommonModes];12
則,無論你滑動不滑動UICollectionView,定時器都是起作用的!
上面的NSTimer無論采用何種方式,都是在主線程上跑的,那麼怎麼在非主線程中跑一個NSTimer呢?
我們簡單的可以使用如下代碼:
//創建並執行新的線程 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil]; [thread start]; - (void)newThread { @autoreleasepool { //在當前Run Loop中添加timer,模式是默認的NSDefaultRunLoopMode [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(animationTimerDidFired:) userInfo:nil repeats:YES]; //開始執行新線程的Run Loop [[NSRunLoop currentRunLoop] run]; } }1234567891011121314
當然了,因為是開啟的新的線程,在定時器的回調方法中,需要切換到主線程才能操作UI。
GCD的方式
//GCD方式 uint64_t interval = 1 * NSEC_PER_SEC; //創建一個專門執行timer回調的GCD隊列 dispatch_queue_t queue = dispatch_queue_create("timerQueue", 0); _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); //使用dispatch_source_set_timer函數設置timer參數 dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0); //設置回調 dispatch_source_set_event_handler(_timer, ^(){ NSLog(@"Timer %@", [NSThread currentThread]); }); dispatch_resume(_timer);//dispatch_source默認是Suspended狀態,通過dispatch_resume函數開始它123456789101112
其中的dispatch_source_set_timer的最後一個參數,是最後一個參數(leeway),它告訴系統我們需要計時器觸發的精准程度。所有的計時器都不會保證100%精准,這個參數用來告訴系統你希望系統保證精准的努力程度。如果你希望一個計時器每5秒觸發一次,並且越准越好,那麼你傳遞0為參數。另外,如果是一個周期性任務,比如檢查email,那麼你會希望每10分鐘檢查一次,但是不用那麼精准。所以你可以傳入60,告訴系統60秒的誤差是可接受的。他的意義在於降低資源消耗。
一次性的timer方式的GCD模式
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"dispatch_after enter timer"); });123
另一種dispatch_after方式的定時器
這個是使用上面的dispatch_after來創建的,通過遞歸調用來實現。
- (void)dispatechAfterStyle { __weak typeof (self) wself = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"dispatch_after enter timer,thread = %@", [NSThread currentThread]); [wself dispatechAfterStyle]; }); }
利用GCD的弱引用型的timer
MSWeaker實現了一個利用GCD的弱引用的timer。原理是利用一個新的對象,在這個對象中:
NSString *privateQueueName = [NSString stringWithFormat:@"com.mindsnacks.msweaktimer.%p", self]; self.privateSerialQueue = dispatch_queue_create([privateQueueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(self.privateSerialQueue, dispatchQueue); self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.privateSerialQueue); - (void)resetTimerProperties { int64_t intervalInNanoseconds = (int64_t)(self.timeInterval * NSEC_PER_SEC); int64_t toleranceInNanoseconds = (int64_t)(self.tolerance * NSEC_PER_SEC); dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, intervalInNanoseconds), (uint64_t)intervalInNanoseconds, toleranceInNanoseconds ); } - (void)schedule { [self resetTimerProperties]; __weak MSWeakTimer *weakSelf = self; dispatch_source_set_event_handler(self.timer, ^{ [weakSelf timerFired]; }); dispatch_resume(self.timer); }
創建了一個隊列self.timer = dispatch_source_create,然後在這個隊列中創建timer dispatch_source_set_timer.
注意其中用到了dispatch_set_target_queue(self.privateSerialQueue, dispatchQueue); 這個是將dispatch隊列的執行操作放到隊列dispatchQueue 中去。
這份代碼中還用到了原子操作!值得好好研讀,以便以後可以在自己的多線程設計中使用原子操作。
為什麼用原子操作呢,因為作者想的是在多線程的環境下設置定時器的開關與否。
if (OSAtomicAnd32OrigBarrier(1, &_timerFlags.timerIsInvalidated)) if (!OSAtomicTestAndSet(7, &_timerFlags.timerIsInvalidated)) { dispatch_source_t timer = self.timer; dispatch_async(self.privateSerialQueue, ^{ dispatch_source_cancel(timer); ms_release_gcd_object(timer); }); }
至於其中
struct { uint32_t timerIsInvalidated; } _timerFlags;
這裡為什麼要用結構體呢?為什麼不直接使用一個uint32_t 的變量?
使用NSTimer方式創建的Timer,使用時候需要注意。
由於
self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(animationTimerDidFired:) userInfo:nil repeats:YES];
會導致timer 強引用 self,而animationTimer又是self的一個強引用,這造成了強引用的循環了。
如果不手工停止timer,那麼self這個VC將不能夠被釋放,尤其是當我們這個VC是push進來的時候,pop將不會被釋放!!!
怎麼解決呢?
當然了,可以采用上文提到的MSWeakerGCD的弱引用的timer
可是如果有時候,我們不想使用它,覺得它有點復雜呢?
1.在VC的disappear方法中應該調用 invalidate方法,將定時器釋放掉,這裡可能有人要說了,我直接在vc的dealloc中釋放不行麼?
-(void)dealloc { [_animationTimer invalidate]; }
很遺憾的告訴你,都已經循環引用了,vc壓根就釋放不了,怎麼調dealloc方法?
在vc的disappear方法中
-(void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [_animationTimer invalidate]; }
這樣的確能解決問題,可是不一定是我們想要的呀,當我們vc 再push了一個新的頁面的時候,本身vc沒有釋放,按理說,其成員timer不應該被釋放呀,你可能會說,那還不容易,在appear方法中再重新生成一下呗…但是這樣的話,又要增加一個變量,標識定時器在上一次disappear時候是不是啟動了吧,是啟動了,被invaliate的時候,才能在appear中重新啟動吧。這樣是不是覺得很麻煩?
3.你可能會說,那簡單啊,直接若引用就可以了想想我們使用block的時候
@property (nonatomic, copy) void (^ myblock)(NSInteger i); __weak typeof (self) weakSelf = self; self.myblock = ^(NSInteger i){ [weakSelf view]; };
在其中,我們需要在block中引用self,如果直接引用,也是循環引用了,采用先定義一個weak變量,然後在block中引用weak對象,避免循環引用 你會直接想到如下的方式
__weak typeof (self) wself = self; self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:wself selector:@selector(animationTimerDidFired:) userInfo:nil repeats:YES];
是不是瞬間覺得完美了,呵呵,我只能說少年,你沒理解兩者之間的區別。在block中,block是對變量進行捕獲,意思是對使用到的變量進行拷貝操作,注意是拷貝的不是對象,而是變量自身。拿上面的來說,block中只是對變量wself拷貝了一份,也就是說,block中也定義了一個weak對象,相當於,在block的內存區域中,定義了一個__weak blockWeak對象,然後執行了blockWeak = wself;注意到了沒,這裡並沒有引起對象的持有量的變化,所以沒有問題,再看timer的方式,雖然你是將wself傳入了timer的構造方法中,我們可以查看NSTimer的
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats1
定義,其target的說明The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated,是要強應用這個變量的 也就是說,大概是這樣的,__strong strongSelf = wself 強引用了一個弱應用的變量,結果還是強引用,也就是說strongSelf持有了wself所指向的對象(也即是self所只有的對象),這和你直接傳self進來是一樣的效果,並不能達到解除強引用的作用!看來只能換個思路了,我直接生成一個臨時對象,讓Timer強用用這個臨時對象,在這個臨時對象中弱引用self,可以了吧。
4.考慮引入一個對象,在這個對象中弱引用self,然後將這個對象傳遞給timer的構建方法 這裡可以參考YYWeakProxy建立這個對象:
@interface YYWeakProxy : NSProxy @property (nonatomic, weak, readonly) id target; - (instancetype)initWithTarget:(id)target; + (instancetype)proxyWithTarget:(id)target; @end @implementation YYWeakProxy - (instancetype)initWithTarget:(id)target { _target = target; return self; } + (instancetype)proxyWithTarget:(id)target { return [[YYWeakProxy alloc] initWithTarget:target]; } //當不能識別方法時候,就會調用這個方法,在這個方法中,我們可以將不能識別的傳遞給其它對象處理 //由於這裡對所有的不能處理的都傳遞給_target了,所以methodSignatureForSelector和forwardInvocation不可能被執行的,所以不用再重載了吧 //其實還是需要重載methodSignatureForSelector和forwardInvocation的,為什麼呢?因為_target是弱引用的,所以當_target可能釋放了,當它被釋放了的情況下,那麼forwardingTargetForSelector就是返回nil了.然後methodSignatureForSelector和forwardInvocation沒實現的話,就直接crash了!!! //這也是為什麼這兩個方法中隨便寫的!!! - (id)forwardingTargetForSelector:(SEL)selector { return _target; } - (void)forwardInvocation:(NSInvocation *)invocation { void *null = NULL; [invocation setReturnValue:&null]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [NSObject instanceMethodSignatureForSelector:@selector(init)]; } - (BOOL)respondsToSelector:(SEL)aSelector { return [_target respondsToSelector:aSelector]; } - (BOOL)isEqual:(id)object { return [_target isEqual:object]; } - (NSUInteger)hash { return [_target hash]; } - (Class)superclass { return [_target superclass]; } - (Class)class { return [_target class]; } - (BOOL)isKindOfClass:(Class)aClass { return [_target isKindOfClass:aClass]; } - (BOOL)isMemberOfClass:(Class)aClass { return [_target isMemberOfClass:aClass]; } - (BOOL)conformsToProtocol:(Protocol *)aProtocol { return [_target conformsToProtocol:aProtocol]; } - (BOOL)isProxy { return YES; } - (NSString *)description { return [_target description]; } - (NSString *)debugDescription { return [_target debugDescription]; } @end
使用的時候,將原來的替換為:
self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:[YYWeakProxy proxyWithTarget:self ] selector:@selector(animationTimerDidFired:) userInfo:nil repeats:YES];
5.block方式來解決循環引用
@interface NSTimer (XXBlocksSupport) + (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats; @end @implementation NSTimer (XXBlocksSupport) + (NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats { return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(xx_blockInvoke:) userInfo:[block copy] repeats:repeats]; } + (void)xx_blockInvoke:(NSTimer *)timer { void (^block)() = timer.userinfo; if(block) { block(); } } @end
注意:以上NSTimer的target是NSTimer類對象,類對象本身是個單利,此處雖然也是循環引用,但是由於類對象不需要回收,所以沒有問題。但是這種方式要注意block的間接循環引用,當然了,解決block的間接循環引用很簡單,定義一個weak變量,在block中使用weak變量即可。
參考文檔
NSTimer循環引用不釋放問題
使用block解決NSTimer循環引用
weak NSTimer
弱引用NSTimer對象