Runloop就如同它名字一样,作为线程入口用来响应传入事件。我们需要提供Runloop实际循环部分的代码,意思就是驱动Runloop的while循环或者for循环代码。在循环中使用Runloop对象来“运行”事件处理逻辑,接收事件并调用已经初始化的处理程序。

Runloop接收两种不同类型的事件源(Source)。输入源(Input Source)用于传输异步事件,通常是来自另一个线程或者其他程序的消息。定时器源(Timer Source)提供同步事件,预定的时间或者固定的时间间隔重复执行。当两种类型的source出现时,程序使用特定处理程序来处理它们。

下图展示了Runloop和各种Source的关系。输入源(Input Source)将异步事件传递到相应的处理程序中,运行runUntilDate:(方法调用与线程相关联的NSRunloop对象)退出。定时器源(Timer Source)传递事件到它的处理事务中,但是并不会导致Runloop退出。

用我自己的话来解释一下上面这张图:首先用户在输入源中,通常来说就是cocoa的主线程中调用performSelector:onThread...方法。然后进入该Thread中,从该线程的开始到结束中间经历了一个while循环。在循环中处理相应的事务。最后处理完事务,while循环退出,runUntilDate:之后线程销毁。

除了处理输入源之外,Runloop还会生成关于循环的通知(Notification),注册Runloop的Observer会接收到这些通知(可用于线程相关操作中),线程的Runloop Observer用Core Foundation相关方法初始化。

下面会介绍有关Runloop Mode,以及在不同操作中的Mode。也会介绍在事件处理过程中不同时刻发出的通知。

3.1.1 Runloop Mode

Runloop Mode是监测输入源和定时器源的集合,同时也包含Runloop Observer。每次运行Runloop时,都需要显示或隐示的指定特定的’Mode‘。在Runloop运行期间,它只会监测与指定Mode相关联的source并传递相应的事件(同理,只有与该模式关联的Observer才会知道Runloop的进度)。

在代码中通过名称(name)来指定Mode,在Cocoa和Core Foundation中定义了默认的Mode和几种常用的Mode,还定义了相关Mode的字符串常量(比如NSDefaultRunLoopModekCFRunLoopDefaultMode等等)。同时我们可以用指定字符串作为名称来自定义Mode。尽管在自定义Mode时是使用的任意的字符串,但Mode的内容不是任意的。我们必须确保创建的Mode中有一个或多个输入源,定时器源或者Observer(只有这样才能使用这个Mode)。

下面用一个网上看到的图来总结一下他们之间的关系(图片来自这里

但是用我自己结合车间工作机制的理解来做的图是

这张图的主要意义在于:Observer和TimerSource/InputSource是要依据于不同的Mode的。

在Runloop中使用Mode通过特殊条件来过滤掉不需要的Source事件,Runloop在多数情况下是在系统默认的Mode中运行。模态面板(Modal Panel 下文表格中会提到)可能在”模态“Mode下运行,在这个Mode下,只有与模态面板相关的的Source才会把事件传递到线程。对于次级线程,可以使用自定义Mode防止低优先级的Source在时间紧急时传递事件。

Mode是基于事件的Source而不是事件的类型来区分(例如,不能使用Mode去匹配鼠标点击或者键盘事件)。我们可以使用Mode去监听不同的端口(Port),临时挂起的Timers,或者更改当前正在监听的Source和Runloop Observer。

下表列出了由Cocoa和Core Foundation定义的标准Mode。

Mode Name 描述
Default(默认) NSDefaultRunLoopMode(Cocoa),kCFRunLoopDefaultMode(Core Foundation) 大多数操作大多数时候使用default mode来启动Runloop,以及配置输入源。
Conection(mac OS) NSConnectionReplyMode(Cocoa) 该Mode联合NSConnection(mac OS)对象来监听事件响应。
Modal(模态,适用于mac OS) NSModalPanelRunLoopMode (Cocoa) 模态面板
Event Tracking(跟踪事件) NSEventTrackingRunLoopMode (Cocoa) 用户交互事件(我们开发中用到的ScrollView滑动事件)
Common Modes NSRunLoopCommonModes (Cocoa),kCFRunLoopCommonModes (Core Foundation) 这是一组Mode,Cocoa中通常包含Default和Event Tracking Mode。在Core Foundation中只包括Default Mode,但是可以使用CFRunLoopAddCommonMode函数将自定义的Mode添加到Common Modes中。

3.1.2 输入源

输入源使用异步的方式传递事件到对应的线程,事件的来源取决于输入源的类型。其包含Port-based和Custom这两种类型的Source,基于端口输入源(Port-based Source)监视程序的Mach Ports。自定义输入源(Custom Input Source)来监视自定义源发出的事件。就Runloop而言,input source是Port-based还是Custom的并不重要,系统通常都会实现这两种输入源。

Port-based Source由内核自动发出信号,而Custom input Sources必须从其他线程手动的发送信号

当你创建一个Input Source(输入源)的时候,可以将它放到一个或者多个Runloop mode中。如果该输入源不在当前被监视的Mode中,该输入源发出的事件就不会被Runloop监控(直达Runloop以正确的Mode运行)。

3.1.2.1 Port-based Source(基于端口源)

Cocoa和Core Foundation提供了创建Port-based输入源相关的对象和函数。

在Cocoa中,不需要明确的创建输入源,我们可以使用NSPort相关的方法来创建一个Port对象,并将该对象添加到Runloop中。该Port对象会负责创建和配置输入源。

但是在Core Foundation中,我们需要手动的创建Port和它的Runloop源。使用CFMachPortRef, CFMessagePortRef, 或者CFSocketRef函数来创建适当地对象。

相关的例子在配置Runloop源中

3.1.2.2 Custom input Source(自定义输入源)

使用CoreFoundation中CFRunLoopSourceRef类型相关的函数来创建自定义输入源(CFRunLoopSourceCreate),通过添加多个回调函数来配置输入源(在CFRunLoopSourceCreate中最后一个参数是CFRunLoopSourceContext结构,该结构最后三个成员是三个函数指针schedule,cancel,perform,也就是这里提到的回调函数,这个在后面配置自定义输入源时会提供相关的代码)。CoreFoundation在不同时间点来调用前面说到的三个函数,处理传入事件,当source从Runloop中移除时删除source

我们还需要定义事件的传递机制。这部分是运行在单独的线程上,负责向输入源提供数据并在适当的时候发出信号。事件的传递机制可自行定义。在最后一节中会具体的讲实现。

3.1.2.3 Cocoa Perform Selector Source(Cocoa可执行选择器来源)

除了Port-based Source外,Cocoa还定义自定输入源,允许在任何线程上执行Selector(Perfrom Selector)。Perfrom Selector在目标线程上会被序列化(减少多个方法时可能发生的同步问题,我猜是queue一样的,FIFO),但是Perfrom Selector Source在执行其选择器后,将会从Runloop中删除。

可以使用这种方式来实现不同线程之间的消息发送

当在另外一个线程执行选择器的时候,目标线程必须要有一个可用的Runloop。对于创建的线程,Perform Selector就必须要显示的去启动Runloop(对于创建的线程,就需要我们手动显示地去启动Runloop之后才能执行选择器,对于非创建线程,该线程必须要有可用的Runloop来执行选择器)。

因为主线程是自动启动Runloop的,所以当调用了applicationDidFinishLaunching:方法之后就可以在主线程上调用相关方法。

Runloop在处理Selector的时候,是当前这一次循环就去所有的Selector而不是每循环一次处理一个Selector

下表定义了NSObject上定义的Selector。下列方法是没有去新建线程的。注意,下面这些操作都是在Runloop的下一个运行循环才开始执行。

方法 描述
performSelectorOnMainThread:withObject:waitUntilDone:/performSelectorOnMainThread:withObject:waitUntilDone:modes: 程序的主线程在Runloop的下一个循环中执行指定的选择器,但是这两个方法可以阻塞当前线程直到开始执行选择器之后。
performSelector:onThread:withObject:waitUntilDone:/performSelector:onThread:withObject:waitUntilDone:modes: 程序在指定线程执行,同样可以阻塞当前线程。
performSelector:withObject:afterDelay:/performSelector:withObject:afterDelay:inModes: 在当前线程执行,同时可以配置延迟时间。如果当前执行Selector的队列过多,它们会按照顺序执行。
cancelPreviousPerformRequestsWithTarget:/cancelPreviousPerformRequestsWithTarget:selector:object: 能够撤销发送到当前线程的消息(撤销的消息是第三个方法发送的消息performSelector:withObject:afterDelay:/performSelector:withObject:afterDelay:inModes:)。

3.1.3 定时器源

定时器源在预定时间内同步地将事件传递给线程(上面输入源是异步的方式)。Timers可以让线程通知自己在某一时刻做一些事务,例如当用户在搜索的时候可以在输入几个关键字母之后使用timer来延迟一定时间来启动自动搜索的功能。

尽管timer是基于时间的通知方式,但是并不是真的时间机制。就像输入源,定时器在Runloop中也是和特定Mode相关联的。如果定时器没有处在Runloop正在监视的Mode中的话,该定时器是不会触发的。必须要等到Runloop在定时器支持的Mode中运行时,该定时器才会正常运行。如果定时器被触发时机是在Runloop执行任务中,那么这个定时器源的相关事件只有在Runloop下一次运行循环时才能被执行。那如果Runloop停止运行,那么该定时器源的事件将永远没办法执行

我们可以配置定时器是只生成一次还是还是反复生成。

反复执行的定时器会根据它的触发时间自动配置,并不是真实的触发时间。例如,一个定时器设置的是每5秒来触发一次,后面每次的触发时间都是在5s内,在真实时间上可能是有点延迟的。如果是因为真实时间的延迟过长而设置的定时器触发时间过短的话,那么这次被错过的触发时机就会被跳过而进入下一轮的触发时机。

3.1.4 Runloop Observer(Runloop 观察者)

和源(Source)稍有不同的是,当异步或者同步触发事件时,观察者(observer)在Runloop执行期间的特殊情况下被触发。可以通过观察者在线程准备进入休眠之前来处理给定的事件。我们可以在以下时间点中关联Runloop Observer:

  1. Runloop入口
  2. Runloop即将处理定时器时
  3. Runloop即将处理输入源时
  4. Runloop即将进入休眠状态
  5. Runloop被唤醒,但是在Runloop处理该事件之前
  6. Runloop退出时

我们可以通过Core Foundation的相关办法将Runloop Observer添加到程序中,创建CFRunLoopObserverRef类型的Observer。

和定时器源类似,可以设置Observer是使用一次还是重复使用,如果是使用一次在触发之后Observer将会从Runloop中移除。如果是重复的使用的话该Observer并不会被移除。在创建Observer的时候来指定是运行一次还是重复多次。

3.1.5 Runloop事件顺序

每次运行时,线程的Runloop都会处理被挂起的事件,并为Observer发出通知。相关顺序如下:

预先一步:通知Observer,进入Runloop;

第1步:通知Observer,定时器即将被触发;

第2步:通知Observer,输入源(非Port-based Source)将被触发;

第3步:触发输入源(非Port-based Source);

第4步:如果一个Port-based的输入源准备或者等待被触发,立即处理它,并跳到第8步;

第5步:通知Observer,线程即将进入休眠状态;

第6步:线程进入休眠状态直到以下事件发生被唤醒:

  • Port-based输入源事件到达。
  • 定时器被触发。
  • Runloop超时。
  • Runloop被唤醒

第7步:通知Observer,线程被唤醒;

第8步:处理待处理的事件:

  • 如果输入源触发,传递该事件(第4步传过来的Port-based事件)。
  • 如果用户定义的定时器触发,处理定时器事件重新开始循环,并跳到第1步。
  • 如果Runloop被唤醒但是没有超时,去走第1步(如果超时则退出Runloop)。

最后一步:通知Observer,Runloop退出;

上诉图片为顺时针,如果第6步中是因为Runloop超时被唤醒,那么走到第8步之后会因为超时而退出Runloop

因为通知Obserser定时器和输入源时机在实际发生之前,所以在通知时间和实际发生时间存在一定的差距。唤醒通知和休眠的通知在真实时间发生之间,可以通过这两个通知来准确的做一些事情。

⚠️注意现在讨论的是上诉第6步中线程唤醒条件之一的Runloop被唤醒,Runloop可以被Runloop对象显示的唤醒。其他事件也可以唤醒Runloop,例如,添加一个非Port-based的输入源唤醒Runloop,可以立即处理输入源而不用进入循环中其他事情(在“配置Runloop源”中自定义输入源时就会用到通过输入源来唤醒工作线程的Runloop)。

本人总结TIPS

  • 我们可以自定义Mode,但是必须要为这个Mode指定Source或者Observe或者Timer;

  • 次级线程,可以使用自定义Mode防止低优先级的Source在时间紧急时传递事件;

  • Input Source包含了Port-based Source和Custom input Source;

  • Port-based Source由内核自动发出信号,而Custom input Sources必须从其他线程手动的发送信号

  • Perfrom Selector Source在执行其选择器将从Runloop中删除;

  • 要在线程上执行选择器就必须要有能用的Runloop

  • Runloop在处理Selector的时候,是当前这一次循环就去处理所有的Selector而不是每循环一次处理一个Selector;

  • 尽管timer是基于时间的通知方式,但是并不是真的时间机制

  • 虽然定时器和输入源的通知时间会有差距,但是唤醒和休眠的通知时机是准确的;

  • 记住,Runloop Observer是可以重复添加的。

results matching ""

    No results matching ""