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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容