RunLoop 浅析

一个小使用

首要咱们需求编写一个使用,这个小使用的要求很简略:它需求履行一些比较耗时的操作,在履行耗时操作的同时还需求能够继续响使用户的操作。

那么首要想到的便是运用两个线程,一个 Main 一个 Worker,在 Main 中响使用户的操作,而将实践的耗时使命放到 Worker 中。

首要看看在不运用 RunLoop 时的代码是怎么完结的:

//
//  main.m
//  Downloader
//
//  Created by mconintet on 11/23/15.
//  Copyright  2015 mconintet. All rights reserved.
//
#import <Foundation/Foundation.h>
// 『音讯行列(messages queue)』这个名词想必是众所周知了
// 这儿 commands 就相当于一个音讯行列的效果
// 主线程在收到了用户的 command 之后并不是
// 当即处理它们,转而将其增加到这个 queue 中,
// 然后 Worker 会逐一的处理这个指令
static NSMutableArray* commands;
// NSMutableArray 并不是 thread-safety,所以
// 需求 @synchronized 来保证数据完整性
void pushCommand(NSString* cmd)
{
    @synchronized(commands)
    {
        [commands addObject:cmd];
    }
}
NSString* popCommand()
{
    @synchronized(commands)
    {
        NSString* ret = [commands lastObject];
        [commands removeLastObject];
        return ret;
    }
}
@interface Worker : NSThread
@end
@implementation Worker
- (void)main
{
	// 如你所见,在 Worker 中咱们
	// 选用了『轮询』的办法,便是不断的
	// 问询音讯行列,是不是有新音讯来了
    while (1) {
        NSString* last = popCommand();
        // 假如经过不断的轮询得到新的指令
        // 那么就处理那个指令
        while (last) {
            NSLog(@"[Worker] executing command: %@", last);
            sleep(2); // 模拟耗时的核算所需的时刻
            NSLog(@"[Worker] executed command: %@", last);
            last = popCommand();
        }
    }
}
@end
int main(int argc, const char* argv[])
{
    @autoreleasepool
    {
        commands = [[NSMutableArray alloc] init];
        Worker* worker = [[Worker alloc] init];
        [worker start];
        int c = 0;
        do {
            c = getchar();
            // 忽略输入的换行
            // 这样 Log 内容愈加明晰
            if (c == 'n')
                continue;
            NSString* cmd = [NSString stringWithCharacters:(const unichar*)&c length:1];
            pushCommand(cmd);
            // 在主线程中 Log 这条信息,
            // 以此来表示主线程能够继续响应
            NSLog(@"[Main] added new command: %@", cmd);
        } while (c != 'q');
    }
    return 0;
}

运转下这个程序,然后切换到 Debug navigator,会看到这样的成果:

RunLoop 浅析

Worker 让 CPU 几乎满了 ,看来 Worker 轮询音讯行列的办法有很大的功用问题。回看 Worker 中这样的代码:

while (1) {
    NSString* last = popCommand();
    while (last) {
        NSLog(@"executint command: %@", last);
        sleep(2); // 模拟耗时的核算所需的时刻
        NSLog(@"executed command: %@", last);
        last = popCommand();
    }
}

上面代码效果便是选用轮询的办法不断的向音讯行列问询是否有新音讯到达。这样的形式会有一个严峻的问题:假如在很长一段时刻内用户并没有输入新的 command,子线程还是会不断的轮询,便是因为这些不断的轮询导致 CPU 资源被占满。

Worker 不断轮询音讯行列的形式现已被咱们证明是具有功用问题的了,那么是不是能够换一种思路?假如能够让 Main 和 Worker 的协作变为这样:

  1. Main 不断地接收到用户输入,将输入放到音讯行列中,然后告知 Worker 说『Wake up,你有新的使命需求处理』
  2. Worker 开端处理音讯行列中使命,使命处理完结之后,主动进入休眠,不再继续占用 CPU 资源,直到接收到下一次 Main 的告知

为了完结这个形式,咱们能够选用 RunLoop。

RunLoop

在运用 RunLoop 之前,先了解下它。详细的在 Run Loops,简明的说:

  1. 每个线程都有一个与之相关的 RunLoop
  2. 与线程相关联的 RunLoop 需求手动的运转,以此让其开端处理使命。主线程现已为你主动的启动了与其关联的 RunLoop(留意指令行程序的主线程并没有这个主动敞开的动作)
  3. RunLoop 需求以特定的 mode 去运转。『common mode』实践上是一组 modes,有相关的 API 能够向其间增加 mode
  4. RunLoop 的目的便是监控 timers 和 run loop sources。每一个 run loop source 需求注册到特定的 run loop 的特定 mode 上,而且只有当 run loop 运转在相应的 mode 上时,mode 中的 run loop source 才有机会在其准备好时被 run loop 所触发
  5. RunLoop 在其每一次的循环中,都会阅历几个不同的场景,比方查看 timers、查看其他的 event sources。假如有需求被触发的 source,那么会触发与那个 source 相关的 callback
  6. 除了运用 run loop source 之外,还能够创建 run loop observers 来追寻 run loop 的处理进展

假如要愈加深化的了解 RunLoop 引荐阅读 深化理解RunLoop

运用 RunLoop 来改写程序

下面的代码运用 RunLoop 来改写上面的程序:

//
//  main.m
//  Downloader
//
//  Created by mconintet on 11/23/15.
//  Copyright  2015 mconintet. All rights reserved.
//
#import <Foundation/Foundation.h>
static NSMutableArray* commands;
void pushCommand(NSString* cmd)
{
    @synchronized(commands)
    {
        [commands addObject:cmd];
    }
}
NSString* popCommand()
{
    @synchronized(commands)
    {
        NSString* ret = [commands lastObject];
        [commands removeLastObject];
        return ret;
    }
}
// run loop source 相关的回调函数
// 在外部代码标记了 run loop 中的某个 run loop source
// 是 ready-to-be-fired 时,那么在未来的某一时刻 run loop
// 发现该 run loop source 需求被触发,那么就会调用到这个与其
// 相关的回调
void RunLoopSourcePerformRoutine(void* info)
{
    // 假如该办法被调用,那么说明其相关的 run loop source
    // 现已准备好。在这个程序中便是 Main 告知了 Worker 『使命来了』
    NSString* last = popCommand();
    while (last) {
        NSLog(@"[Worker] executing command: %@", last);
        sleep(2); // 模拟耗时的核算所需的时刻
        NSLog(@"[Worker] executed command: %@", last);
        last = popCommand();
    }
}
// Main 除了需求标记相关的 run loop source 是 ready-to-be-fired 之外,
// 还需求调用 CFRunLoopWakeUp 来唤醒指定的 RunLoop
// RunLoop 是不能手动创建的,所以有必要注册这个回调来向 Main 暴露 Worker
// 的 RunLoop,这样在 Main 中才知道要唤醒谁
static CFRunLoopRef workerRunLoop = nil;
// 这也是一个 run loop source 相关的回调,它发生在 run loop source 被增加到
// run loop 时,经过注册这个回调来获取 Worker 的 run loop
void RunLoopSourceScheduleRoutine(void* info, CFRunLoopRef rl, CFStringRef mode)
{
    workerRunLoop = rl;
}
@interface Worker : NSThread
@property (nonatomic, assign) CFRunLoopSourceRef rlSource;
@end
@implementation Worker
- (instancetype)initWithRunLoopSource:(CFRunLoopSourceRef)rlSource
{
    if ((self = [super init])) {
        _rlSource = rlSource;
    }
    return self;
}
- (void)main
{
    NSLog(@"[Worker] is running...");
    // 往 RunLoop 中增加 run loop source
    // 咱们的 Main 会经过 rls 和 Worker 协调作业
    CFRunLoopAddSource(CFRunLoopGetCurrent(), _rlSource, kCFRunLoopDefaultMode);
    // 线程需求手动运转 RunLoop
    CFRunLoopRun();
    NSLog(@"[Worker] is stopping...");
}
@end
// 告知 Worker 使命来了
// 把 Worker 拎起来干事
void notifyWorker(CFRunLoopSourceRef rlSource)
{
    if (workerRunLoop) {
        CFRunLoopSourceSignal(rlSource);
        CFRunLoopWakeUp(workerRunLoop);
    }
}
int main(int argc, const char* argv[])
{
    @autoreleasepool
    {
        NSLog(@"[Main] is running...");
        commands = [[NSMutableArray alloc] init];
        // run loop source 的上下文
        // 便是一些 run loop source 相关的选项以及回调
        // 别的咱们这的第一个参数是 0,有必要是 0
        // 这样创建的 run loop source 就被增加在
        // run loop 中的 _sources0,作为用户创建的
        // 非主动触发的
        CFRunLoopSourceContext context = {
            0, NULL, NULL, NULL, NULL, NULL, NULL,
            RunLoopSourceScheduleRoutine,
            NULL,
            RunLoopSourcePerformRoutine
        };
        CFRunLoopSourceRef runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
        Worker* worker = [[Worker alloc] initWithRunLoopSource:runLoopSource];
        [worker start];
        int c = 0;
        do {
            c = getchar();
            if (c == 'n')
                continue;
            NSString* cmd = [NSString stringWithCharacters:(const unichar*)&c length:1];
            pushCommand(cmd);
            NSLog(@"[Main] added new command: %@", cmd);
            notifyWorker(runLoopSource);
        } while (c != 'q');
        NSLog(@"[Main] is stopping...");
    }
    return 0;
}

能够运转一下看下功用怎么:

RunLoop 浅析

能够看到,在没有新的用户输入到达,且音讯行列中没有需求处理的使命时,整个使用程序没有继续的强占 CPU 资源,这就归功于 RunLoop。

最终简略概括下为什么 RunLoop 有这么『神奇』的功用吧。

首要 RunLoop 内部核心也是一个 loop 循环(和它的姓名呼应),然后这个循环中做了一些有意思的作业:

  1. 首要每一次的循环中,都会查看被增加到其间的 timers 和 run loop sources,假如它们之中有符合条件的,那么自然是需求触发相关的回调操作
  2. 假如没有 timers 或许 run loop sources 或许 run loop 被手动的停止了 那么 run loop 会退出内部的循环
  3. 假如被增加到内部的 timers 和 run loop sources 都没有准备好被触发,那么 run loop 就会进行一个系统调用,使线程进入休眠
  4. 进入休眠了就不会占用 CPU 资源,那么唤醒的作业就需求其外部的代码进行,比方上面代码中 Main 中的 notifyWorker

这都是嘛

有这么几个名词真是十分的饶人:RunLoopRunLoop SourceRunLoop ModeCommonMode

『这些都是嘛?』这便是我刚见到它们的感觉,假如你也有这样的感觉,那么再次引荐你先看下 深化理解RunLoop,我也是看了其间内容,然后下载了 RunLoop 的源码,自己动手剖析剖析,接下来将是我剖析的备忘。

首要是看下 RunLoop 的结构:

struct __CFRunLoop {
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
}

所以看到,与 RunLoop 有直接关系的是 RunLoop Mode。那么看看 RunLoop Mode 的结构:

struct __CFRunLoopMode {
	CFStringRef _name;
	CFMutableSetRef _sources0;
	CFMutableSetRef _sources1;
	CFMutableArrayRef _observers;
	CFMutableArrayRef _timers;
}

发现与 RunLoop Mode 有关的是 RunLoop sourcetimer 以及 observer

所以就有了这个图:

+---------------------------------------------------------+
|                                                         |
|                        RunLoop                          |
|                                                         |
|  +----------------------+    +----------------------+   |
|  |                      |    |                      |   |
|  |     RunLoopMode      |    |     RunLoopMode      |   |
|  |                      |    |                      |   |
|  |  +----------------+  |    |  +----------------+  |   |
|  |  | RunLoopSources |  |    |  | RunLoopSources |  |   |
|  |  +----------------+  |    |  +----------------+  |   |
|  |                      |    |                      |   |
|  |    +-----------+     |    |    +-----------+     |   |
|  |    | Observers |     |    |    | Observers |     |   |
|  |    +-----------+     |    |    +-----------+     |   |
|  |                      |    |                      |   |
|  |      +--------+      |    |      +--------+      |   |
|  |      | Timers |      |    |      | Timers |      |   |
|  |      +--------+      |    |      +--------+      |   |
|  |                      |    |                      |   |
|  +----------------------+    +----------------------+   |
|                                                         |
+---------------------------------------------------------+

然后看看 Common Mode 是干什么的,首要看看这个函数:

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName);

便是往 RunLoop 中增加 Common Mode,而 Common Mode 在 RunLoop 中以 Set 的结构去存放(见上面 RunLoop 数据结构中的 CFMutableSetRef _commonModes;),也便是 RunLoop 中能够有多个 Common Mode,而且留意到增加时是以 Mode Name 去代表详细的 Mode 的。

然后再看下这个函数:

void CFRunLoopAddSource(
	CFRunLoopRef rl, 
	CFRunLoopSourceRef rls, 
	CFStringRef modeName
);

这儿就不放函数体了,有爱好的能够下载源码去看,大约的意思便是:

假如 CFRunLoopAddSource 被调用时,形参 modeName 的实参值为 kCFRunLoopCommonModes 时,就会将 rls 增加到 RunLoop 中的 _commonModeItems 中。上面我知道了 _commonModes 其实是一个 Set,里面存放的是 Mode Names,所以下一步 RunLoop 就会迭代 _commonModes 这个 Set 中的元素。关于迭代时的元素,很明显都是 Mode Name,然后经过 __CFRunLoopFindMode 办法,根据 Mode Name 找出存储在 RunLopp 中的 _modes 中的 Mode,然后将 rls 增加到那些 Mode 中。

假如觉得很乱的话,只要知道为什么这么干就行了:

RunLoop 中是有多个 Mode 的,而 RunLoop 需求以指定的 Mode 去运转,而且一旦运转就无法切换到其他 Mode 中。那么当你将一个 rls(run loop source) 增加到 RunLoop 的某一个 Mode 之后,一旦 RunLoop 不是运转在 rls 被增加到的 Mode 上,那么 rls 将无法被检测并触发到,为了解决这个问题,能够将 rls 增加到 RunLoop 中的一切 Modes 中就行了,这样无论 RunLoop 作业在哪一个 Mode 上 rls 都有机会被检测和触发。

这是关于上面描绘的一个详细比如:

使用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都现已被标记为”Common”属性。DefaultMode 是 App 平常所处的状况,TrackingRunLoopMode 是追寻 ScrollView 滑动时的状况。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此刻滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,而且也不会影响到滑动操作。

那么怎么将 rls 增加到 RunLoop 一切的 Modes 中呢?所以供给了这样的办法:

CFRunLoopAddSource(
	CFRunLoopRef rl, 
	CFRunLoopSourceRef rls, 
	CFStringRef kCFRunLoopCommonModes // 留意到 kCFRunLoopCommonModes 了吗
); 

暂时就这么多,enjoy!