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();}}
}@endint 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,会看到这样的结果:

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);// 线程需要手动运行 RunLoopCFRunLoopRun();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;
}
复制代码

可以运行一下看下性能如何:

可以看到,在没有新的用户输入到达,且消息队列中没有需要处理的任务时,整个应用程序没有持续的霸占 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!

转载于:https://juejin.im/post/5c400361518825258124f26b

RunLoop 浅析相关推荐

  1. 浅析 JavaScript 中的 函数 uncurrying 反柯里化

    柯里化 柯里化又称部分求值,其含义是给函数分步传递参数,每次传递参数后部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果. 因此柯里化的过程是 ...

  2. 浅析Python中bytes和str区别

    本博转载自:Chown-Jane-Y的浅析Python3中的bytes和str类型 Python 3最重要的新特性之一是对字符串和二进制数据流做了明确的区分.文本总是Unicode,由str类型表示, ...

  3. 学习《Linux设备模型浅析之设备篇》笔记(深挖一)

    这篇文章既然说了是浅析,那就是跳过了一些东西,我们把这些跳过的东西给它尽可能的补回来 今天登陆 lxr.free-electrons.com 发现内核版本已经升级到3.15了,那以后都使用3.15的源 ...

  4. 学习《Linux设备模型浅析之设备篇》笔记(一)

    最近在学习Linux设备模型,前面几篇文章也是读这篇的时候遇到问题,然后为了搞清楚先转去摸索才写出来的. 当然了,刚开始是先读到<Linux那些事儿之我是Sysfs>,搞不清楚才去读的&l ...

  5. 2016 - 1- 21 - RunLoop使用(2016-1-24修改一次)(2016 - 1 - 24 再次修改)

    一:常驻线程 :当需要一个线程一直处理一些耗时操作时,可以让它拥有一个RunLoop.具体代码如下:    1.通过给RunloopMode里加源来保证RunLoop不直接退出. 这里有个很重要得知识 ...

  6. SRWebSocket源码浅析(上)

    2017-06-12 涂耀辉 Cocoa开发者社区 一. 前言: WebSocket协议是基于TCP的一种新的网络协议.它实现了浏览器与服务器全双工(full-duplex)通信--可以通俗的解释为服 ...

  7. iOS RunLoop详解

    一.简介 CFRunLoopRef源码 RunLoop是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件.UI刷新事件.定时器事件.Selector事件),从而保持程序的 ...

  8. runLoop和runtime的分析

    一.RunLoop: Runloop是事件接收和分发机制的一个实现. Runloop提供了一种异步执行代码的机制,不能并行执行任务. 在主队列中,Main RunLoop直接配合任务的执行,负责处理U ...

  9. 在ARC环境中autoreleasepool(runloop)的研究

    引言 最近有个大佬考察了我关于autoreleasepool的了解, 之前一直认为自己了解, 但是稍微一问深, 自己却哑口无言. 仔细思考了下, 决定要将这个问题结合之前的知识从新梳理一下, 当然, ...

最新文章

  1. 快意报表存储过程标准写法
  2. druid连接池mysql5.7_Spring Boot 使用Druid连接池整合Mybatis-Plus连接Mysql数据库
  3. 成功启动spark集群程序,使用多台电脑进行计算
  4. tampermonkey怎么不能用了_发现键盘数字小键盘不能用怎么办?
  5. freetextbox java_FreeTextBox 增加按钮
  6. java应用servlt_关于Java:将Servlet用于非Servlet应用程序
  7. finally中关闭资源
  8. Padavan完整编译教程
  9. 综合扫描 -- Ladon
  10. 数字电路与逻辑设计习题
  11. 重写QLabel实现图片显示框选截取保存
  12. 程序员2020年必看的10部影视作品
  13. 音视频开发-音频数据处理流程
  14. Mock数据Mustache语法学习记录
  15. 计时器(Chronometer)的使用
  16. WinRAR解压War包
  17. linux 汇编 push rbp,无法从汇编(yasm)代码调用64位Linux上的C标准库函数
  18. Typescript(一)
  19. 人工智能里的数学修炼 | 隐马尔可夫模型:基于EM的鲍姆-韦尔奇算法求解模型参数
  20. 【笔记】复旦微FM33L026实现远程升级

热门文章

  1. JavaScript中typeof的用法
  2. 如何让一滴水不蒸发?
  3. linux 下挂载光盘
  4. 小试“ASUS WL-500W无线路由”
  5. webpack组织模块的原理 - 基础篇
  6. Android开发——内存优化 图片处理
  7. Mysql5.6主从复制-基于binlog
  8. 为自己给操作系统留个后门?我的安全管理经验谈
  9. 域控服务器发生w32time错误
  10. 在 .NET 框架程序中通过DllImport使用 Win32 API