这几天研究了一下iOS的Runloop,看了不少的文章,收获不少,但是疑问也挺多。所以我就试着去翻译了并分析总结了一下苹果的Runloop文档,注意:并不是CFRunloop的源码,而是这篇总体概述了Runloop的文档。
这里先给出一些有关RunLoop的官方文档及一些好的文章的链接:
官方文档:
入门可以看:
大神文章:
RunLoop怎么用:
RunLoop问题集:
提前说明,这篇文章你可以对照着英文文档来看,目录结构是一样的。但并不是逐句翻译的,会有省略,并加上了我自己的总结分析。
本文目录
- Run Loops(RunLoop)
- Anatomy of a Run Loop(RunLoop的剖析)
- Run Loop Modes(RunLoop的Mode)
- Input Sources
- Port-Based Sources(基于端口的输入源)
- Custom Input Sources(自定义输入源)
- Cocoa Perform Selector Sources(PerformSelector源)
- Timer Sources(定时器源)
- Run Loop Observers(RunLoop的观察者Observer)
- The Run Loop Sequence of Events(RunLoop的内部运行逻辑)
- 关于CFRunLoop(这个为个人补充)
- 关于CFRunLoopMode(这个为个人补充)
- When Would You Use a Run Loop?(什么时候使用RunLoop?)
- Using Run Loop Objects (如何使用RunLoop对象)
- Getting a Run Loop Object(获取RunLoop对象)
- Configuring the Run Loop(配置RunLoop)
- Starting the Run Loop(启动RunLoop)
- Exiting the Run Loop(退出RunLoop)
- Thread Safety and Run Loop Objects(线程安全和RunLoop对象)
- Configuring Run Loop Sources(配置RunLoop的Source)
- Defining a Custom Input Source(定义自定义输入源,即Source0)
- Configuring Timer Sources(配置定时器源,即Timer)
- Configuring a Port-Based Input Source(配置基于端口的输入源,即Source1)
Run Loops(RunLoop)
RunLoop是与线程相关的基础架构中的一部分。RunLoop是一个事件处理环,它可以用来安排工作并协调传入的事件。RunLoop的目的是在有任务的时候保持线程的运行,在没有任务的时候使线程休眠。
RunLoop的使用管理不是完全自动的。你必须在合适的时机使用线程代码启动RunLoop,并对接收的事件进行反应处理。Cocoa(NSRunLoop)和Core Foundation(CFRunLoop)都提供了RunLoop的对象。
每一个线程都对应着一个RunLoop。在应用程序启动的时候,主线程的RunLoop会自动启动,而辅助线程(即子线程)是需要你手动去获取的。
Anatomy of a Run Loop(RunLoop的剖析)
RunLoop与它的名字的意思相像,它是一个环,线程进入这个环并且可以对收到的事件进行运行和处理。
image如上图所示:RunLoop从两种不同类型的源接收事件,一个是输入源(Input Source),另一个是定时器源(Timer Source)。Input Source 提供异步事件,通常是来自另一个线程或不同应用程序的消息。Timer Source提供同步事件,是发生在预定时间的或重复间隔里的。
除了处理输入的源,RunLoop还会生成有关RunLoop的通知,已经注册了的Observer可以接收这些通知并使用它们在线程上执行其它的处理。你可以通过Core Foundation在你的线程上使用RunLoop Observer。
分析说明:
- RunLoop有两种源:Input sources和Timer sources,Input sources里面又分了几种。RunLoop还包含了Observer。
- 看图,Input Source里面有三种:Port、Custom、performSelector,其实只有两种:基于端口的输入源(Port)和自定义的输入源(Custom),因为performSelector是苹果自定义的输入源(它比较特殊)。
- 关于CFRunLoop的里面的Source又分为了Source1和Source0,我们下面会说。
下面如无特别说明,Source就是指Input sources,Timer就是指Timer sources。
Run Loop Modes(RunLoop的Mode)
一个RunLoop Mode是多个输入源(Input Source)、多个定时器(Timer)、多个观察者(Observer)的集合。每次你运行一个RunLoop,你都得显式或隐式地指定要运行的Mode。在RunLoop的运行过程中,仅监测该Mode下的源(sources)并允许其传递事件,并且只有该Mode下的Observer才有作用。在其它Mode下的源(sources)会挂起任何新的事件,直到RunLoop以在该Mode下运行。
分析说明:
- 每个线程只能有一个对应的RunLoop,RunLoop必须手动去开启才能存在,但是主线程对应的RunLoop是在应用启动的时候自动就开启了,所以只需要你主动去开启子线程的RunLoop不用管主线程的RunLoop。关于RunLoop的创建下面会说。现在先了解这一点。
分析说明:
一般我们常用的Mode有三种:
- kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。一般情况下App都是运行在这个mode下的
- CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
一般作用于ScrollView滚动的时候的模式,保证滑动的时候不受其他事件影响。
- kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
这个并不是某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程中只包含NSDefaultRunLoopMode。注意:
①在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功。
②在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。
③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到kCFRunLoopCommonModes组合。
分析说明:
- 注意,一个RunLoop里会有多个Mode(这点后面会说明);
- 一个Mode下有多个Source、Timer、Observer;
- RunLoop的运行必须指定一个Mode,不管是显式或隐式的指定;
- RunLoop在一个Mode下运行,该Mode里的Source、Timer、Observer才会有效,其它Mode里的Source、Timer、Observer就不能有效果了。所以说Mode其实是为了把不同的Source、Timer、Observer分开来;
- 其它没有运行的Mode会挂起的新来的事件,只有当RunLoop运行到该Mode下时,该Mode的新事件才会被处理。
你可以通过Mode的名字来识别和使用这些Mode。Cocoa 和 Core Foundation都定义了一个默认的Mode和其它一些常用的Mode。你也可以使用任何名称自定义一个Mode,但必须确保这个自定义的Mode里有一个或多个Input Source、Timer Source、Observer。
你可以在不同Mode下运行RunLoop,以此过滤掉不需要的来源中的事件。
分析说明:
- 为什么要用多个Mode,就是为了不同的Mode里面的Source、Timer、Observer互不影响。
- 典型的例子就是NSTimer在平常管用,因为主线程平常是在NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的默认模式下,但在scrollview滑动的时候NSTimer就不管用了,因为scrollview滑动的时候,RunLoop是运行在UITrackingRunLoopMode模式下的。所以要想NSTimer在滑动的时候也管用,就要将NSTimer添加进NSDefaultRunLoopMode和UITrackingRunLoopMode这两个Mode下。
- NSTimer是一种Timer Source。
- 这里说明一下,RunLoop必须有Timer或Source才能运行,否则会退出,即使只有Observer也不行。
Input Sources
Input Sources以异步方式向线程传递事件。事件的来源取决于Input Sources的类型,通常为两个类型中的一个:1. 基于端口(Port)的输入源,它监视应用程序的Mach端口;2. 自定义(Custom)输入源,监视自定义事件源。
就RunLoop来说,Input Sources是哪一种类型并无所谓。两种类型的源的唯一区别是,基于端口(Port)的输入源自动从内核发出信号,而自定义(Custom)输入源必须手动地从另一个线程发出信号。
分析说明:
- 这里我也一知半解,关于Mach端口可以去看这篇大神文章里的说明。至于Input Source 和Source1和Source0的关系在后面会说到。
1. Port-Based Sources(基于端口的输入源)
在Cocoa和Core Foundation里,提供了与端口(Port)相关的的对象和函数,你可以使用它们来创建基于端口的输入源。
比如,在Cocoa里,你根本不必直接去创建一个端口输入源,你只需要创建一个端口对象,并使用NSPort的方法将这个端口对象添加到RunLoop中,端口对象就会自动为你处理输入源的创建和配置。
在Core Foundation中,您必须手动创建端口及其RunLoop源。
有关如何设置和配置基于端口的自定义源的示例,请参阅配置基于端口的输入源。
分析说明:
- 基于端口的输入源:就是Source1。具体后面说明。
2. Custom Input Sources(自定义输入源)
要创建自定义输入源,必须使用 Core Foundation 里的CFRunLoopSourceRef的相关函数。
有关如何创建自定义输入源的示例,请参阅定义自定义输入源。有关自定义输入源的参考信息,另请参阅CFRunLoopSource参考。
3. Cocoa Perform Selector Sources(PerformSelector源)
除了基于端口的输入源(Port-Based Sources),Cocoa还定义了一个自定义输入源,允许你在任何线程去执行一个selector。
与基于端口的源相同的是,Perform Selector请求在目标线程上被执行,从而缓解了一个线程上运行多个方法时可能会发生的同步问题。与基于端口的源不同的是,一个Perform Selector Source会在执行完后从这个RunLoop中被移除。
想要在目标线程上执行一个selector,目标线程必须有一个活动的RunLoop。主线程在应用程序启动时,已经具备了一个RunLoop;而子线程必须去你自己手动获取RunLoop,子线程的RunLoop才会存在。
分析说明:
- 关于RunLoop是如何获取和创建的,可以去看这篇文章。
RunLoop通过一次循环处理所有排队的Perform Selector,而不是每次循环只处理一个。
在其它线程上执行selector的方法如下:
//在主线程的下一个RunLoop的循环里,去执行selector。这两个方法可以选择是否阻塞当前线程直到这个selector被执行完毕。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
//在任意线程中(前提是你有这个线程的对象)执行selector。这两个方法可以选择是否阻塞当前线程直到这个selector被执行完毕。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
//在RunLoop的下一个循环周期和可选的延迟之后,在当前线程执行selector。因为它必须等到下一个循环去执行selector,所以这些方法提供了来自当前执行代码的自动迷你延迟。多个selector按照排队顺序执行。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
//这个是针对performSelector:withObject:afterDelay: or performSelector:withObject:afterDelay:inModes: method使用的,用来取消发送到当前线程的消息。
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
分析说明:
- Perform Selector也是自定义输入源。
- Perform Selector它是比较特殊的,也是属于Source0(非端口输入源)。
Timer Sources(定时器源)
定时器源在预设的时间将时间同步传递给你的线程。定时器是线程通知它自己干事情的一种方式。
虽然定时器是基于时间的通知,但是它并不是一种实时机制。与输入源类似,Timer与Mode相关联,如果Timer不在RunLoop当前所运行的Mode中,它就不会被触发(分析说明:这一点上面提过了)。还有,如果RunLoop正在执行一段程序,而这时定时器的时间到了,它也不会被触发,它会等下一次的时间点去触发。
分析说明:
- 假如一个定时器的触发时间是5点0秒,5点10秒,5点20秒...那么到了5点0秒的时候,假如RunLoop正在执行一大段代码,那么定时器不会被触发,它只能等5点10秒这次了。如果5点10这次也错过了,那么就等5点20秒了。
Run Loop Observers(RunLoop的观察者Observer)
Observer在RunLoop的特殊位置触发。可以使用observer来准备线程以处理事件,或在线程进入休眠状态之前准备线程。
observer触发的位置(可以去看下面的那张图):
- 即将进入RunLoop,通知observer;
- 即将处理Timer,通知observer;
- 即将处理Source(非端口的输入源),通知observer;
- 线程即将休眠,通知observer;
- 线程刚被唤醒,但在它处理唤醒它的事件之前,通知observer;
- 线程退出了RunLoop,通知observer;
你可以使用Core Foundation添加Observer到应用程序里,需使用CFRunLoopObserverRef类型。
与Timer类似,Observer可以使用一次或重复使用。一次性Observer在触发后将其自身从RunLoop中移除,而重复的Observer会继续存在于RunLoop中,您可以指定Observer在创建时运行一次还是重复运行。
The Run Loop Sequence of Events(RunLoop的内部运行逻辑)
每一次运行RunLoop,线程对应的RunLoop就会处理挂起的事件,并通知观察者。它执行的顺序如下:
- 通知Observer即将进入RunLoop
- 通知Observer即将处理Timer
- 通知Observer即将处理Source0(非端口的输入源)
- 处理Source0(非端口的输入源)
- 如果有Source1(基于端口的输入源)准备就绪并等待被触发,立即处理该事件,并跳到步骤9
- 通知Observer即将休眠
- 线程休眠,直到发生以下事件之一:
- 一个事件到达Source1(基于端口的输入源)
- 一个定时器(Timer)触发
- RunLoop超时
- RunLoop被手动唤醒(例如添加一个Source0非端口的输入源)
- 通知Observer线程刚刚唤醒
- 处理待处理的事件
- 如果用户定义的Timer触发了,则处理这个定时器事件并重新启动RunLoop循环,跳到步骤2
- 如果输入源触发了,则传递事件
- 如果RunLoop被手动唤醒,但尚未超时,重新启动RunLoop循环,跳到步骤2
- 通知Observer RunLoop已经退出。
可以使用RunLoop对象显式唤醒RunLoop,其它事件也可能导致RunLoop被唤醒,例如添加一个Source0(非端口的输入源)会唤醒RunLoop,以便立即处理输入源,而不是等到其它事件发生。
****注意:下图有错误,最左边应该改为Source1(port),且缺少一个超时唤醒;10应该改为通知Observer,RunLoop已经退出,而不是即将退出****。
image以下是CFRunLoop的一些分析,文档中并没有,属于补充,帮助更好的理解。这里也会说明Source1和Source0。
关于CFRunLoop
在Core Foundation中,CFRunLoop的结构大致如下:
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current RunLoop Mode
CFMutableSetRef _modes; // Set
...
};
这里我们可以看到CFRunLoop里:
- CFMutableSetRef _modes:说明一个RunLoop中有多个Mode。
- 还有一个叫currentMode的,这就是RunLoop当前所运行的Mode,正如上文所说的,RunLoop只能指定一个Mode来运行。(补充,记住,RunLoop要想切换Mode,只能退出RunLoop,再指定一个Mode重新运行。)
- commonModes:一个Mode可以将自己标记为“common”属性(通过使用其 Mode的Name添加到RunLoop的“commonModes”中)。主线程的 RunLoop 里有两个预置的Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个Mode都已经被标记为Common”属性。kCFRunLoopCommonModes/NSRunLoopCommonModes包含这两个Mode。
- commonModeItems:Source/Observer/Timer都是item,你可以将source/timer/observer放入到RunLoop的commonModeItems中。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将commonModeItems里的 Source/Observer/Timer同步到具有 “Common” 标记的所有Mode里。所以说,NSTimer有两种方式去解决滑动时不运行,一种方式是上面所说,将NSTimer对象加入到kCFRunLoopDefaultMode和UITrackingRunLoopMode(或者NSRunLoopCommonModes中)中;另一种方式就是将Timer加入到顶层的RunLoop的 “commonModeItems”中。”commonModeItems” 会被RunLoop自动更新到所有具有“Common”属性的Mode里去。
关于CFRunLoopMode
CFRunLoopMode的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
看CFRunLoopMode的结构发现,Mode里面有Source(在set集合里)、Observer(array数组里)、Timer(在array数组里)。
而根据前面的文档,我们知道RunLoop的源有Timer Source和Input Source,RunLoop还包括了Observer。这里我们对应一下,Timer Source就是CFRunLoop的Timer,Input Source就是CFRunLoop的Source,Observer是CFRunLoop的Observer。
苹果的文档里又将Input Source分为了基于端口的输入源和自定义输入源,我们再看CFRunLoop的结构,发现里面的Source分为了Source0和Source1,那么Source0和Source1怎么区分呢?
我去找了
CFRunLoopSource的文档来看:
CFRunLoopSource是RunLoop的输入源的抽象,输入源通常是异步事件。输入源在CFRunLoop里包括:CFMachPort, CFMessagePort, and CFSocket。
CFRunLoopSource有两类:
-
版本0(Version 0),即Source0,这样命名是因为它的上下文结构的版本字段为0。Source0在应用里必须手动管理(注意了,Source0是手动去触发的)。当一个Source0准备触发的时候,必须使用CFRunLoopSourceSignal通知RunLoop这个Source准备触发了。CFSocket是Source0。
-
版本1(Version 1),即Source1。Source1是由RunLoop和内核管理的。当消息到达Mach端口的时候,Source1会自动发出信号。CFMachPort和CFMessagePort是Source1。
总结一下,在前文中的Input Source又分了基于端口的输入源和自定义输入源:这里基于端口的输入源就为Source1,而自定义输入源应该就是Source0(自定义输入源需要手动从另一个线程触发)。至于performSelector比较特殊,也应该是属于Source0(非端口)的,至于内部到底是怎么实现的,我就不清楚了。
总的来说:输入源就分为基于端口的输入源Source1和非端口的输入源Source0。
苹果文档里稍微说了一下怎么配置Source的,在文章最后。
When Would You Use a Run Loop?(什么时候使用RunLoop?)
-
你需要显式运行RunLoop的唯一一种情况是,为应用程序创建辅助线程(这里的辅助线程就是指子线程)。因为应用程序的主线程的RunLoop会在应用创建的时候自动启动,所以不需要你去管主线程的RunLoop。
-
对于辅助线程,你需要确定是否需要RunLoop,如果是,则自行去配置并启动它。通常在所有的情况下,你都不需要去启动一个线程的RunLoop。比如你要使用一个线程去执行一个长时间运行且预定义的任务,你可以避免去使用RunLoop。
-
RunLoop适用于这种情况:当你希望与线程进行更多的交互时。比如,如果你计划执行以下任何操作,则需要启动RunLoop:
- 使用端口或自定义的输入源与其他线程通信。
- 在线程上使用定时器。
- 在Cocoa框架下,使用任何的performSelector方法。
- 保持线程以执行定期的任务。
如果您确实选择使用RunLoop,则配置和设置非常简单。与线程的编程一样,你应该确定在适当的时机退出RunLoop。并且,最好通过退出而不是强制终止来结束一个线程。
分析说明:
- 主线程对应的RunLoop是在应用创建的时候自动开启的,而子线程的RunLoop需要你手动去获取,你不去获取,子线程的RunLoop就不存在。
- 也就是说,当你在子线程使用NSTimer的时候或者你对子线程使用performSelector系列方法时,必须先去将子线程的RunLoop开启了。
- performSelector需要RunLoop才能有用。
- 这里的performSelector是指上面列出的方法,应该不包括performSelectorInBackGround:withObject:,这个方法是开启一个新的子线程去执行任务。
Using Run Loop Objects (如何使用RunLoop对象)
- 一个RunLoop对象提供了添加Input Source、Timer Source、Observer这些主要接口。
- 一个线程只有一个与之关联的RunLoop对象。
- 在Cocoa中,RunLoop对象是NSRunLoop类的实例;在底层应用中,它是CFRunLoopRef类型的指针。
1. Getting a Run Loop Object(获取RunLoop对象)
获取RunLoop,可以使用下面的方式:
//获得当前线程的RunLoop
[NSRunLoop currentRunLoop];
//主线程的RunLoop
[NSRunLoop mainRunLoop];
//CFRunLoop方法,获得当前现成的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void);
//CFRunLoop方法,获得主线程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void);
也可以使用NSRunLoop的实例方法:- (CFRunLoopRef)getCFRunLoop; 返回一个CFRunLoopRef类型的RunLoop。
2. Configuring the Run Loop(配置RunLoop)
当你在子线程运行一个RunLoop的时候,你至少得添加一个Source或Timer给它,否则当你运行它时,它会立即退出,即使有Observer也不行,必须有Source或Timer。
除了配置源(Input Source和Timer)给RunLoop,你也可以配置Observer并使用它来监测RunLoop的不同执行阶段。你可以使用 CFRunLoopObserverRef 类型和 CFRunLoopAddObserver 函数去添加一个Observer到RunLoop。注意,Observer只能使用Core Foundation(CFRunLoop)的相关方法来创建,即使在Cocoa中也是这样,也就是说,NSRunLoop没有创建Observer的相关方法。
下面是一个例子:创建Observer并添加到RunLoop中,Observer用来监视RunLoop的所有活动。(例子不用深究)
- (void)threadMain
{
// 应用程序采用垃圾回收机制,所以不需要autorelease pool
//获取当前的RunLoop
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 创建一个Observer,kCFRunLoopAllActivities监视所有活动
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);//myRunLoopObserver是一个回调函数
//如果observer存在,就将其关联到RunLoop上
if (observer)
{
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// 创建一个定时器Timer。注意使用scheduledTimerWithTimeInterval:方法的定时器会自动添加到当前RunLoop的默认模式(kCFRunLoopDefaultMode)下。RunLoop必须有一个Source或Timer才能正常运行
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
//使用do while循环创建RunLoop的退出时机。运行RunLoop十次。
NSInteger loopCount = 10;
do
{
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}
这段代码下面有这么一段话:当你为长期存在的线程配置RunLoop时,最好添加一个输入源(Source)来接收消息。虽然你可以只添加一个Timer定时器到RunLoop中,但是Timer一旦被触发,它通常就失效了,这会造成RunLoop的退出。如果你添加一个重复触发的定时器Timer,它会使RunLoop运行更长的时间,但是这就涉及到了定期触发定时器去唤醒线程,这实际上是另一种方式的轮询。相比之下,输入源会等待事件发生,让线程保持休眠状态。
3. Starting the Run Loop(启动RunLoop)
只有在辅助线程(子线程)中才需要启动RunLoop。一个RunLoop必须有一个Source或Timer,如果没有,RunLoop会立即退出。
这里有几种启动RunLoop的方式:
- 无条件地启动
- 设置一个超时值来启动
- 通过特定的Mode启动
无条件地进入RunLoop是最简单也是最不被推荐的方式。无条件地RunLoop会使线程置于永久循环之中,可以添加和删除输入源和定时器,但停止RunLoop的方式是终止它。
最好使用第二种方式,设置一个超时值。设置一个时间值后,RunLoop将一直运行,直到事件到达或者超出时间值。如果事件到达,则将该事件分派给处理程序进行处理,然后退出RunLoop,然后,你的代码可以重新启动RunLoop以处理下一个事件。如果时间到了,你只需要重新启动RunLoop或使用这个时间去进行任何需要的内务处理。
除了时间值,还可以使用特定的Mode去运行RunLoop。设置时间值和Mode并不互斥。Mode类型将限制事件传递到RunLoop的源类型。
下面的代码显示了RunLoop的基本结构,实质上,你将输入源和定时器添加到RunLoop中,然后重复的调用一段程序(这里是do-while)去启动RunLoop,每次这段调用RunLoop的程序返回时,都去检查是否出现了退出该线程的条件。如果有,则不再次启动RunLoop了,将会退出线程。如果没有,再次启动RunLoop。
- (void)skeletonThreadMain
{
// Set up an autorelease pool here if not using garbage collection.
BOOL done = NO;
// Add your sources or timers to the run loop and do any other setup.
do
{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result ** kCFRunLoopRunStopped) || (result ** kCFRunLoopRunFinished))
done = YES;
// Check for any other exit conditions here and set the
// done variable as needed.
}
while (!done);
// Clean up code here. Be sure to release any allocated autorelease pools.
}
补充:可以递归地运行RunLoop,也就是说,可以嵌套RunLoop。
分析说明:
下面是NSRunLoop中启动RunLoop的几种方法:
//Puts the receiver into a permanent loop, during which time it processes data from all attached input sources.
- run
//Runs the loop once, blocking for input in the specified mode until a given date.
- runMode:beforeDate:
//Runs the loop until the specified date, during which time it processes data from all attached input sources.
- runUntilDate:
//Runs the loop once or until the specified date, accepting input only for the specified mode.
- acceptInputForMode:beforeDate:
4. Exiting the Run Loop(退出RunLoop)
在RunLoop处理事件之前,有两种退出方式:
- 配置RunLoop的超时值,超时就会退出
- 使用CFRunLoopStop函数显式停止RunLoop
如果您可以管理它,那么使用超时值肯定是首选。指定超时值可让运行循环完成所有正常处理,包括在退出之前向运行循环观察器发送通知。
使用该CFRunLoopStop函数显式停止运行循环会产生类似于超时的结果。运行循环发出任何剩余的运行循环通知,然后退出。不同之处在于,您可以在无条件启动的运行循环中使用此技术。
还有一种不可靠的退出方式:
- 删除输入源和定时器,但这不是停止运行循环的可靠方法。某些系统例程将输入源添加到运行循环以处理所需的事件。因为你的代码可能不知道这些输入源,所以它将无法删除它们,这将阻止RunLoop退出。
分析说明:
- 退出的三种方式
- 超时
- CFRunLoopStop函数显式停止
- 删除Source和Timer,但这种方式不可靠
5. Thread Safety and Run Loop Objects(线程安全和RunLoop对象)
Core Foundation的CFRunLoop是线程安全的,可以从任何线程调用。但是,应该尽可能地在RunLoop所属于的线程中,去配置RunLoop。
Cocoa NSRunLoop类不是线程安全的。你应该在RunLoop所属于的线程中去修改RunLoop。将Source和Timer添加到属于不同线程的RunLoop可能会出现错误。
Configuring Run Loop Sources(配置RunLoop的Source)
以下部分显示了如何在Cocoa和Core Foundation中设置不同类型输入源的示例。
1. Defining a Custom Input Source(定义自定义输入源,即Source0)
这部分没看,感兴趣的人可以自行去文档中看一下。
2. Configuring Timer Sources(配置定时器源,即Timer)
要创建一个定时器源,需要做的就是创建一个定时器对象并将其添加到RunLoop上。
在Cocoa中,使用NSTimer类创建定时器对象,在Core Foundation中使用CFRunLoopTimerRef类型。NSTimer类只是对Core Foundation的扩展。
创建NSTimer对象的类方法有如下两类:
第一类:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法
上面这类方法在创建NSTimer对象后,会自动添加到RunLoop的默认Mode(NSDefaultRunLoopMode/kCFDefaultRunLoopMode)中。
第二类:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法
上面这类方法在创建了NSTimer对象后,必须手动添加到RunLoop的Mode中,使用addTimer:forMode:方法。你可以选择Mode的类型,Mode的默认类型还是其他类型。
注意一点:定时器必须添加到RunLoop中才能够使用!
例子:使用NSTimer创建和调度计时器
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 创建并调度第一个定时器
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
// 创建并调度第二个定时器
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];
例子:使用Core Foundation创建和调度计时器
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0,NULL,NULL,NULL,NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,0.1,0.3,0,0,
&myCFTimerCallback,&context);
CFRunLoopAddTimer(runLoop,timer,kCFRunLoopCommonModes);
3. Configuring a Port-Based Input Source(配置基于端口的输入源,即Source1)
Cocoa和Core Foundation都提供了基于端口的对象,用于线程之间或进程之间的通信。以下部分介绍如何使用多种不同类型的端口设置端口通信。
下面的我也没有认真去研究,只是大致看了一下。
3.1 Configuring an NSMachPort Object(配置NSMachPort对象)
要与NSMachPort对象建立本地连接,要创建端口对象并将其添加到主线程的RunLoop中。启动子线程时,将同一对象传递给线程的入口点函数(entry-point function)。子线程可以使用相同的对象将消息发送回主线程。
3.1.1 Implementing the Main Thread Code(实现主线程)
以下代码为:在主线程里,启动子线程。
- (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
// This class handles incoming port messages.
[myPort setDelegate:self];
// Install the port as an input source on the current run loop.
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// Detach the thread. Let the worker release the port.
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}
}
....
3.1.2 Configuring an NSMessagePort Object(配置NSMessagePort对象)
4. Configuring a Port-Based Input Source in Core Foundation(在Core Foundation中配置基于端口的输入源)
到这里就结束了,以上的从 3. Configuring a Port-Based Input Source(配置基于端口的输入源,即Source1)开始,都是怎么配置Source的,感兴趣的自己去看吧。
如有错误,烦请指正,谢谢!
网友评论