最近在研究检测页面卡顿的问题,发现基本上都是基于Runloop的,所以打算把Runloop再好好复习一遍,于是就把学习的过程记录一下。
在OSX/iOS 系统中,提供了两个对象:NSRunLoop
(线程不安全) 和 CFRunLoopRef
(线程安全)。但是NSRunloop
是闭源的,我们无法直接查看到内部实现,但是CFRunLoopRef
是开源的,CFRunLoopRef
在 CoreFoundation
框架内,它提供了纯 C 函数的 API,接下来我们就分析一下CFRunLoopRef
的内部实现。目前最新的版本是 CF-855.17
.
在开发中,我们可以通过两种方式来获取线程,
NSThread:
[NSThread mainThread]
获取主线程[NSThread currentThread]
获取当前线程
pthread:
pthread_main_np()
获取主线程pthread_self()
获取当前线程
CFRunLoop 是基于 pthread 来管理的。
可以看出这两者是一一对应的,但是没有找到这两者的转换方式. 不过我们可以用pthread_t pthread_from_mach_thread_np(mach_port_t)
进行pthread_t
和mach_port_t
两者的转换。 task_threads
这个函数可以返回任务中的线程列表。
mach_msg_type_number_t count;
thread_act_array_t list;
task_threads(mach_task_self(), &list, &count);
我们不能直接创建RunLoop
,我们也可以通过两种方式来获取,
NSRunLoop:
[NSRunLoop mainRunLoop];
获取主runloop[NSRunLoop currentRunLoop];
获取当前runloop
CFRunLoopRef:
CFRunLoopGetMain()
获取主runloopCFRunLoopGetCurrent()
获取当前runloop
我们先来看一下CFRunLoopGetMain
函数的实现方式:
1 | CFRunLoopRef CFRunLoopGetMain(void) { |
CFRunLoopGetCurrent
函数的实现方式:
1 | CFRunLoopRef CFRunLoopGetCurrent(void) { |
看到这两个函数都调用了_CFRunLoopGet0
这个函数,可见这个函数才是核心,看下这个函数的实现:
1 | // key是pthread_t, value是CFRunLoopRef |
从上面的代码可以看出,线程
和RunLoop
之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
接下来我们可以在CFRunLoop.h
文件中看到几个对外提供的接口:
1 | typedef CFStringRef CFRunLoopMode CF_EXTENSIBLE_STRING_ENUM; |
在CFRunLoop.c
文件看到了还有__CFRunLoopMode
结构体,
1 | struct __CFRunLoopMode { |
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
1 | // 指定一个Mode启动,允许设置超时时间 |
1 | // 使用kCFRunLoopDefaultMode启动 |
实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回.
1 | // runloop的实现 |
通过代码我们可以看出:一个 RunLoop 包含多个 Mode,每个 Mode 又包含多个 Source/Timer/Observer。每次调用CFRunLoopRunInMode
时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
Runloop内部逻辑
我们给当前runloop添加一个观察者:
1 | static void callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity,void *info) { |
我们可以自己写一个timer试验一下,发现如下打印:
1 | /** |
执行逻辑跟图示一样
Runloop线程保活
从苹果的官方文档看,我们可以看到runloop开启有这四种方式:
1 | - (void)run; |
我们先来看下这个方法,它会开启一个永驻的runloop,来处理Input Source,内部会循环调用runMode:beforeDate,并且运行在NSDefaultRunLoopMode这个模式下,即使用
void CFRunLoopStop(CFRunLoopRef rl)
也无法停止runloop的运行,除非能移除这个runloop上的所有事件源,包括timer和source,不然这个子线程就无法停止,只能永久运行下去。
但是并不建议我们使用这个方法来开启,如果我们想要停止runloop。我们可以采用这种方式,
1 | BOOL shouldKeepRunning = YES; // global |
当条件
shouldKeepRunning
设置为NO的时候,我们就可以退出runloop
1 | - (void)runUntilDate:(NSDate *)limitDate; |
这个方法我们可以设置一个超时时间,如果没有检测到timer和source输入源,runloop则立刻退出,否则当到达超时时间的时候才会退出,这个方法也是运行在NSDefaultRunLoopMode模式下的,需要注意的是
void CFRunLoopStop(CFRunLoopRef rl)
也无法停止runloop的运行
1 | BOOL shouldKeepRunning = YES; // global |
这个方法无非就是每隔5秒退出一次,然后判断自己需要做的事并可以设置shouldKeepRunning是否需要设置为NO
1 | - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; |
这个方法比上面的方法多了一个mode参数,这种方式是可以使用
void CFRunLoopStop(CFRunLoopRef rl)
停止runloop运行的。但是需要注意的是,这种方法会导致runloop退出,我们看如下代码:
1 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ |
我们会发现打印 :
2017-06-20 10:14:45.653 CodeTest[47702:5665253] 线程开始
2017-06-20 10:14:45.654 CodeTest[47702:5665253] 线程结束
也就是说根本不会执行
recieveMsg
这个方法,因为这个线程执行完,没有检测到输入源就会立刻退出runloop。需要注意的是,如果我们在- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
这个方法中把wait设置成了YES,则如果线程退出了话,就会crash
但是如果我们把
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
这行代码打开的话,会出现如下打印:
2017-06-20 10:27:02.673 CodeTest[47749:5678254] 线程开始
2017-06-20 10:27:04.867 CodeTest[47749:5678254] 收到消息了,在这个线程:<NSThread: 0x608000069fc0>{number = 3, name = (null)}
2017-06-20 10:27:04.867 CodeTest[47749:5678254] 线程结束
那么为什么添加一个端口就可以让线程不退出呢? 添加一个端口监听这个端口的事件,这个就是我们之前所说的source1,保证runloop有输入源,也就是保证runloop不会退出,这也是一种线程间的通信方式-基于端口的通信。
1
2
3
4
5
6
7
8 CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}如果一个 Source1 (基于port) 发出事件了,就处理这个事件,这就是为什么有port能线程保活的原因。
线程通信
线程间的通信,实际上是各种输入源,触发runloop去处理对应的事件,输入源会异步的发送消息给你的线程。事件来源取决于输入源的种类:基于端口的输入源和自定义输入源。
- 基于端口的输入源监听程序相应的端口。
- 自定义输入源则监听自定义的事件源。
两类输入源的区别在于:基于端口的输入源由内核自动发送,而自定义的则需要人工从其他线程发送。
基于端口的输入源:
在runloop中,被定义名为souce1。Cocoa和CoreFoundation内置支持使用端口相关的对象和函数来创建的基于端口的源。在Cocoa里面你从来不需要直接创建输入源。你只要简单的创建端口对象,并使用NSPort的方法把该端口添加到runloop。端口对象会自己处理创建和配置输入源。
Cocoa里用来线程间传值的是NSMachPort,它的父类是NSPort。
1 | NSPort *port = [NSPort port]; |
然后发现怎么创建都是返回 NSMachPort
对象。。。。。
利用NSMachPort来实现线程通信的🌰
1 | NSPort *mainPort = [NSPort port]; |
出现如下打印:
2017-06-20 12:05:34.441 CodeTest[48264:5786736] 收到消息了,线程为:<NSThread: 0x608000261800>{number = 3, name = (null)}
2017-06-20 12:05:34.441 CodeTest[48264:5786736] hello world!
说明我们从主线程往子线程发送了消息
需要注意的是,components这个传参数组里面只能装两种类型的数据,一种是NSPort的子类,一种是NSData的子类。
自定义输入源,自定义输入源必须是使用CoreFoundation里面的
CGRunLoopSourceRef
类型相关的函数来创建。
1 | CFRunLoopRef _runLoopRef; |
出现如下打印:
2017-06-20 12:27:39.164 CodeTest[48418:5812271] starting thread…….
2017-06-20 12:27:44.165 CodeTest[48418:5812163] RunLoop 正在等待事件输入
2017-06-20 12:27:44.165 CodeTest[48418:5812271] 我现在正在处理后台任务
2017-06-20 12:27:44.165 CodeTest[48418:5812271] hello
2017-06-20 12:27:44.165 CodeTest[48418:5812271] end thread…….
最后,学习还请看大神的文章
最后更新: 2023年03月25日 22:39:55
本文链接: http://aeronxie.github.io/post/cf8f80df.html
版权声明: 本作品采用 CC BY-NC-SA 4.0 许可协议进行许可,转载请注明出处!