1. 程式人生 > >NSTimer和實現弱引用的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];

當使用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];

則,無論你滑動不滑動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];
    }
}

當然了,因為是開啟的新的執行緒,在定時器的回撥方法中,需要切換到主執行緒才能操作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函式開始它

其中的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");
    });

另一種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方法!!

  2. 在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)repeats

    定義,其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變數即可

參考文件