博客> Objective-C之RunLoop详解
Objective-C之RunLoop详解
2017-08-16 02:20 评论:0 阅读:1145 dayewoxingxiao

Run Loops

Run loop 字面意思就是运行循环,Run loops是线程相关的的基础框架的一部分。一个run loop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。

Cocoa和Core Fundation都提供了run loop objects来帮助配置和管理你线程的run loop。你的应用程序不需要显式的创建这些对象(run loop objects);每个线程,包括程序的主线程都有与之对应的run loop object。只有子线程才需要显式的运行它的run loop。在Carbon和Cocoa程序中,主线程会自动创建并运行它run loop,作为一般应用程序启动过程的一部分。

关于run loop object的额外信息,参阅NSRunLoop Class Reference和CFRunLoop Reference文档。

Run loop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。输入源传递异步事件,通常消息来自于其他线程或程序。定时源则传递同步事件,发生在特定时间或者重复的时间间隔。两种源都使用程序的某一特定的处理例程来处理到达的事件。

图3-1显示了run loop的概念结构以及各种源。输入源传递异步消息给相应的处理例程,并调用runUntilDate:方法来退出(在线程里面相关的NSRunLoop对象调用)。定时源则直接传递消息给处理例程,但并不会退出run loop。

 Enter your image description here: Figure 3-1 Structure of a run loop and its sources

除了处理输入源,run loops也会生成关于run loop行为的通知(notifications)。注册的run loop观察者(run-loop Observers)可以收到这些通知,并在线程上面使用它们来做额外的处理。你可以使用Core Foundation在你的线程注册run-loop观察者。

1. Run Loop 模式

Run loop模式是所有要监视的输入源和定时源以及要通知的run loop注册观察者的集合。每次运行你的run loop,你都要指定(无论显示还是隐式)其运行个模式(RunLoop Model 以下称Model)。在run loop运行过程中,只有和模式(Model)相关的源才会被监视并允许他们传递事件消息。(类似的,只有和模式(Model)相关的观察者会通知run loop的进程)。和其他模式(Model)关联的源只有在run loop运行在其模式(Model)下才会运行,否则处于暂停状态。

通常在你的代码中,你可以通过指定名字来标识模式。Cocoa和Core foundation定义了一个默认的和一些常用的模式,在你的代码中都是用字符串来标识这些模式。当然你也可以给模式名称指定一个字符串来自定义模式。虽然你可以给模式指定任意名字,但是模式的内容则不能是任意的。你必须添加一个或多个输入源,定时源或者run loop的观察者到你新建的模式中让他们有价值。

通过指定模式可以使得run loop在某一阶段过滤来源于源的事件。大多数时候,run loop都是运行在系统定义的默认模式上。但是模态面板(modal panel)可以运行在 “modal”模式下。在这种模式下,只有和模式面板相关的源才可以传递消息给线程。对于子线程,你可以使用自定义模式在一个时间周期操作上屏蔽优先级低的源传递消息。

注意:模式区分基于事件的源而非事件的种类。例如,你不可以使用模式只选择处理鼠标按下或者键盘事件。你可以使用模式监听端口,暂停定时器或者改变其他源或者当前模式下处于监听状态run loop观察者。

系统定义的Mode有以下几种:

1.CFRunLoopDefaultMode: 这个是默认 Mode,也是空闲状态。主线程通常在这个 Mode 下运行的。 2.UITrackingRunLoopMode: ScrollView滚动时候的模式。 3.UIInitializationRunLoopMode: 在刚启动程序时进入的第一个 Mode,私有,启动完成后就不再使用。 4.GSEventReceiveRunLoopMode: 接受系统事件的内部的Mode,这个Mode由GraphicsServices调用在CFRunLoopRunSpecific前面。通常用不到。 5.CFRunLoopCommonModes: 这是一个数组,默认包括了第1和第2种模式,可以添加自己的Mode。

1.1输入源 输入源异步的发送消息给你的线程。事件来源取决于输入源的种类:基于端口的输入源和自定义输入源。基于端口的输入源监听程序相应的端口。自定义输入源则监听自定义的事件源。至于run loop,它不关心输入源的是基于端口的输入源还是自定义的输入源。系统会实现两种输入源供你使用。两类输入源的区别在于如何显示:基于端口的输入源由内核自动发送,而自定义的则需要人工从其他线程发送。

当你创建输入源,你需要将其分配给run loop中的一个或多个模式。模式只会在特定事件影响监听的源。大多数情况下,run loop运行在默认模式下,但是你也可以使其运行在自定义模式。若某一源在当前模式下不被监听,那么任何其生成的消息只在run loop运行在其关联的模式下才会被传递。

基于端口的输入源 Cocoa和Core Foundation内置支持使用端口相关的对象和函数来创建的基于端口的源。例如,在Cocoa里面你从来不需要直接创建输入源。你只要简单的创建端口对象,并使用NSPort的方法把该端口添加到run loop。端口对象会自己处理创建和配置输入源。

在Core Foundation,你必须人工创建端口和它的run loop源.在两种情况下,你都可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建合适的对象。

更多例子关于如何设置和配置一个自定义端口源,参阅“配置一个基于端口的输入源”部分。

自定义输入源 为了创建自定义输入源,必须使用Core Foundation里面的CFRunLoopSourceRef类型相关的函数来创建。你可以使用回调函数来配置自定义输入源。Core Fundation会在配置源的不同地方调用回调函数,处理输入事件,在源从run loop移除的时候清理它。

除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并通知它处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。

关于创建自定义输入源的例子,参阅“定义一个自定义输入源”。关于自定义输入源的信息,参阅CFRunLoopSource Reference

定时源 定时源在预设的时间点同步方式传递消息。定时器是线程通知自己做某事的一种方法。例如,搜索控件可以使用定时器,当用户连续输入的时间超过一定时间时,就开始一次搜索。这样使用延迟时间,就可以让用户在搜索前有足够的时间来输入想要搜索的关键字。

经管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和你的run loop的特定模式相关。如果定时器所在的模式当前未被run loop监视,那么定时器将不会开始直到run loop运行在相应的模式下。类似的,如果定时器在run loop处理某一事件期间开始,定时器会一直等待直到下次run loop开始相应的处理程序。如果run loop不再运行,那定时器也将永远不启动。

你可以配置定时器工作仅一次还是重复工作。重复工作定时器会基于安排好的时间而非实际时间调度它自己运行。举个例子,如果定时器被设定在某一特定时间开始并5秒重复一次,那么定时器会在那个特定时间后5秒启动,即使在那个特定的触发时间延迟了。如果定时器被延迟以至于它错过了一个或多个触发时间,那么定时器会在下一个最近的触发事件启动,而后面会按照触发间隔正常执行。

关于更多配置定时源的信息,参阅“配置定时源”部分。关于引用信息,查看NSTimer Class Reference或CFRunLoopTimer Reference。

Run Loop观察者 源是合适的同步或异步事件发生时触发,而run loop观察者则是在run loop本身运行的特定时候触发。你可以使用run loop观察者来为处理某一特定事件或是进入休眠的线程做准备。你可以将run loop观察者和以下事件关联:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

        kCFRunLoopEntry         = (1UL <&lt; 0), // 即将进入Loop

        kCFRunLoopBeforeTimers  = (1UL <&lt; 1), // 即将处理 Timer

        kCFRunLoopBeforeSources = (1UL <&lt; 2), // 即将处理 Source

        kCFRunLoopBeforeWaiting = (1UL <&lt; 5), // 即将进入休眠

        kCFRunLoopAfterWaiting  = (1UL <&lt; 6), // 刚从休眠中唤醒

        kCFRunLoopExit          = (1UL <&lt; 7), // 即将退出Loop

};

你可以给run loop观察者添加到Cocoa和Carbon程序里面,但是如果你要定义观察者并把它添加到run loop的话,那就只能使用Core Fundation了。为了创建一个run loop观察者,你可以创建一个CFRunLoopObserverRef类型的实例。它会追踪你自定义的回调函数以及其它你感兴趣的活动。

和定时器类似,run loop观察者可以只用一次或循环使用。若只用一次,那么在它启动后,会把它自己从run loop里面移除,而循环的观察者则不会。你在创建run loop观察者的时候需要指定它是运行一次还是多次。

关于如何创建一个run loop观察者的实例,参阅“配置run loop”部分。关于更多的相关信息,参阅CFRunLoopObserver Reference。

Run Loop的事件队列 每次运行run loop,你线程的run loop对会自动处理之前未处理的消息,并通知相关的观察者。具体的顺序如下:

因为定时器和输入源的观察者是在相应的事件发生之前传递消息,所以通知的时间和实际事件发生的时间之间可能存在误差。如果需要精确时间控制,你可以使用休眠和唤醒通知来帮助你校对实际发生事件的时间。

因为当你运行run loop时定时器和其它周期性事件经常需要被传递,撤销run loop也会终止消息传递。典型的例子就是鼠标路径追踪。因为你的代码直接获取到消息而不是经由程序传递,因此活跃的定时器不会开始直到鼠标追踪结束并将控制权交给程序。

Run loop可以由run loop对象显式唤醒。其它消息也可以唤醒run loop。例如,添加新的非基于端口的源会唤醒run loop从而可以立即处理输入源而不需要等待其他事件发生后再处理。

1.2 何时使用Run Loop

仅当在为你的程序创建子线程的时候,你才需要显式运行一个run loop。Run loop是程序主线程基础设施的关键部分。所以,Cocoa和Carbon程序提供了代码运行主程序的循环并自动启动run loop。IOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作为程序启动步骤的一部分,它在程序正常启动的时候就会启动程序的主循环。类似的,RunApplicationEventLoop函数为Carbon程序启动主循环。如果你使用xcode提供的模板创建你的程序,那你永远不需要自己去显式的调用这些例程。

对于子线程,你需要判断一个run loop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的run loop。比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动run loop。Run loop在你要和线程有更多的交互时才需要,比如以下情况:

 1.使用端口或自定义输入源来和其他线程通信
 2.使用线程的定时器
 3.Cocoa中使用任何performSelector…的方法
 4.使线程周期性工作

如果你决定在程序中使用run loop,那么它的配置和启动都很简单。和所有线程编程一样,你需要计划好在子线程退出线程的情形。让线程自然退出往往比强制关闭它更好。关于更多介绍如何配置和退出一个run loop,参阅”使用Run Loop对象”的介绍。

1.3使用Run Loop对象

Run loop对象提供了添加输入源,定时器和run loop的观察者以及启动run loop的接口。每个线程都有唯一的与之关联的run loop对象。在Cocoa中,该对象是NSRunLoop类的一个实例;而在Carbon或BSD程序中则是一个指向CFRunLoopRef类型的指针。

1.3.1获得Run Loop对象

为了获得当前线程的run loop,你可以采用以下任一方式:

1.在Cocoa程序中,使用NSRunLoop的currentRunLoop类方法来获取一个NSRunLoop对象。
2.使用CFRunLoopGetCurrent函数。

虽然它们并不是完全相同的类型,但是你可以在需要的时候从NSRunLoop对象中获取CFRunLoopRef类型。NSRunLoop类定义了一个getCFRunLoop方法,该方法返回一个可以传递给Core Foundation例程的CFRunLoopRef类型。因为两者都指向同一个run loop,你可以在需要的时候混合使用NSRunLoop对象和CFRunLoopRef不透明类型。

1.4配置Run Loop

在你在辅助线程运行run loop之前,你必须至少添加一输入源或定时器给它。如果run loop没有任何源需要监视的话,它会在你启动之际立马退出。关于如何添加源到run loop里面的例子,参阅”配置Run Loop源”。

除了安装源,你也可以添加run loop观察者来监视run loop的不同执行阶段情况。为了给run loop添加一个观察者,你可以创建CFRunLoopObserverRef不透明类型,并使用CFRunLoopAddObserver将它添加到你的run loop。Run loop观察者必须由Core foundation函数创建,即使是Cocoa程序。

1.5启动Run Loop

1.run() 函数(无条件的)
2.runUntilDate: (设置超时时间)
3.runMode:beforeDate: (特定的模式)

无条件的进入run loop是最简单的方法,但也最不推荐使用的。因为这样会使你的线程处在一个永久的循环中,这会让你对run loop本身的控制很少。你可以添加或删除输入源和定时器,但是退出run loop的唯一方法是杀死它。没有任何办法可以让这run loop运行在自定义模式下。

替代无条件进入run loop更好的办法是用预设超时时间来运行run loop,这样run loop运作直到某一事件到达或者规定的时间已经到期。如果是事件到达,消息会被传递给相应的处理程序来处理,然后run loop退出。你可以重新启动run loop来等待下一事件。如果是规定时间到期了,你只需简单的重启run loop或使用此段时间来做任何的其他工作。

除了超时机制,你也可以使用特定的模式来运行你的run loop。模式和超时不是互斥的,他们可以在启动run loop的时候同时使用。模式限制了可以传递事件给run loop的输入源的类型,这在”Run Loop模式”部分介绍。

可以递归的运行run loop。换句话说你可以使用CFRunLoopRun,CFRunLoopRunInMode或者任一NSRunLoop的方法在输入源或定时器的处理程序里面启动run loop。这样做的话,你可以使用任何模式启动嵌套的run loop,包括被外层run loop使用的模式。

1.6 退出Run Loop

有两种方法可以让run loop处理事件之前退出:

1.给run loop设置超时时间
1.2通知run loop停止

如果可以配置的话,推荐使用第一种方法。指定一个超时时间可以使run loop退出前完成所有正常操作,包括发送消息给run loop观察者。

使用CFRunLoopStop来显式的停止run loop和使用超时时间产生的结果相似。Run loop把所有剩余的通知发送出去再退出。与设置超时的不同的是你可以在无条件启动的run loop里面使用该技术。

尽管移除run loop的输入源和定时器也可能导致run loop退出,但这并不是可靠的退出run loop的方法。一些系统例程会添加输入源到run loop里面来处理所需事件。因为你的代码未必会考虑到这些输入源,这样可能导致你无法没从系统例程中移除它们,从而导致退出run loop。

1.7线程安全和Run Loop对象

线程是否安全取决于你使用那些API来操纵你的run loop。Core Foundation 中的函数通常是线程安全的,可以被任意线程调用。但是如果你修改了run loop的配置然后需要执行某些操作,任何时候你最好还是在run loop所属的线程执行这些操作。

至于Cocoa的NSRunLoop类则不像Core Foundation具有与生俱来的线程安全性。如果你想使用NSRunLoop类来修改你的run loop,你应用在run loop所属的线程里面完成这些操作。给属于不同线程的run loop添加输入源和定时器有可能导致你的代码崩溃或产生不可预知的行为。

1.8 配置RunLoop的源

1.8.1定义自定义输入源

创建自定义的输入源包括定义以下内容:

1.输入源要处理的信息。
2.使感兴趣的客户端(可理解为其他线程)知道如何和输入源交互的调度例程。
3.处理其他任何客户端(可理解为其他线程)发送请求的例程。
4.使输入源失效的取消例程。

由于你自己创建输入源来处理自定义消息,实际配置选是灵活配置的。调度例程,处理例程和取消例程都是你创建自定义输入源时最关键的例程。然而输入源其他的大部分行为都发生在这些例程的外部。比如,由你决定数据传输到输入源的机制,还有输入源和其他线程的通信机制也是由你决定。

作用

使程序一直运行,并且接收用户输入等事件 决定程序什么时候处理什么事件 调用方面 解耦(比如用户划一下屏幕,会产生N个event事件,但是用户不可能等着被调方全部执行完再进行下一步的动作,也就是会将此系列事件扔到一个消息队列里,每次再从消息队列里面取,主调方与被调方实现解耦) 节省CPU(因为runloop在没事干的时候是休眠状态,只有接收到信号的时候才会唤醒,执行相应的操作)

优点

一个run loop就是一个事件处理循环,用来不停的监听和处理输入事件并将其分配到对应的目标上进行处理。如果仅仅是想实现这个功能,你可能会想一个简单的while循环不就可以实现了吗,用得着费老大劲来做个那么复杂的机制?显然,苹果的架构设计师不是吃干饭的,你想到的他们早就想过了。

首先,NSRunLoop是一种更加高明的消息处理模式,他就高明在对消息处理过程进行了更好的抽象和封装,这样才能是的你不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者是timer source(见后文)中了。

其次,也是很重要的一点,使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,这可以大大节省系统资源。

收藏
3
sina weixin mail 回到顶部