iOS开发之RunLoop-赵鹏举

Posted tarena_3G

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发之RunLoop-赵鹏举相关的知识,希望对你有一定的参考价值。

1. RunLoop 概念

当我们打开 iPhone 手机进入一款 APP 时,这款 APP 会一直在当前屏幕运行,直到我们 kill 进程或切换到后台。联想到我们刚学习 C 语言时写的一些 C 程序,运行结束后会显示结果,程序就退出了。为什么 iPhone 上的 APP 会一直响应用户的请求?背后的机制是如何实现的呢?

这个小节从 What-How-Why 三个方面解释一下,RunLoop 是什么?是如何运行的?为何采用这种设计?

RunLoop是事件驱动的一个大循环,从 APP 启动开始进入循环直到 APP 退出。它是与线程相关的底层架构,一个run loop 就是一个事件处理循环,我们用它来安排任务和协调事件。runloop 的目的是为了让有任务的时候保持线程忙碌,无任务时让线程休眠。

RunLoop不是一直处于唤醒状态,它有三种状态:

  • 睡眠
  • 唤醒
  • 处理事件

这三种状态的执行顺序是 『睡眠-唤醒-处理事件-睡眠-...』。当有事件需要处理时,RunLoop 被唤醒开始处理事件,事件处理结束后,RunLoop 重新进入睡眠状态。

因为一个线程一次只能执行一个任务,执行完成后线程就会退出。如果需要线程一直可以响应不退出, EventLoop 模型(事件循环)可以满足这样的需求。使用事件循环,需要考虑这样几个问题:

  • 如何管理事件、消息?
  • 如何在无任务处理时避免资源占用?
  • 如何在有任务时被唤醒?

RunLoop对象负责管理事件和消息,并提供一个函数(名副其实—— runloop 内部是一个 do-while 循环)来进行Event Loop,该函数负责处理上述的问题2\\3,当线程执行该函数后,会一直处于『睡眠-唤醒-处理事件-睡眠-...』的状态循环中,直到循环结束,函数返回。

ios 中是通过CFRunLoop实现的,CFRunLoop 是CoreFoundation框架中的 C 语言API,是线程安全的。在CFRunLoop的基础上封装了NSRunLoop,NSRunLoop是面向对象的,但不是线程安全的。

CFRunLoop 的开源代码可下载,查看 CFRunLoop.c.

不需要去创建 runloop 对象,每个线程(包括主线程)都有一个相关联的 runloop 对象。APP 启动时就会在主线程自动创建和运行一个 runloop。

哪些事件会触发 RunLoop 的状态变化呢? 有以下6种:

  • Observer事件,runloop 状态变化时通知。
    static void CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION();

  • Block事件非延迟的NSObject PerformSelector\\dispatch_after, 和 block 回调。
    static void CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK();

  • main dispatch queue事件:一次RunLoop有两个机会执行GCD dispatch main queue中的任务,分别在休眠前和被唤醒后。
    static void CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE();

  • timer事件延迟的 NSObject PerformSelector\\dispatch_after\\timer。
    static void CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION();

  • source0事件:UIEvent\\CFSocket
    static void CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION();

  • source1事件:系统内核 mach_msg、CADisplayLink 事件。
    static void CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION();

上面的这些方法名可以通过打断点查看调用栈信息看到。


RunLoop 内部逻辑

2. RunLoop 剖析

runloop 有两个事件源:input sources(输入源) 传送异步事件,通常是来自其他线程或其他应用的信息。Timer sources (定时源)传送同步事件,定时或在某个时间间隔重复发生。

input source 给相应的处理方法发送异步事件,并导致 runUntileDate:方法退出。Timer sources 发送事件给它的处理方法,但不会导致 runloop 退出。


runloop

CF 框架中关于 RunLoop几个概念的对应关系:


RunLoop 概念 E-R图

2.1 RunLoop Mode

Run loop模式是一个集合,其中包括所有要监视的输入源和定时源以及要通知的注册观察者。mode 和源是相对应的,一个 runloop 运行时需要指定一个 mode,与该 Mode 对应的源在本次 runloop 可被监测;非对应的源处于暂停状态,等待与其匹配的 mode 的 runloop 启动时才可被监测。换言之,一次 runloop 只关注一类感兴趣的源。

cocoa和 CF 框架定义了一些默认的通用 mode,是字符串形式:

  • default :用于启动 runloop 并配置输入源。
  • reply:NSConnection, 通常不会自己配置这个 mode.
  • modal panel: modal
  • event tracking:用户交互事件
  • common modes:是一个数组,可以配置很多 mode 进去,默认包含 除 replay 之外的其他 mode.

cocoa\\CF RunLoop Mode


CFRunLoop与线程是一对一的关系,每条线程中可开启一个 runloop,但 runloop 是可以嵌套的(一个 runloop 中嵌套一个)。CFRunLoop 与 CFRunLoopMode 是一对多的关系,一个 runloop 可以有多种 mode。

2.2 Input Sources(输入源)

2.3 Timer Sources(定时源)

2.4 RunLoop Observer(观察者)

2.5 消息的 runloop 顺序

3. 何时使用 RunLoop?

只有在创建次线程时才需要运行 runloop,对于主线程会自动创建并运行。用次线程进行一个耗时很长的任务时不需要启动 runloop,只有在需要与线程进行交互的时候才启动:

  • 使用端口或自定义输入源和其他线程通信
  • 使用定时器
  • cocoa中使用任何performSelector
  • 使线程履行周期性任务

当需要在次线程中使用 RunLoop时,需要创建 RunLoop对象并进行配置。

4. 使用 RunLoop 对象

4.1 获取 RunLoop 对象

  • cocoa:[NSRunLoop currentRunLoop]
  • 使用CFRunLoopGetCurrent函数

4.2 配置

在次线程启动run loop前,你必须至少添加一类。因为如果run loop没有任何源需要监视的话,它会在你启动之际立马退出。

5. 设置 RunLoop 源


6. RunLoop 在 iOS 中的应用

6.1 系统中的应用

  • autoreleasepool
    自动释放池中的对象在当前 runloop 进入睡眠之前统一释放。

  • 事件响应

  • 手势识别
  • 界面更新
  • 定时器
  • performSelector

runloop mode
滑动列表时,default-tracking-default

6.2 其他应用

  • AFNetworking源码(待研究)
  • TableView 延迟加载图片
UIImage *img = ...;
[self.xxxImageView performSelector:@selector(setImg:) withObject:obj afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

因为 default 类型的 Mode的一次 runloop 不接收滑动事件(见2.1中的图,UIEvent对应的mode 是 tracking),因此在 tableview 滑动时,不加载图片。

  • crash 后重启 runloop (待尝试)
CFRunLoopRef runloop = CFRunLoopGetCurrent();
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runloop));
while(1)
  for(NSString *mode in allModes) 
    CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
  

7. 一些面试题

考察对 mode 的理解:
1)为何NSTimer在界面滚动时无响应?
当用户触摸界面时,主线程的run loop不再对timer事件进行处理。解决办法如下:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

2)NSURLConnection或NSStream指定RunLoop Mode的原因?
如果是在主线程,那么在滚动ScrollView或者TableView时,主线程的Run Loop会运行在UITrackingRunLoopMode模式,那么NSURLConnection或者NSStream的回调就无法运行,设置为NSRunLoopCommonModes,都可以保证NSURLConnection或者NSStream的回调可以被调用。

8. 一些坑

1)创建NSTimer
在线程中添加Timer时,肯定需要先生成Timer对象啦,有类方法也有实例方法,如果是使用scheduledTimerWithXXX 接口生成的Timer对象,会自动添加到当前线程的NSDefaultRunLoopMode中;如果是其他接口生成的Timer对象,则需要用 -addTimer:forMode添加Timer,这样做的好处是可以指定添加Timer的Run Loop以及模式。

2) timer 事件不会导致 runloop 退出,需要删除。
如果Run Loop中添加的是Timer而没有其他Input Source,而这个Timer只运行一次,那么Timer事件触发后Timer事件源就会从Run Loop删除,那么再运行Run Loop就会立刻返回;同时Timer事件触发是不会让Run Loop返回的,即使使用CF层的CFRunLoopRef运行接口 SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);运行Run Loop,其第三个参数为YES,Timer事件触发仍然不会导致当前Run Loop的运行返回。

3)运行 NSRunLoop 的接口选择。
如果是使用NSRunLoop,有三个运行的接口:

//运行 NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制 
- (void)run; 

//运行 NSRunLoop: 参数为运行模式、时间期限,返回值为YES表示是处理事件后返回的,NO表示是超时或者停止运行导致返回的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; 

//运行 NSRunLoop: 参数为运时间期限,运行模式为默认的NSDefaultRunLoopMode模式 
- (void)runUntilDate:(NSDate *)limitDate;

建议是使用第二个接口来运行,因为它能够设置Run Loop的运行参数最多,而且最重要的是可以使用CFRunLoopStop(runLoopRef)来停止Run Loop的运行,而第一个和第三个接口无法使用CFRunLoopStop(runLoopRef)来停止Run Loop的运行。

4)NSURLConnection
在使用NSURLConnection或者NSStream时,需要考虑到Run Loop的问题,默认情况下这两个对象生成后都是运行在当前线程的NSDefaultRunLoopMode模式的,如果是在主线程,那么在滚动ScrollView或者TableView时,主线程的Run Loop会运行在UITrackingRunLoopMode模式,那么NSURLConnection或者NSStream的回调就无法运行。因此最好是指定NSURLConnection或NSStream在Run Loop中的运行模式,两者有相同的接口 - (void)scheduleInRunLoop:( NSRunLoop )aRunLoop forMode:(NSString )mode;来设置NSURLConnection和NSStream的Run Loop以及模式,而且设置的Mode要设置为NSRunLoopCommonModes,因为NSRunLoopCommonModes默认会包含NSDefaultRunLoopMode和UITrackingRunLoopMode,这样无论Run Loop运行在哪个模式,都可以保证NSURLConnection或者NSStream的回调可以被调用。另外如果是在子线程中你设置了自定义的Run Loop模式,还可以用接口 CFRunLoopAddCommonMode(runLoopRef, mode)添加到NSRunLoopCommonModes。
不过NSURLConnection的使用有点特殊,必须使用它的designated initializer - (id)initWithRequest:(NSURLRequest )request delegate:(id)delegate startImmediately:(BOOL)startImmediately生成NSURLConnection对象,而且第三个参数是否立刻启动NSURLConnection要设置为NO,之后再用 - (void)scheduleInRunLoop:( NSRunLoop )aRunLoop forMode:(NSString )mode;设置Run Loop与模式,再调用 [NSURLConnectionObject start]启动。如果是其他接口生成NSURLConnection,用 - (void)scheduleInRunLoop:( NSRunLoop )aRunLoop forMode:(NSString *)mode;设置Run Loop和mode都不会起作用。

以上是关于iOS开发之RunLoop-赵鹏举的主要内容,如果未能解决你的问题,请参考以下文章

runtime学习总结-赵鹏举

iOS开发之RunLoop--转

李洪强iOS开发之RunLoop的原理和核心机制

UIStackView的使用-赵鹏举

UIStackView的使用-赵鹏举

runloop应用之iOS线程保活