美文网首页iOS开发攻城狮的集散地乔帮主的遗产
重新认识了NSTimer以及他与RunLoop关系

重新认识了NSTimer以及他与RunLoop关系

作者: 羊羊养 | 来源:发表于2016-08-08 16:02 被阅读797次

    已经将近两年没有写过文章了,之前记录的知识点都在有道笔记上,看到网上那么多人分享知识,突然也想重新写了,分享知识能够使自己学到更多.
    最近在查阅iOS中RunLoop资料时无意间看到了NSTimer与RunLoop的关系,于是开始去了解NSTimer,发现之前对NSTimer的运用只是把代码写上了,并没有深入去了解他里面存在的问题.通过查看资料,以及自己写代码测试,现将学到的知识总结一下,里面有认识理解不正确的欢迎指正

    一.NSTimer创建方法
    我个人认为NSTimer的创建可以分为三种
    1.scheduledTimer创建

    1),scheduledTimerWithTimeInterval: invocation: repeats:
    2),scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:
    

    2.timerWithTimeInterval类方法

    1),timerWithTimeInterval: target: selector: userInfo: repeats:
    2),timerWithTimeInterval: invocation: repeats:
    

    3.init创建

    initWithFireDate: interval: target: selector: userInfo: repeats:
    

    那么他们有什么区别呢?
    大部分人习惯使用方法1,简单直接,有的人习惯性的设置一个全局变量,在viewWillDisappear:或者viewDidDisappear:方法中写上

    if(self.testTimer.isValid) {
    [self.testTimerinvalidate];
    }
    self.testTimer=nil;
    

    并没有想过为什么,而大多数情况,我们设置Timer也是在主线程中,也并未出现过Timer设置完后无效的情况,所以都没有去深入研究过.
    接下来我们一个问题一个问题的说:
    其实NSTimer和Runloop有着密不可分的关系(这里不是讲Runloop的,而我也并没有对runloop了解特别深入,所以不多说),大部分人直接使用scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:方法创建Timer,只要创建好,就可以直接执行Timer的触发事件,因为这个方法系统会默认为我们添加到Runloop的NSDefaultRunLoopMode中,通过代码用各种方法创建Timer测试

    - (void)viewDidLoad {
    [superviewDidLoad];
    self.view.backgroundColor= [UIColorwhiteColor];
    [selfcreateView];
    [self initTestTimerWithMethod:0 repeats:YES];
    //[self createCustomTimer];
    //[self createThread];
    }
    #pragma mark - NSTimer
    //创建NSTimer
    - (void)initTestTimerWithMethod:(int)method repeats:(BOOL)repeat {
    switch(method) {
    case0://scheduledTimerWithTimeInterval:方法创建
    {
    //会自动执行,并且自动加入当前线程的Run Loop中其mode为:NSDefaultRunLoopMode
    self.testTimer= [NSTimerscheduledTimerWithTimeInterval:2target:selfselector:@selector(timerAction1)userInfo:nilrepeats:repeat];
    }
    break;
    default:
    break;
    }
    }
    - (void)timerAction1 {
    NSLog(@"scheduledTimer方法%@",@"执行Timer事件");
    }
    

    点击按钮,我们会发现控制台输出如下

    1.png

    接下来用另外两种方法创建,以上代码不再重复,直接写case: 内容

    case1://timerWithTimeInterval:方法创建
    {
    //需要手动加入主循环池中
    self.testTimer= [NSTimertimerWithTimeInterval:2target:selfselector:@selector(timerAction2)userInfo:nilrepeats:repeat];
    }
    break;
    case2://initWithFireDate:方法创建
    {
    //init方法需要手动加入循环池,它会在设定的启动时间启动
    self.testTimer= [[NSTimeralloc]initWithFireDate:[NSDatedateWithTimeIntervalSinceNow:5]interval:1target:selfselector:@selector(timerAction3)userInfo:nilrepeats:repeat];
    }
    break;
    
    //Timer执行方法
    - (void)timerAction2 {
    NSLog(@"timerWithTimeInterval:方法%@",@"执行Timer事件");
    }
    - (void)timerAction3 {
    NSLog(@"initWithFireDate:方法%@",@"执行Timer事件");
    }
    

    当我们把viewDidLoad方法中调用的initTestTimerWithMethod: repeats:方法参数改为1时,点击按钮,会发现控制台没有任何输出,将参数改为2同样控制台没有任何输出,查阅官方文档发现,原来这两种方法创建的Timer,不会自动添加到Runloop中,需要我们手动添加到当前的Runloop中才会执行,也就是说明Timer与Runloop有着密不可分的关系,于是修改代码

    case1://timerWithTimeInterval:方法创建
    {
    //需要手动加入主循环池中
    self.testTimer= [NSTimertimerWithTimeInterval:2target:selfselector:@selector(timerAction2)userInfo:nilrepeats:repeat];
    [[NSRunLoop currentRunLoop]addTimer:self.testTimerforMode:NSDefaultRunLoopMode];
    }
    break;
    case2://initWithFireDate:方法创建
    {
    //init方法需要手动加入循环池,它会在设定的启动时间启动
    self.testTimer= [[NSTimeralloc]initWithFireDate:[NSDatedateWithTimeIntervalSinceNow:5]interval:1target:selfselector:@selector(timerAction3)userInfo:nilrepeats:repeat];
    [[NSRunLoopcurrentRunLoop]addTimer:self.testTimerforMode:NSDefaultRunLoopMode];
    }
    break;
    

    修改后把viewDidLoad方法中调用的initTestTimerWithMethod: repeats:方法参数分别改为1和2依次运行代码,会发现控制台有输出

    每个线程都对应一个Runloop,而主线程的Runloop默认是开启的,子线程的Runloop默认不是开启的.通常情况我们的Timer是在主线程中创建的,但是也不乏有的时候是在子线程中创建的,前段时间我就遇到了问题,我们公司是做软硬件的,产品智能音箱需要联网,其中一种联网方法是热点联网,用到了TCP,UDP,通常我们是要另开线程创建Socket的,Socket连接以及数据发送等在任何一个过程中都有可能失败,这里不详细说明,我们的需求是在建立TCP,UDP连接,发送数据以及联网过程加入定时器设置总超时时间,测试测出bug,在一个特定条件下,APP界面的联网提示一直不消失,当时花了很长时间解决这个bug,因为联网过程分了很多步,在任何一步都可能失败,最终发现只要在那个特定的一步出错导致联网失败都会出现这个bug,找了很久才发现是因为Timer设置的执行方法没有执行,但是也想不明白为什么没有执行,在网上查阅资料才知道,原来在子线程创建Timer需要加入Runloop中并开启Runloop,不妨测试一下

    case3:
    {
    [self createThread];
    }
    break;
    
    //多线程创建Timer
    - (void)createThread {
    //NSLog(@"主线程%@", [NSThread currentThread]);
    //创建并执行新的线程
    NSThread*thread = [[NSThreadalloc]initWithTarget:selfselector:@selector(createTimerWithThread)object:nil];
    [threadstart];
    }
    - (void)createTimerWithThread {
    //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
    self.threadTimer= [NSTimerscheduledTimerWithTimeInterval:2.0target:selfselector:@selector(threadTimerAction)userInfo:nilrepeats:YES];
    //开始执行子线程的Run Loop
    [[NSRunLoopcurrentRunLoop]run];
    }
    //子线程中timer的回调方法
    - (void)threadTimerAction {
    NSLog(@"子线程中创建Timer %@",@"执行Timer事件");
    }
    

    我们先把[[NSRunLoopcurrentRunLoop]run];这行代码注释掉,会发现控制台没有任何输出,但是添加上这行代码,Timer的执行事件会正常触发.所以要注意在子线程中创建Timer,一定要开始当前线程的Runloop.
    二,Timer正常执行后也会遇到的问题
    1.循环引用,内存泄露
    前面我们已经提到,通常我们会将Timer设置为全局变量,在界面将要消失或者消失的时候将Timer invalidate掉,这是为什么呢?下面我们就来探讨一下
    其实Timer会强引用自己的target对象的,而target对象也会对Timer强引用,不妨我们测试一下,还是上面的代码,我们在dealloc方法中打印

    - (void)dealloc {
    NSLog(@"dealloc");
    }
    

    这里补充一下,当前TestTimerViewController是由ViewController presen过来的第二个VC,点击关闭按钮,返回ViewController,presen进来的时候Timer触发.这时候会发现控制台输出了,点击关闭按钮返回主界面,dealloc方法并没有调用,而且无论是调用哪种方法创建的Timer都是没有调用dealloc方法,认真观察我们发现,以上调用的Timer都是重复执行的,即repeats的值为YES,那我们改为NO结果会怎么样呢?

    - (void)viewDidLoad {
    [superviewDidLoad];
    self.view.backgroundColor= [UIColorwhiteColor];
    [selfcreateView];
    [selfinitTestTimerWithMethod:2repeats:NO];
    }
    

    我们看控制台输出,结果是无论调用哪种方法,返回上一界面的时候都会发现调用dealloc方法了,但是重复执行的时候dealloc始终没有调用,这个时候怎么办呢?
    我们只需要在界面消失的时候将Timer invalidate

    - (void)viewWillDisappear:(BOOL)animated {
    [superviewWillDisappear:animated];
    //在invalidate之前最好先用isValid先判断是否还在线程中
    //将定时器从循环池中移除。
    if(self.testTimer.isValid) {
    [self.testTimerinvalidate];
    }
    self.testTimer=nil;
    }
    

    这时候再将repeates值修改为YES会看到返回界面的时候控制台输出了dealloc,即调用了dealloc方法, 但是还有一种情况,我们这里是TestTimerViewController强引用了_testTimer,那如果只是单单的创建一个临时变量的Timer的时候上面的现象还会发生吗? 不妨试一试

    - (void)viewDidLoad {
    [superviewDidLoad];
    self.view.backgroundColor= [UIColorwhiteColor];
    [selfcreateView];
    [selfcreateCustomTimer];
    }
    //无全局变量创建Timer
    - (void)createCustomTimer {
    [NSTimerscheduledTimerWithTimeInterval:1target:selfselector:@selector(customTimerAction)userInfo:nilrepeats:YES];
    }
    
    

    我们会看到答案是YES,当repeats:参数为YES的时候,返回时dealloc仍然不会调用,当repeats参数为NO时候,返回上一界面dealloc会调用
    总结: 我们在使用Timer的时候,只要创建了Timer,持有Timer的对象都会对Timer强引用,而Timer的target对象也会被Timer强引用,其实根本原因是Timer在isValid为YES的时候是强引用自己的target的对象,当界面回收的时候Timer持有VC,回收Timer时候要回收发现VC持有Timer,这样就造成循环引用. 但是当Timer的target触发事件是只有一次即repeats参数为NO时候,Timer会invalidate自身,这样VC也会回收,当Timer的target触发事件是重复的即repeats参数为YES的时候,Timer不会invalidate自身,需要我们自己手动invalidate,所以在使用NSTimer的时候最好用全局变量定义,界面消失的时候要将Timer invalidate掉,这样才会避免由于循环引用造成的内存泄露

    2,Timer中Runloop的mode
    我们有时在使用Timer的时候会发现他触发事件的时机不对,这就与Runloop相关了,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer.每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称为CurrentMode,Runloop的模式也分为几种:常见的是default和common modes模式以及event tracking模式(组件拖动输入源 UITrackingRunLoopModes 不处理定时事件),而connection模式(处理NSConnection事件,属于系统内部)用户基本不用.这里需要强调common modes模式:NSRunLoopCommonModes 这是一组可配置的通用模式。将input sources与该模式关联则同时也将input sources与该组中的其它模式进行了关联.每次运行一个run loop,你指定run loop的运行模式。当相应的模式传递给run loop时,只有与该模式对应的 input sources才被监控并允许run loop对事件进行处理(与此类似,也只有与该模式对应的observers才会被通知),针对不同的Mode系统有不同的处理策略和优先级,而default Mode是优先级比较低的,例如当我们在滑动屏幕的时候,其Runloop的mode会切换到event tracking模式,event tracking模式是不处理定时事件的,所以此时当我们的Timer添加的Runloop的模式是default的时候,Timer的事件是不执行的,只有滑动结束了,又重新切换到default模式时候Timer才会执行,而此时他会把之前这段时间的Timer的事件都一次性执行,因为为了避免这种情况发生,我们通常把他添加到Runloop中,设置模式为common modes.话不多说,看代码

    case0://scheduledTimerWithTimeInterval:方法创建
    {
    //会自动执行,并且自动加入当前线程的Run Loop中其mode为:NSDefaultRunLoopMode
    self.testTimer= [NSTimerscheduledTimerWithTimeInterval:2target:selfselector:@selector(timerAction1)userInfo:nilrepeats:repeat];
    [[NSRunLoopcurrentRunLoop]addTimer:self.testTimerforMode:NSRunLoopCommonModes];
    }
    break;
    
    //Timer执行方法
    - (void)timerAction1 {
    NSLog(@"scheduledTimer方法%@",@"执行Timer事件");
    NSLog(@"timerAction1 %@", [[NSRunLoopcurrentRunLoop]currentMode]);
    }
    - (void)scrollViewDidScroll:(UIScrollView*)scrollView {
    NSLog(@"滑动屏幕时%@", [[NSRunLoopcurrentRunLoop]currentMode]);
    }
    

    这里我创建的TestTimerViewController直接继承自UITableViewController,我设置了100行数据,可以自由滑动,将Timer添加的Runloop的mode设置为NSRunLoopCommonModes,滑动过程看控制台输出情况会发现Timer的触发事件仍然是每隔两秒执行一次

    但是若将其模式更改为defaultMode,则控制台输出如下,
    [图片上传中。。。(5)]

    我们会发现事件触发时间与我们设置的不同,
    同时你会发现在子线程创建的Timer默认添加到当前的的Runloop,其mode是default,但是当我们滑动屏幕的时候,并不会影响Timer的执行时间,因为他是在子线程中的Runloop中,而滑动事件是在主线程中的,这里就不再上代码了
    三,GCD定时
    相信用GCD定时器的人不太多,我也是之前在一个demo上看到这些代码后,才去搜索查看的,GCD定时不需要我们的管理内存释放,我们只需要写出想要执行的事件.
    1.只执行一次

    - (void)createGCDTimerSourceActionOnce {
    delayInSeconds=2.0;
    //参数1:开始执行的时间,参数2:延迟时间(单位是纳秒)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,delayInSeconds*NSEC_PER_SEC),dispatch_get_main_queue(), ^(void){
    //执行事件
    NSLog(@"GCD定时器只执行一次");
    });
    }
    

    2.重复执行

    - (void)createGCDTimerSourceActionRepeat {
    delayInSeconds=2.0;
    //创建Dispatch Source
    GCDTimerSource=dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0,0,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0));
    //设置Timer的参数
    //参数1: dispatch_source_t,参数2:开始执行的时间,参数3:执行时间间隔(单位是纳秒),参数4:时间精度(系统可以延时的时间间隔)
    //系统已预订了宏NSEC_PER_SEC,设置时间:间隔时间(单位秒)*NSEC_PER_SEC
    dispatch_source_set_timer(GCDTimerSource,DISPATCH_TIME_NOW,delayInSeconds*NSEC_PER_SEC,0.0);
    //设置Dispatch Source的事件回调
    dispatch_source_set_event_handler(GCDTimerSource, ^{
    //重复执行的事件
    NSLog(@"GCD定时器重复执行");
    });
    //dispatch_source默认是挂起的状态,通过dispatch_resume函数开启
    dispatch_resume(GCDTimerSource);
    }
    

    总结
    1.使用Timer的时候最好使用全局变量,在页面消失的时候将Timer invalidate掉,防止循环引用造成的内存泄露(当然了,是在repeats值为YES的时候)
    2.子线程中创建Timer要将其Runloop开启[[NSRunLoopcurrentRunLoop]run];否则会不执行Timer事件
    3.最好将Timer添加到Runloop的Mode设置为CommonModes

    最后,对于Runloop,我还了解的不够好,希望再多查资料,多运用,大家也可以多研究研究,上面有不对的地方还请提出宝贵意见

    相关文章

      网友评论

      • frankisbaby:楼主我认为你上边有一条有问题。"当界面回收的时候Timer持有VC,回收Timer时候要回收发现VC持有Timer,这样就造成循环引用. "因为当timer是局部变量的时候,vc并不持有timer。因此你的解释有问题。应该是:当前的runloop强引用了timer,timer强引用了vc,导致了vc无法释放。
      • ConnerLi:楼主,首先感谢这么辛苦的总结,但是能不能简化一下,篇幅有点长啊,简明扼要一下更好 :joy:
        ConnerLi:@怪小智 要保留一颗严谨的求知心 :smile:
        4de97f4b3a77:楼上是来砸场的吧
        ConnerLi:还有一点,应该留下demo,方便读者对比,以及自己调试,这样子更利于学习。 :smile:

      本文标题:重新认识了NSTimer以及他与RunLoop关系

      本文链接:https://www.haomeiwen.com/subject/chjrsttx.html