JHHK

欢迎来到我的个人网站
行者常至 为者常成

参考:RunLoop(一)

参考书籍 《多线程编程指南》
原著:Apple Inc.
翻译:謝業蘭 【老狼】

目录

什么是runloop?

Run loops 是线程相关的的基础框架的一部分。一个 run loop 就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。使用 run loop 的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。

每个线程,包括程序的主线程都有与之对应的 run loop object。只有辅助线程才需要显式的运行它的 run loop。在 Carbon 和 Cocoa 程序中, 主线程会自动创建并运行它的 run loop,作为一般应用程序启动过程的一部分。

基本作用

保持程序的持续运行

处理App中的各种事件比如触摸事件、定时器事件等

RunLoop剖析

它是一个循环,你的线程进入并使用它运行事件处理程序,该程序会响应输入的事件。
你的代码要提供实现循环部分的控制语句,换言之就是要有 while 或 for 循环语句来驱动 run loop。在你的循环中,使用 run loop object 来运行事件处理代码,它响应接收到的事件并启动处理程序。

Run loop 接收输入事件来自两种不同的来源:输入源(input source)和定时源 (timer source)。两种源都使用程序的某一特定的处理例程来处理到达的事件。
输入源:传递异步事件,通常消息来自于其他线程或程序。输入源传递异步消息给相应的处理例程,并调用 runUntilDate:方法来退出。
定时源:传递同步事件,发生在特定时间或者重复的时间间隔。定时源则直接传递消息给处理例程,但并不会退出 run loop。

一、RunLoop模式
1.Run loop 模式是输入源和定时源以及要通知观察者的集合。在coreFoundation框架下Mode的数据结构为:

typedef struct __CFRunLoopMode * CFRunLoopModeRef;
struct __CFRunLoopMode{
    CFStringRef _name;
    CFMutableSetRef _source0;
    CFMutableSetRef _source1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
}

2.一个run loop 包含若干个Mode,每个Mode又包含 _source0 _source1 _observers _timers 。在coreFoundation框架下常见的Mode有两种:

//app默认Mode,通常主线程是在这个mode下运行
NSDefaultRunLoopMode

//界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
UITrackingRunLoopMode

3.一个run loop 只能运行在一个Mode下,所以每次运行你的 run loop,你都要指定其运行的模式。如果要切换Mode,只能退出当前loop,再重新选择一个Mode进入。这样的好处是 run loop 在运行过程中,只有和该模式相关的源才会被监视并允许传递事件消息。和其他模式关联的源只有在该模式下才会运行,否则处于暂停状态。
举个例子:NSTimer

//该timer 只能在NSDefaultRunLoopMode模式下工作
//当程序中有ScrollView滑动时 runloop 会在 UITrackingRunLoopMode 模式下运行,此时timer就失效了
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"timer调用");
}];

//解决办法 将该timer也添加到 UITrackingRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

//或者 NSRunLoopCommonModes 不是一个具体的模式,代表多个模式的集合
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

举个例子:NSTimer

//该timer默认没有加入到任何Mode下 所以直接这么写是不会触发timer的事件的
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"timer调用");
}];

//在默认模式下 运行timer
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

//在追踪模式下 运行timer
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

//在多模式下 运行timer
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

二、输入源

1.基于端口的输入源
你只要简单的创建端口对象,并使用 NSPort 的方法把该端口添加到 run loop。端口对象会自己处理创建和配置输入源。
更多例子关于如何设置和配置一个自定义端口源,参阅“配置一个基于端口的输 入源”部分。

[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];

2.自定义输入源
关于创建自定义输入源的例子,参阅“定义一个自定义输入源”。关于自定义输 入源的信息,参阅 CFRunLoopSource Reference。

3.Cocoa 执行 Selector 的源

ViewController * obj = [[ViewController alloc] init];

//waitUntilDone:参数传YES立马执行,传NO 加入runloop等待合适的时机执行 可能是下一个循环
[obj performSelectorOnMainThread:@selector(myThreadMainMethod:) withObject:@"test" waitUntilDone:NO];

sleep(5);

NSLog(@"end");

-(void)myThreadMainMethod:(id)obj{
    NSLog(@"thread=%@",[NSThread currentThread]);
    NSLog(@"obj=%@",obj);
}

另外还有其它执行selector源的方法

performSelectorOnMainThread:withObject:waitUntilDone:mod
  
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
 
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
 
/**
Lets you cancel a message sent to the current thread using the
performSelector:withObject:afterDelay:
or
performSelector:withObject:afterDelay
*/ 
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
   

三、定时源
定时源在预设的时间点同步方式传递消息。定时器是线程通知自己做某事的一种方法。

尽管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定 时器也和你的 run loop 的特定模式相关。如果定时器所在的模式当前未被 run loop 监视,那么定时器将不会开始直到 run loop 运行在相应的模式下。类似的,如果定 时器在 run loop 处理某一事件期间开始,定时器会一直等待直到下次 run loop 开始 相应的处理程序。如果 run loop 不再运行,那定时器也将永远不启动。

你可以配置定时器工作仅一次还是重复工作。重复工作定时器会基于安排好的时 间而非实际时间调度它自己运行。举个例子,如果定时器被设定在某一特定时间开始并5秒重复一次,那么定时器会在那个特定时间后5秒启动,即使在那个特定的触发时间延迟了。如果定时器被延迟以至于它错过了一个或多个触发时间,那么定时器会在下一个最近的触发事件启动,而后面会按照触发间隔正常执行。

NSLog(@"[lilog]:star");

[NSTimer scheduledTimerWithTimeInterval:5 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"[lilog]:timer");

}];

sleep(2);

/**
定时器被设定在37秒这个特定时间开始,并5秒重复一次 那第一次特定的触发时间就是37+5=42秒。    
由于线程休眠2秒,可能导致触发时间延迟,但2秒小于5秒间隔,那么42秒就是第一次触发时间。
之后按照正常的触发间隔执行
22:01:37.669227+0800 ThreadDemo[1620:1002639] [lilog]:star
22:01:42.670425+0800 ThreadDemo[1620:1002639] [lilog]:timer
22:01:47.670311+0800 ThreadDemo[1620:1002639] [lilog]:timer
22:01:52.669979+0800 ThreadDemo[1620:1002639] [lilog]:timer
22:01:57.669944+0800 ThreadDemo[1620:1002639] [lilog]:timer
*/


如果改成 sleep(8);    
/**
定时器被设定在34秒这个特定时间开始,并5秒重复一次 那第一次特定的触发时间应该是34+5=39秒。    
但由于线程休眠8秒,导致错过了39秒这个触发时间,那么42秒就是下一个runloop循环最近的触发事件的时间,所以就会立即触发。
第二次触发时间是34+10=44,之后按照正常的触发间隔执行

21:40:34.052526+0800 ThreadDemo[1614:996674] [lilog]:star
21:40:42.157996+0800 ThreadDemo[1614:996674] [lilog]:timer
21:40:44.053417+0800 ThreadDemo[1614:996674] [lilog]:timer
21:40:49.053165+0800 ThreadDemo[1614:996674] [lilog]:timer
21:40:54.053062+0800 ThreadDemo[1614:996674] [lilog]:timer
21:40:59.052956+0800 ThreadDemo[1614:996674] [lilog]:timer
*/

四、Run Loop 观察者
run loops 也会生成关于 run loop 行为的通知 (notifications)。注册的 run loop 观察者(run-loop Observers)可以收到这些通知,并在线程上面使用它们来做额外的处理。
和定时器类似,run loop 观察者可以只用一次或循环使用。若只用一次,那么在 它启动后,会把它自己从 run loop 里面移除,而循环的观察者则不会。你在创建 run loop 观察者的时候需要指定它是运行一次还是多次。

  • Run loop 入口
  • Run loop 何时处理一个定时器
  • Run loop 何时处理一个输入源
  • Run loop 何时进入睡眠状态
  • Run loop 何时被唤醒,但在唤醒之前要处理的事件
  • Run loop 终止
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}

// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

五、Run Loop 的事件队列
每次运行 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 结束。

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

img


行者常至,为者常成!





R
Valine - A simple comment system based on Leancloud.