iOS RunLoop 探究

3,263 阅读10分钟

RunLoop常见用法

AFN
AFN2.x中把网络请求全部都放在一个子线程中进行。由于子线程运行完任务后就会自动销毁,所以在子线程中运行了一个Runloop保证线程不会被销毁掉。(线程的创建和销毁耗费的资源虽然很少,但是大量网络请求导致大量创建和销毁所耗费的资源还是十分可观的)

#pragma mark AFN
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

用CFRunloop也可建立一个Runloop

    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    //A perform callback for the run loop source. This callback is called when the source has fired.
    //Availability
    context.perform = fire;

    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    NSLog(@"自定义RunLoopRun");
    CFRunLoopRun();

线程与RunLoop的一些概念

线程安全
  • CFRunLoopRef 是线程安全的
  • NSRunLoop 非线程安全
线程运行
  • 子线程执行完任务自动销毁
  • runloop实际是一个循环,并不会自动停止
  • 添加runloop后,run,如果要销毁这个线程,必须要停止runloop。
  • 如果当前线程没有Runloop就

关于AFN 3.x

  • 由于NSUrlSession参考了AF的2.x的优点,自己维护了一个线程池,做Request线程的调度与管理,所以在AF3.x中,没有了常驻线程,都是用的时候run,结束的时候stop。

线程间的通信

  • iOS线程间的通信,实际上是各种输入源,触发Runloop去处理对应的事件。

在什么情况下使用RunLoop

仅当在为你的程序创建辅助线程的时候,你才需要显式运行一个run loop。
Run loop在你要和线程有更多的交互时才需要,比如以下情况:
>

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

RunLoop 详细介绍


Run Loop的处理两大类事件源:Timer Source和Input Source(包括performSelector* 方法簇、Port或者自定义Input Source),每个事件源都会绑定在Run Loop的某个特定模式mode上,而且只有RunLoop在这个模式运行的时候才会触发该Timer和Input Source。

  • Runloop 处理两大类事件源 1.Timer Source 2.Input Source
  • 如果没有任何事件源添加到Run Loop上,Run Loop就会立刻exit
Input Source:传递异步事件,通常消息来源于其他线程或程序。
  • 基于端口的输入源
    • Cocoa和Cocoa Foundation 内置支持使用端口相关的对象和函数来创建的机遇端口的源。
    • Cocoa中只要简单的创建端口对象,将端口添加到Runloop即可。端口对象会自己处理创建和配置输入源。
    • 在Core Fundation中,你必须人工创建端口和他的Runloop源。我们可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建合适的对象。

Example:

void createPortSource()
{

    CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.someport"),myCallbackFunc, NULL, NULL);
    CFRunLoopSourceRef source =  CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    while (pageStillLoading) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
    }

    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}
  • 自定义输入源
    • 自定义输入源需要人工从其他线程发送。
    • 使用Core Fundation中的CFRunLoopSourceRef类型相关的函数来创建。
    • 需要定义消息传递机制
      Example:
      void createCustomSource()
      {
      CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
      CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
      CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
      while (pageStillLoading) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
      }
      CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
      CFRelease(source);
      }
  • Cocoa的Selector源

    • 除了基于端口的源,Cocoa定义了自定义输入源,允许你在任何线程执行selector方法
    • 当在其他线程上面执行selector时,目标线程须有一个活动的run loop。对于你创建的线程,这意味着线程在你显式的启动run loop之前是不会执行selector方法的,而是一直处于休眠状态。(导致Crash
    • 和基于端口的源一样。执行selector请求会在目标线程上序列化,减缓多线程上允许多个方法容易引起的同步问题。
  • 定时源

    • 定时源在预设的时间点同步方式传递消息,这些消息都会发生在特定时间或者重复的时间间隔。定时源直接传递消息给处理例程,不会立即退出run loop.
    • 定时器与runloop中的特定模式有关。
      Example:
      //方法一:
      NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0
                                                     target:self
                                                   selector:@selector(backgroundThreadFire:) userInfo:nil
                                                    repeats:YES];
      [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];
      //方法二:
      [NSTimer scheduledTimerWithTimeInterval:10
                                        target:self
                                       selector:@selector(backgroundThreadFire:)
                                       userInfo:nil
                                       repeats:YES];

事件源按照函数调用栈的分类

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

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

  1. Runloop入口
  2. Runloop何时处理一个定时器
  3. Runloop何时处理一个输入源
  4. Runloop何时进入睡眠状态
  5. Runloop何时被唤醒,但在唤醒之前要处理的事件
  6. Runloop终止
RunLoop事件队列

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

  1. 通知观察者run loop已经启动
  2. 通知观察者任何即将要开始的定时器
  3. 通知观察者任何即将启动的非基于端口的源
  4. 启动任何准备好的非基于端口的源
  5. 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9。
  6. 通知观察者线程进入休眠
  7. 将线程置于休眠直到任一下面的事件发生:
    • 某一事件到达基于端口的源
    • 定时器启动
    • Run loop设置的时间已经超时
    • run loop被显式唤醒
  8. 通知观察者线程将被唤醒。
  9. 处理未处理的事件
    • 如果用户定义的定时器启动,处理定时器事件并重启run loop。进入步骤2
    • 如果输入源启动,传递相应的消息
    • 如果run loop被显式唤醒而且时间还没超时,重启run loop。进入步骤2
  10. 通知观察者run loop结束。

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

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

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

从这个事件队列中可以看出:

①如果是事件到达,消息会被传递给相应的处理程序来处理, runloop处理完当次事件后,run loop会退出,而不管之前预定的时间到了没有。你可以重新启动run loop来等待下一事件。

②如果线程中有需要处理的源,但是响应的事件没有到来的时候,线程就会休眠等待相应事件的发生。这就是为什么run loop可以做到让线程有工作的时候忙于工作,而没工作的时候处于休眠状态。

什么时候使用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,那么它的配置和启动都很简单。和所有线程编程一样,你需要计划好在辅助线程退出线程的情形。让线程自然退出往往比强制关闭它更好。

CFRunLoop 对外接口

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopSourceRef

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理时间,然后手动调用CFRunLoopWeakUp(runloop)来唤醒RunLoop,让其处理这个事件。
  • Source1 包含了一个mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop的线程。

CFRunLoopTimerRef 是基于时间的触发器
CFRunLoopObserverRef 是观察者

Runloop 使用

Run Loop运行接口

  • 要操作Run Loop,Foundation层和Core Foundation层都有对应的接口可以操作Run Loop:
    Foundation层对应的是NSRunLoop,Core Foundation层对应的是CFRunLoopRef;
    两组接口差不多,不过功能上还是有许多区别的:

  • 例如CF层可以添加自定义Input Source事件源、(CFRunLoopSourceRef)Run Loop观察者Observer(CFRunLoopObserverRef),很多类似功能的接口特性也是不一样的。
    NSRunLoop的运行接口:

//运行 NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制
- (void)run;
//运行 NSRunLoop: 参数为运时间期限,运行模式为默认的NSDefaultRunLoopMode模式 
- (void)runUntilDate:(NSDate *)limitDate;
//运行 NSRunLoop: 参数为运行模式、时间期限,返回值为YES表示是处理事件后返回的,NO表示是超时或者停止运行导致返回的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
模式问题

Run Loop运行时只能以一种固定的模式运行,如果我们需要它切换模式,只有停掉它,再重新开启它。
运行时它只会监控这个模式下添加的Timer Source和Input Source,如果这个模式下没有相应的事件源,Run Loop的运行也会立刻返回的。注意Run Loop不能在运行在NSRunLoopCommonModes模式,因为NSRunLoopCommonModes其实是个模式集合,而不是一个具体的模式,我可以在添加事件源的时候使用NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes中任何一个模式,这个事件源都可以被触发。