Runloop对象提供了为runloop添加输入源,定时器源,Runloop Observer的主要接口,并使用Runloop对象来运行Runloop。每个线程都有与之关联的Runloop对象。Cocoa中Runloop对象是NSRunloop类。在CoreFoundation中是一个CFRunloopRef类型的指针。

3.3.1 获取Runloop对象

使用下列方法在当前线程获取Runloop:

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(k​CFAllocator​Default, 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或者k​CFAllocator​Default。

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:

  1. 无条件启动;
  2. 设置时间限制;
  3. 特定模式下;

进入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也非常简单,换句话说,在输入源或定时器的程序中我们可以使用CFRunLoopRunCFRunLoopRunInMode或者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;

results matching ""

    No results matching ""