iOS多线程之NSRunloop

1.简介

Runloop是与线程有关的基础框架的一部分,是用来规划事件处理的,当有任务的时候Runloop会让线程处理任务,当没有任务的时候Runloop会让线程处于休眠状态。
Runloop的管理不完全是自动的,我们必须在合适的时候开启Runloop和处理到达的事件,Cocoa和Core Foundation都提供了Runloop对象来配置和管理线程的Runloop。我们的应用程序不需要显示的创建这些对象,包括应用主线程在内的每一个线程都有一个相关的Runloop对象。而且只有第二线程是需要显示地运行Runloop,主线程是不需要的,APP把主线程Runloop的配置和运行作为了应用程序启动的一部分。
操作Runloop的两个接口: 1. NSRunLoop Class Reference 2. CFRunLoop Reference

2.Runloop解析

Runloop的功能就像他的名字听起来一样,使线程进入一个循环,当事件到达的时候调用事件处理方法。我们要自己提供实现Runloop实际循环运行的控制语句,话句话说就是我们要提供 while 或者 for 来驱动Runloop。在循环内部我们运行Runloop,当事件到达的时候调用事件处理器。
Runloop接收来自两种源的事件, Input sources 异步的分发通常是来自于同一个应用内的另一个线程的消息事件。 Timer sources 同步的分发在设定好的时间发生或者循环间断地发生的事件。这两种事件源都是使用应用指定的事件处理方法来处理到达的事件。

下面的图显示了Runloop和事件源的概念结构。Input sources异步的分发事件到响应的处理器,然后引起runUntilDate:(由线程相关的Runloop对象调用)方法退出。Timer sources同步分发事件到相应的处理器但是不会引起Runloop退出。


除了处理输入源的事件,Runloop也会生成Runloop行为的通知。注册Runloop的观察者可以收到这些消息,然后在线程内用他们做一些额外的处理。我们只能使用Core Foundation接口来注册线程的Runloop观察者。

2.1 Runloop运行模式

一种Runloop运行模式就是一个要监控的Input和Timer事件源的集合或者是一个要通知的Runloop观察者的集合。每次运行Runloop,都要指定一个运行模式(显示地或者隐式地)。在Runloop的运行期间,只有和当前运行模式相关的源才能被监控和允许发送事件。相似的,只有和当前运行模式相关的观察者才会被通知Runloop的行为。和其他模式相关的源会保留新的事件直到Runloop运行在了合适的模式才会分发。

在我们的代码中,我们可以通过字符串来标识模式。Cocoa和Core Foundation定义了一个默认模式和几个普通的有用的模式,这些模式都是用字符串来标识的。我们可以用一个字符串当做名字来自定义一个模式,虽然我们自定义模式的名字是随意的,但是模式的内容不是随意的,在我们自己创建的要用的模式中至少要添加一个Input源Timer源或者Runloop观察者

在Runloop的特殊阶段我们可是使用运行模式来过滤我们不想要的源的事件,大多数的情况下,Runloop都运行在系统提供的默认模式下,然而Model Panel可能运行在“模式”模式,当运行在这个模式期间,只有和这个模式相关的事件源才会发送事件到我们的线程。对于第二线程来说,我们通常使用自定义模式来阻止低优先级的事件源在其他关键处理的时间内发送事件。

注意:运行模式不是根据事件类型划分的,而是根据事件源划分的。我们不能通过模式来匹配鼠标按下事件或者键盘事件,但是我们可以用运行模式来监听一组不同的Port、暂时挂起Timers或者改变当前被监控的事件源和Runloop观察者。

下面列举了一些Cocoa和Core Foundation定义的标准模式:

  • NSDefaultRunLoopMode:默认的运行模式,用于大部分操作,除了NSConnection对象事件。
  • NSConnectionReplyMode:用来监控NSConnection对象的回复的,很少能够用到。
  • NSModalPanelRunLoopMode:用于标明和Mode Panel相关的事件。
  • NSEventTrackingRunLoopMode:用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动)。
  • NSRunLoopCommonModes:是一个模式集合,当绑定一个事件源到这个模式集合的时候就相当于绑定到了集合内的每一个模式。Cocoa应用默认包含Default、Panel、Event Tracking模式,Core Foundation只包含Default模式,我们可以通过CFRunLoopAddCommonMode添加模式。

2.2 Input Sources

Input Sources异步地分发事件到线程。大概有两种类型的Input Sources,Port-based类型的输入源监控着应用的Mach端口,自定义的输入源监控着自定义的事件源。NSRunloop不关心输入源的类型。两种输入源唯一的不同是输入源的触发方式,Port-based输入源是由系统内核触发的,而自定义的输入源要我们自己触发。创建输入源的时候我们就给给输入源添加指定的模式。下面是一些输入源:

2.2.1 Port-Based Sources

Cocoa和Core Foundation提供了类和接口用来创建Port-Based源,Cocoa只要创建NSPort对象,并添加到NSRunloop中就可以啦,NSPort负责输入源的创建和配置。Core Foundation需要手动的常见port和输入源。

2.2.2 Custom Input Sources

我们要用到CFRunLoopSourceRef函数创建输入源,并定义几个回调函数用于配置输入源、处理事件和删除输入源。事件的触发机制要我们自己定义。

2.2.3 Cocoa Perform Selector Sources

Cocoa定义了可以在任何线程上执行方法的事件源,在想要执行的线程上执行方法是顺序执行的,避免了多个方法在线程上执行的同步问题。Perform Selector Sources在方法执行完之后就会自己从NSRunloop中删除。

Perform Selector Sources要求目标线程的NSRunloop必须是运行的,主线程默认是运行的。NSRunloop在一次迭代过程中会处理所有的Perform Selector调用,而不是一次迭代处理一个Perform Selector调用。NSObject中定义的Perform Selector方法如下

  • performSelectorOnMainThread:withObject:waitUntilDone:
  • performSelectorOnMainThread:withObject:waitUntilDone:modes:
  • performSelector:onThread:withObject:waitUntilDone:
  • performSelector:onThread:withObject:waitUntilDone:modes:
  • performSelector:withObject:afterDelay:
  • performSelector:withObject:afterDelay:inModes:
  • cancelPreviousPerformRequestsWithTarget:
  • cancelPreviousPerformRequestsWithTarget:selector:object:
延迟执行是在NSRunloop的 下一次迭代 中过了指定的延迟事件才执行。取消操作是针对延迟执行方法的。

2.3 Timer Sources

Timer Sources同步地在将来的一个确定的时间分发事件到我们的线程。Timers可以让线程通知自己去处理一些事情。Timers不是一个实时的机制,当Timers触发的时候NSrunloop刚好正在执行处理函数,Timers会等待NSRunloop调用自己的处理函数。

Timers可以创建一次性的和重复性的事件,当创建重复性的事件的时候,Timers只会根据规划好的触发时间来重新规划触发时间,而不是根据确切的触发时间。而且由于延迟触发丢失了几次触发的话,Timers只会补充一次触发。

2.4 NSRunloop观察者

不像是事件源一样在事件触发的时候执行处理函数。NSRunloop观察者是在NSRunloop几个执行的特定的点触发。NSRunloop可以观察的几个事件是:

  • 进入NSRunloop
  • NSRunloop将要处理Timer事件
  • NSRunloop将要处理Input事件
  • NSRunloop将要进入睡眠
  • NSRunloop被唤醒,但是是在处理事件之前
  • 退出NSRunloop

创建观察者的方法是CFRunLoopObserverRef,我们可以通过Core Foundation方法添加到指定的NSRunloop。观察者也可以创建一次性的和重复性的。一次性的观察者触发之后就会从NSRunloop中删除。

2.5 NSRunloop事件顺序

NSRunloop所有处理事件和通知观察者的顺序如下:

  1. 通知观察者NSRunloop进入
  2. 如果有Timer即将触发时,通知观察者
  3. 如果有非Port的Input Sourc即将e触发时,通知观察者
  4. 触发非Port的Input Source事件源
  5. 如果基于Port的Input Source事件源即将触发时,立即处理该事件,跳转到步骤9
  6. 通知观察者当前线程将进入休眠状态
  7. 将线程进入休眠状态直到有以下事件发生:基于Port的Input Source被触发、Timer被触发、Run Loop运行时间到了过期时间、Run Loop被唤起
  8. 通知观察者线程将要被唤醒
  9. 处理被触发的事件:如果是用户自定义的Timer,处理Timer事件后重新启动Run Loop进入步骤2、如果线程被唤醒又没有到过期时间,则进入步骤2、如果是其他Input Source事件源有事件发生,直接处理这个事件
  10. 到达此步骤说明Run Loop运行时间到期,或者是非Timer的Input Source事件被处理后,Run Loop将要退出,退出前通知观察者线程已退出

3.什么时候使用NSRunloop

由于主线程的NSRunloop默认自动运行,所以只有第二线程才需要我们自己运行NSRunloop。并不是所有使用线程的情况都要运行NSRunloop,下面一些情况你需要运行NSRunloop:

  • 需要使用Port或者自定义Input Source与其他线程进行通讯。
  • 需要在线程中使用Timer。
  • 需要在线程上使用performSelector*****方法。
  • 需要让线程执行周期性的工作。
  • NSURLConnection在子线程中发起异步请求。

4.补充

4.1 线程安全性

基于Cocoa的接口不是线程安全的,基于Core Foundation的接口是线程安全的。

4.2 自定义输入源的原理图

iOS多线程之NSRunloop_第1张图片

5.NSRunloop的使用

具体关于NSRunloop中NSTimer、Runloop观察者等运行模式的使用参见hrchen的ExamplesForBlog例子。由于NSPortMessage是私有接口,所以没有实例。

6.参考链接

  • Threading Programming Guide
  • iOS多线程编程Part 1/3 - NSThread & Run Loop

你可能感兴趣的:(iOS)