iOS 开发 - 深入理解 NSTimer 为什么要配合 NSRunLoop 进行使用

2017-01-14 10:46:22来源:http://www.jianshu.com/p/8b42e2992624作者:CoderGin人点击

第七城市
前言

相信大部分 iOS 开发者都知道 NSTimer,这个类能够帮助我们实现每隔一定时间定时调用某个方法的效果。而在使用 NSTimer 的时候我们一般都会与 NSRunLoop 相结合使用,那么为什么要配合 runLoop 进行使用,runLoop 又是什么?


下面我们来一起探讨一下 NSTimer 为什么要和 NSRunLoop 结合使用

首先来说点大家都应该知道的东西


1.NSTimer 如果使用的不恰当,很容易造成循环引用。


而为什么会发生循环引用呢?


使用 timer 发生循环引用,主要出现在下面一种情况:如果你让一个对象对 timer 进行持有,而实例化 timer 的时候使用下面代码,将 self 作为 target 进行实例化


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

这种做法如果没有特殊的处理,就会发生循环引用。因为 timer 为了保证传入的 target 中的 aSelector 方法,会被正确调用,就默默的帮我们对 target 进行了一次 retain。这样就造成了循环引用。


进入正题,首先来看看, timer 单独使用,不配合 runLoop 会发生什么问题

使用 timer 的时候,我们经常会将它加到 runLoop 中,虽然不加似乎也并没有什么问题。


下面我写了一个 demo 专门对上面的 3 进行探究,也就是探究不将 timer 加入 runLoop (不将 timer 配合 runLoop 进行使用)会有什么问题。



不将 timer 加入 runLoop 中.gif

如图所示,我在第一个界面点击 Present 跳转到第二个界面之后,有一个 label 自动开始使用 timer 进行倒计时,并且这个 timer 没有和 runLoop 进行配合使用,开始的时候似乎顺利,但是当我开始拖动下方的 textView 的时候,问题就出现了,timer 此时并没有在工作了。


先说解决方法,再进行解释。解决方法当然是将 timer 和 runLoop 进行配合使用。具体代码如下:


self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(updateTimer) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

第一句没什么好说的,需要注意的是循环引用的问题,上面已经提过。


这里重点解释第二句,首先来看看什么是 runLoop
这里我特意去查阅了下苹果官方文档对于 NSRunLoop 的描述


Your application cannot either create or explicitly manage NSRunLoop
objects. Each NSThread
object—including the application’s main thread—has an NSRunLoop
object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.


他的大致意思是:你所写的程序不能直接创建或者管理 NSRunLoop 的对象,任一线程包括程序的主线程,当你需要获取对应的 runLoop 对象的时候系统都会为之自动创建一个 runLoop 对象,(也就是,你不主动获取线程的 runLoop,线程就没有与之对应的 runLoop 对象),你可以通过 [NSRunLoop currentRunLoop] 来获取当前线程的 runLoop 对象。


那么 runLoop 究竟是什么


runLoop,直译:跑圈,也就是运行循环,一般来讲,一个线程一次只能执行一个任务,执行完成后,线程就会退出,但是有时候,我们需要一种机制让线程不进行退出,而是处于一种待命状态,随时处理操作而不退出。


runLoop 就是苹果为了这一个机制所设计的。


总的来说,runLoop 是一个管理线程的类,当线程的 runLoop 一旦开启,runLoop 便开始对线程进行管理工作。当线程中的任务执行完毕,线程本应该结束,但会因为 runLoop 的关系,处于休眠状态,不会退出,随时等待新的任务。


runLoop 和线程之间的关系


1.每一条线程都有唯一与之对应的 runLoop 对象。
2.runLoop 对象只在第一次获取时创建,在线程退出时销毁,并且你可以通过 currentRunLoop 获取当前线程的 runLoop,也可以在任意线程通过 mainRunLoop 获取主线程的 runLoop。


runLoop 的 层次结构


3.每个 runLoop 有若干个 Mode,每个 Mode 又包含着若干个 Source/Observer/Timer。
4.runLoop 一次只可以指定一种 Mode,如果要执行其他 Mode 中的事件,需要等当前 Mode 中的事件处理完,才能切换 Mode。
5.主线程的 runLoop 默认在启动时由系统帮我们创建,主线程中的任务执行完毕,将会由于 runLoop 的关系处于一种“随时待命的休眠”状态。子线程默认没有 runLoop,在子线程执行完所有任务之后(也就是所有 Mode 中都不存在 Source/Observer/Timer之后),runLoop 将会随着线程的退出而被销毁。



RunLoop 层次结构

说完上面的这些基本概念,剩下的就简单了。
如果忘记了上面的代码,这里再贴出来一次。


[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

这里使用 mainRunLoop 是为了获取主线程的 runLoop 对象,然后给 runLoop 的其中一个叫做 NSRunLoopCommonModes 的 Mode 添加一个 Timer。


timer 和滑动 textView 分别属于什么 Mode


timer 添加定时事件默认是被注册在 runLoop 的 NSDefaultRunLoopMode 中的。而滑动 textView 事件属于 UITrackingRunLoopMode(这个 Mode 属于 NSRunLoopCommonModes)。


timer 停止的原因


知道了,runLoop 一次只能处理一个 Mode 中的一个任务,要想执行其他 Mode 的任务,必须退出当前 Mode;也知道了,timer 和滑动 textView 分别处于什么 Mode,那么 timer 停止的原因也就显而易见了。在这个 demo 中,timer 停止工作是因为当界面跳转完成之后,主线程中的任务因为 timer 被默认注册在 NSDefaultRunLoopMode 中,此时手指在 textView 上进行了滑动,就造成了 mainRunLoop 退出了 NSDefaultRunLoopMode,然后进入 UITrackingRunLoopMode 中,处理用户的滑动 textView 操作。


所以,此处添加 timer 到 mainRunLoop 的 NSRunLoopCommonModes 中是为了给 timer 额外注册一种 Mode(不是取消原来的 Mode),这样,mainRunLoop 在两种 Mode 中,都能顺利的执行 timer。


下面来验证我上面所说的

我让 textView 的代理设置成了第二个界面,并且在滑动代理方法中,打印了当前线程的 runLoop 所处的 Mode


- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
NSLog(@"%@", [[NSRunLoop mainRunLoop] currentMode]);
}

然后,在 timer 的定时调用的方法中,也打印当前线程的 runLoop 所处的 Mode。


运行之后,我跳转到第二个界面之后,依然拖动 textView(这里将 timer 添加到了 mainRunLoop 的 NSRunLoopCommonModes 中。控制台打印结果如下图:



控制台打印结果

来解释一下这个控制台打印的信息
首先,我刚跳转到第二个界面的时候,timer 开始工作,执行的方法中打印了当前调用的方法和当前线程的 runLoop 对象的 Mode,我们可以看到是 KCFRunLoopDefaultMode(NSDefaultRunLoopMode),
然后,我开始拖动 textview,主线程退出 NSDefaultRunLoopMode,进入 UITrackingRunLoopMode(NSRunLoopCommonModes),timer 由于此时也被注册到了 mainRunLoop 的 NSRunLoopCommonModes 中,所以,timer 仍然工作,只是工作的 Mode 变成了和拖动 textView 一样的 UITrackingRunLoopMode,执行了几秒后,我松开了对 textView 的拖动,textView 的 scrollViewDelegate 响应,并打印当前拖动 textView 属于 UITrackingRunLoopMode,而后,timer 工作的 Mode 又变回了 KCFRunLoopDefaultMode。


总结

上面提到了相当多的 Mode,说的有点乱,稍微做下总结
1.timer 被停止的原因是因为 runLoop 一次只能执行一个 Mode 中的事件,要执行其他 Mode,必须退出当前 Mode,而 timer 和滑动 textView 同一 runLoop 中的不同 Mode。
2.解决方法是将 timer 注册到和滑动 textView 同一个 Mode 中,就可以让 timer 原先处于的 Mode 结束之后,在其他 Mode 仍然可以工作。
3.最好使用 mainRunLoop,添加 timer,虽然此处用两个都行,因为这里没有涉及到子线程,但是,最好要理解要在主线程的 runLoop 的 NSRunLoopCommonModes 中添加 timer,以防日后开发中,不小心进入其他线程而造成不必要的麻烦。
4.如果对 runLoop 的一些更详细的概念和底层实现有兴趣的,在这里我推荐两篇博客:##iOS开发-- RunLoop的基本概念与例子分析##深入理解RunLoop##
5.另外附上本片博客所使用的 demo 的 github 地址 ##CoderGin/NSTimerDemo##




第七城市

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台