iOS 深入理解NSTimer與RunLoop及內存泄漏問題

NSTimer平時用的很多,如果不是真正的懂它,會發生各種各樣的問題。如你滑動tableview的時候定時器不走了。或者是出現,內存不能釋放的問題。

NSTimer是基于RunLoop的

  • 先看看定時器的創建
 [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];

一般情況下我們都是用第一種方法。為什么不用第二種呢??因為第二種如果只是創建一個timer對象發現timeEvent不調用。看看下面的代碼

NSTimer *timer = [[NSTimer alloc]initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

發現定時器起作用了,這說明定時器是基于RunLoop的,如果沒有了RunLoop的驅動,定時器是不會執行的。
那么第一種為什么會執行呢?因為第一種在創建了定時器之后會把定時器加到RunLoop中,不用我們自己手動加。蘋果給這一步做了,所以定時器得以很好的玩耍。現在我們已經通過上面證實了NSTimer得加到RunLoop中才會執行。

讓NSTimer在滑動tableview的時候繼續工作

如果你在viewdidload創建了一個定時器,然后又創建了tableview。當你滑動tableview的時候定時器不走了。這又是為什么呢?
這個問題又跟RunLoop的幾個模式有關

Default mode(NSDefaultRunLoopMode)
默認模式中幾乎包含了所有輸入源(NSConnection除外),一般情況下應使用此模式。

Connection mode(NSConnectionReplyMode)
處理NSConnection對象相關事件,系統內部使用,用戶基本不會使用。

Modal mode(NSModalPanelRunLoopMode)
處理modal panels事件。

Event tracking mode(UITrackingRunLoopMode)
在拖動loop或其他user interface tracking loops時處于此種模式下,在此模式下會限制輸入事件的處理。例如,當手指按住UITableView拖動時就會處于此模式。

Common mode(NSRunLoopCommonModes)
這是一個偽模式,其為一組run loop mode的集合,將輸入源加入此模式意味著在Common Modes中包含的所有模式下都可以處理。在Cocoa應用程序中,默認情況下Common Modes包含default modes,modal modes,event Tracking modes.

剛才不是說蘋果給定時器加到RunLoop了嗎?蘋果加的是默認的模式NSDefaultRunLoopMode,當你滑動tableview的時候runloop將會切換到UITrackingRunLoopMode,切換了模式,當然不工作了。因為你是默認的不是托拽的。解決辦法就是蘋果給我們默認的模式,我們再修改下,改為NSRunLoopCommonModes模式。這樣所有模式都可以處理了。

  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

你這次滑動tableview的時候發現定時器還在執行。

多線程下創建定時器然后讓定時器執行

   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      
      NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
       [[NSRunLoop currentRunLoop] run];
       
   });

RunLoop用來循環處理響應事件,每個線程都有一個RunLoop,蘋果不允許自己創建RunLoop,scheduledTimerWithTimeInterval這個方法創建好NSTimer以后會自動將它添加到當前線程的RunLoop,但是只有主線程的RunLoop是默認打開的,而其他線程的RunLoop如果需要使用就必須手動打開,所以我們用了這句 [[NSRunLoop currentRunLoop] run];打開線程的NSRunLoop。定時器就開始工作了。

NSTimer容易導致內存泄漏。

為了保證參數的生命周期,NSTimer會對target對象做強引用,也就是retain一次。為什么要強引用你?因為如果target銷毀了,我定時器還怎么來做事?誰來替我調用timeEvent呢?所以它就強引用target,因為定時器是加到RunLoop中的,所以RunLoop強引用著NSTimer,一般情況下我們的target就是當前的控制器,如果我們想讓控制器如我所愿的銷毀了,首先得除掉NSTimer這個禍害。要不NSTimer強引用著self,self就銷毀不了。所以經常會因為不理解它導致我們的內存泄漏。

現在的唯一辦法就是先讓定時器死了。然后self也就釋放了。定時器唯一死的辦法就是

[self.timer invalidate];
 self.timer = nil;

當一個timer被schedule的時候,timer會持有target對象,NSRunLoop對象會持有timer。當invalidate被調用時,NSRunLoop對象會釋放對timer的持有,timer會釋放對target的持有。除此之外,我們沒有途徑可以釋放timer對target的持有。所以解決內存泄露就必須撤銷timer,若不撤銷,target對象將永遠無法釋放。

解決NSTimer的內存泄漏。

有的人會在viewWillDisappear里銷毀定時器。有的人會在返回按鈕里銷毀定時器。雖然能解決問題但是不太友好。

我們想如果target不是咱們的控制器,timer不就不強引用self了嗎?
如果timer不強引用self,那么self就可以盡情的釋放了。通過這個思路可以寫出一個自定義的YBTimer,通過一個YBTimerTarget作為timer 和self的紐帶,timer強引用YBTimerTarget,YBTimerTarget和self是弱引用,這樣我們就不會造成循環引用了。而且可以在dealloc里銷毀定時器

- (void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}
YBTimer實現
#import <Foundation/Foundation.h>

typedef void (^YBTimerBlock)(id userInfo);

@interface YBTimer : NSObject

+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(id)userInfo
                                     repeats:(BOOL)repeats;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      block:(YBTimerBlock)block
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats;

@end

#import "YBTimer.h"

//------------------------------YBTimerTargetBegin-------------------------------------


@interface YBTimerTarget : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;

@end

@implementation YBTimerTarget

- (void)timeAction:(NSTimer *)timer {
    if(self.target) {

        [self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];

    } else {
        [self.timer invalidate];
    }
}

@end


//------------------------------YBTimerTargetEnd-------------------------------------

@implementation YBTimer

+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(id)userInfo
                                     repeats:(BOOL)repeats {
    YBTimerTarget* timerTarget = [[YBTimerTarget alloc] init];
    timerTarget.target = aTarget;
    timerTarget.selector = aSelector;
    timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                         target:timerTarget
                                                       selector:@selector(timeAction:)
                                                       userInfo:userInfo
                                                        repeats:repeats];
    return timerTarget.timer;
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      block:(YBTimerBlock)block
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats {
    NSMutableArray *userInfoArray = [NSMutableArray arrayWithObject:[block copy]];
    if (userInfo != nil) {
        [userInfoArray addObject:userInfo];
    }
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(timerBlock:)
                                       userInfo:[userInfoArray copy]
                                        repeats:repeats];
}

+ (void)timerBlock:(NSArray*)userInfo {
    YBTimerBlock block = userInfo[0];
    id info = nil;
    if (userInfo.count == 2) {
        info = userInfo[1];
    }
    
    if (block) {
        block(info);
    }
}

@end

demo 地址 https://github.com/yinbowang/YBTimerDemo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容