Runloop对象提供了为runloop添加输入源,定时器源,Runloop Observer的主要接口,并使用Runloop对象来运行Runloop。每个线程都有与之关联的Runloop对象。Cocoa中Runloop对象是NSRunloop类。在CoreFoundation中是一个CFRunloopRef类型的指针。
3.3.1 获取Runloop对象
使用下列方法在当前线程获取Runloop:
- 在Cocoa程序中,使用NSRunloop的currentRunLoop类方法获取NSRunloop对象;
- 使用CFRunLoopGetCurrent方法;
NSRunloop
对象可以获取CFRunloopRef
类型的数据,使用NSRunloop类中定义的getCFRunLoop
方法,该方法会返回一个CFRunLoopRef
类型的数据。因为这两个对象都是引用的相同的Runloop,所以在需要的时候可以将NSRunloop对象和CFRunloopRef类型变量混合调用。
NSRunLoop *loop = [NSRunLoop currentRunLoop];
CFRunLoopRef cf_currentloop = CFRunLoopGetCurrent();
CFRunLoopRef cf_loop = [loop getCFRunLoop];
3.3.2 配置Runloop对象
如果在次级辅助线程上运行了Runloop,我们至少要为它添加一个输入源。如果在Runloop中没有任何监控源,当你准备运行它的时候会马上就退出。至于怎么为Runloop添加源在后面的配置Runloop源来说。
除了为Runloop添加源之外,我们还可以为Runloop添加Observer来检测Runloop执行的不同阶段。创建CFRunLoopObserverRef
类型的变量,并使用CFRunLoopAddObserver
函数来将该变量添加到Runloop中。创建Runloop Observer必须要用CoreFoundation中的方法。
const void *runloop_retain(const void *info);
void runloop_release(const void *info);
void myobservercallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
- (void)runloop_config{
/**
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
} CFRunLoopObserverContext;
*/
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
CFRunLoopObserverContext context = {0,(__bridge void *)(self),runloop_retain,runloop_release,runloop_copydes};
/// order:在Runloop中的处理顺序
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, myobservercallback, &context);
if (observer) {
CFRunLoopRef cf_runloop = [runloop getCFRunLoop];
CFRunLoopAddObserver(cf_runloop, observer, kCFRunLoopDefaultMode);
}
[NSTimer scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer * _Nonnull timer) {
}];
NSInteger loop_count = 10;
do {
[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loop_count--;
} while (loop_count);
}
const void *runloop_retain(const void *info){
return info;
}
void runloop_release(const void *info){
}
void myobservercallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
}
CFStringRef runloop_copydes(const void *info){
return NULL;
}
上面的程序主要是创建一个Runloop Observer并以Default Mode添加到Runloop中。CFRunLoopObserverContext
是一个结构体。接下来重点讲一下CFRunLoopObserverCreate
函数。
allocator:为对象分配内存的分配器。传入NULL或者kCFAllocatorDefault。
Allocator | 描述 |
---|---|
kCFAllocatorDefault | 就是NULL,大部分情况就用该常量。 |
kCFAllocatorSystemDefault | 系统默认的分配器,我们基本上不用这个。 |
kCFAllocatorMalloc | 一般也不会使用,在CFData和CFString时使用 |
kCFAllocatorMallocZone | malloc_default_zone() |
kCFAllocatorNull | 空分配器,不执行任何操作,不分配任何内存。同样用于CFData和CFString |
kCFAllocatorUseContext | CFAllocatorCreate() 的参数。 |
activities:通俗来说就是Observer起作用的时间点。在"剖析Runloop"一节中提到了进入和退出Runloop,线程的休眠和唤醒,定时器源,和非Port-based输入源这六个时刻。下面是CFRunLoopActivity枚举的值,如果要是两个或者多个时刻的可以使用OR运算符,例如:kCFRunLoopEntry | kCFRunLoopExit
:
Activities | 发生时刻 |
---|---|
kCFRunLoopEntry | 进入Runloop是通知Observer |
kCFRunLoopBeforeTimers | 定时器源触发之前通知Observer |
kCFRunLoopBeforeSources | 非Port-based输入源触发之前通知Observer |
kCFRunLoopBeforeWaiting | 线程进入休眠之前通知Observer |
kCFRunLoopAfterWaiting | 线程即将被唤醒通知Observer |
kCFRunLoopExit | Runloop即将推出通知Observer |
kCFRunLoopAllActivities | 包含了上诉所有情况通知Observer |
repeats:是否多次调用,这同样在”剖析Runloop“一节中说过,Observer是可以重复调用的。如果为NO,则在调用一次之后该Observer就无效了。如果为YES,则可以多次重复调用。
当我们为一个常住线程配置Runloop的时候,至少添加一个输入源用来接收消息。尽管只有一个timer的时候就可以进入Runloop,就算这个timer触发,它也是无效的,可能会导致Runloop退出。将定时器设置为重复执行的话可以延长Runloop的运行时间,通过触发timer来唤醒线程,这实际上是另外一种轮询的形式。相比之下,让你的线程进入休眠状态,直到有输入源的时间发生来唤醒线程。
3.3.3 启动Runloop
只有在辅助线程中才有必要去启动Runloop。Runloop必须至少要监视一个输入源或者定时器源。如果一个都没有的话,这个Runloop会马上退出。下面列出来几个方法来启动Runloop:
- 无条件启动;
- 设置时间限制;
- 特定模式下;
进入Runloop最简单的办法就是第一种,但也是最合意的。让我们的Runloop无条件的在常驻线程上运行,这使得我们几乎没法去控制Runloop本身。仅仅能添加或者移除输入源或者定时器源,但这也是唯一一个终止线程的办法(因为在开头就说了Runloop必须要监视一个源)。我们无法在自定义mode中去运行Runloop。
为Runloop设置超时时间比无条件启动Runloop要好一点。Runloop要等到事件处理完成或者超时之后才会停止运行。在事件到达之后,该事件会被分配到特定的处理程序中去处理。可以通过重启Runloop来处理下一次的事件,同样在超时之后也可以重启Runloop做其他的事儿。(这一段主要讨论的是事件处理和超时情况)
除了上诉两种情况外还可以使用特定模式来运行Runloop。设置超时值和指定特定模式这两个是不冲突的,它们都可以用于启动Runloop。mode会限制事件传递到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。在每次Runloop返回时思考一下是否需要退出线程。
使用递归来启动Runloop也非常简单,换句话说,在输入源或定时器的程序中我们可以使用CFRunLoopRun,CFRunLoopRunInMode或者NSRunLoop的方法来启动Runloop。如果这样做的话,我们可以任何一个模式来嵌套的运行Runloop。
3.3.4 退出Runloop
- 为Runloop配置超时时间值;
- 告知Runloop停止运行;
上诉两个方法可以用来退出Runloop。
首选方案应该是设置超时值,因为这样我们完全可以预料到什么时候退出,能够在退出之前完成所有需要处理的工作,包括在Runloop中各个阶段的通知。
如果要手动去退出Runloop可以使用CFRunLoopStop函数会产生类似超时的结果,这个操作并不是马上就回退出Runloop,而是要把剩余的循环部分处理完成之后再退出。
尽管通过移除Runloop的输入源或者定时器源液能够导致Runloop退出,但这并不是一个可靠的办法。例如,系统会将一些输入源添加到Runloop,但是我们并不知道有这些输入源,也就说明我们没法通过代码去移除它们,因为这些输入源的存在就会导致Runloop不会正常退出(就是说系统会在Runloop中添加一些输入源,而不是我们自己添加的,而这输入源我们又不知道)。
3.3.5 Runloop对象和线程安全
线程安全取决于我们使用的Runloop API(Cocoa和CoreFoundation)。CoreFoundation的函数通常是线程安全的,可以在任何线程调用它。Cocoa的NSRunloop是线程不安全的,所以在使用NSRunloop的时候最好是在当前Runloop的那个线程中进行,不要将输入源或者定时器源添加到不同线程的Runloop中。
本人总结TIPS
- 每个线程都有与之关联的Runloop对象;
- 我们必须要为次级辅助线至少添加一个输入源;
- 只有在辅助线程中才有必要去启动Runloop;
退出Runloop的首选方案还是设置超时值;
使用CFRunLoopStop函数退出Runloop,不会立即退出Runloop;
NSRunloop不是线程安全的,所以最好是在一个线程中去操作source;