看苹果官方文档怎么说RunLoop

这几天研究了一下iOS的Runloop,看了不少的文章,收获不少,但是疑问也挺多。所以我就试着去翻译了并分析总结了一下苹果的Runloop文档,注意:并不是CFRunloop的源码,而是这篇总体概述了Runloop的文档

这里先给出一些有关RunLoop的官方文档及一些好的文章的链接:

官方文档:

Core Foundation框架源码

CFRoopLoop源码

苹果对于Runloop的概括说明文档

CFRunLoop的官方参考文档

NSRunLoop的官方参考文档

入门可以看:

《iOS RunLoop入门小结》

《RunLoop入门 看我就够了》

大神文章:

《深入理解RunLoop》

《iOS刨根问底-深入理解RunLoop》

《iOS程序启动与运转——RunLoop个人小结》

RunLoop怎么用:

《RunLoop已入门?不来应用一下?》

RunLoop问题集:

《RunLoop问题集》

《iOS基础面试题之RunLoop篇》


提前说明,这篇文章你可以对照着英文文档来看,目录结构是一样的。但并不是逐句翻译的,会有省略,并加上了我自己的总结分析。

本文目录

  • Run Loops(RunLoop)
  • Anatomy of a Run Loop(RunLoop的剖析)
  • Run Loop Modes(RunLoop的Mode)
  • Input Sources
      1. Port-Based Sources(基于端口的输入源)
      1. Custom Input Sources(自定义输入源)
      1. 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对象)
      1. Getting a Run Loop Object(获取RunLoop对象)
      1. Configuring the Run Loop(配置RunLoop)
      1. Starting the Run Loop(启动RunLoop)
      1. Exiting the Run Loop(退出RunLoop)
      1. Thread Safety and Run Loop Objects(线程安全和RunLoop对象)
  • Configuring Run Loop Sources(配置RunLoop的Source)
      1. Defining a Custom Input Source(定义自定义输入源,即Source0)
      1. Configuring Timer Sources(配置定时器源,即Timer)
      1. Configuring a Port-Based Input Source(配置基于端口的输入源,即Source1)

Run Loops(RunLoop)

RunLoop是与线程相关的基础架构中的一部分。RunLoop是一个事件处理环,它可以用来安排工作并协调传入的事件。RunLoop的目的是在有任务的时候保持线程的运行,在没有任务的时候使线程休眠。

RunLoop的使用管理不是完全自动的。你必须在合适的时机使用线程代码启动RunLoop,并对接收的事件进行反应处理。Cocoa(NSRunLoop)和Core Foundation(CFRunLoop)都提供了RunLoop的对象。

每一个线程都对应着一个RunLoop。在应用程序启动的时候,主线程的RunLoop会自动启动,而辅助线程(即子线程)是需要你手动去获取的

这里有CFRunLoopNSRunLoop的参考文档。

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有三种:

  1. kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)

默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。一般情况下App都是运行在这个mode下的

  1. CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)

一般作用于ScrollView滚动的时候的模式,保证滑动的时候不受其他事件影响。

  1. 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触发的位置(可以去看下面的那张图):

  1. 即将进入RunLoop,通知observer;
  2. 即将处理Timer,通知observer;
  3. 即将处理Source(非端口的输入源),通知observer;
  4. 线程即将休眠,通知observer;
  5. 线程刚被唤醒,但在它处理唤醒它的事件之前,通知observer;
  6. 线程退出了RunLoop,通知observer;

你可以使用Core Foundation添加Observer到应用程序里,需使用CFRunLoopObserverRef类型。

与Timer类似,Observer可以使用一次或重复使用。一次性Observer在触发后将其自身从RunLoop中移除,而重复的Observer会继续存在于RunLoop中,您可以指定Observer在创建时运行一次还是重复运行

The Run Loop Sequence of Events(RunLoop的内部运行逻辑)

每一次运行RunLoop,线程对应的RunLoop就会处理挂起的事件,并通知观察者。它执行的顺序如下

  1. 通知Observer即将进入RunLoop
  2. 通知Observer即将处理Timer
  3. 通知Observer即将处理Source0(非端口的输入源)
  4. 处理Source0(非端口的输入源)
  5. 如果有Source1(基于端口的输入源)准备就绪并等待被触发,立即处理该事件,并跳到步骤9
  6. 通知Observer即将休眠
  7. 线程休眠,直到发生以下事件之一:
    • 一个事件到达Source1(基于端口的输入源)
    • 一个定时器(Timer)触发
    • RunLoop超时
    • RunLoop被手动唤醒(例如添加一个Source0非端口的输入源)
  8. 通知Observer线程刚刚唤醒
  9. 处理待处理的事件
    • 如果用户定义的Timer触发了,则处理这个定时器事件并重新启动RunLoop循环,跳到步骤2
    • 如果输入源触发了,则传递事件
    • 如果RunLoop被手动唤醒,但尚未超时,重新启动RunLoop循环,跳到步骤2
  10. 通知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?)

  1. 你需要显式运行RunLoop的唯一一种情况是,为应用程序创建辅助线程(这里的辅助线程就是指子线程)。因为应用程序的主线程的RunLoop会在应用创建的时候自动启动,所以不需要你去管主线程的RunLoop。

  2. 对于辅助线程,你需要确定是否需要RunLoop,如果是,则自行去配置并启动它。通常在所有的情况下,你都不需要去启动一个线程的RunLoop。比如你要使用一个线程去执行一个长时间运行且预定义的任务,你可以避免去使用RunLoop。

  3. 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退出。

分析说明:

  • 退出的三种方式
      1. 超时
      1. CFRunLoopStop函数显式停止
      1. 删除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的,感兴趣的自己去看吧。

如有错误,烦请指正,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容