目录
概念
一般来讲线程一次只能执行一个任务,执行完后就会退出,我们现在想实现一个功能:线程一直在处理事件并且不会退出,这就是我们Runloop
的作用。其实很像runloop的名字所表示的,绕着一个圈圈一直跑。
要实现runloop这种模型一个关键点就是怎么样去管理事件/消息,让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒
runloop就是一个可以管理需要处理的消息与事件的一个对象,CFRunLoopRef
可以理解为在 CoreFoundation
框架内的NSRunLoop,它提供了纯 C 函数的 API,但是它的API都是线程安全的,而NSRunLoop却不是线程安全的
RunLoop与线程的关系
基本上所有的线程操作的底层都是对pthread_t
的封装
而关于RunLoop和线程,苹果不允许直接创建Runloop
,它只提供了两个自动获取的函数:CFRunLoopGetMain
() 和 CFRunLoopGetCurrent
()
CFRunLoopGetMain的实现如下:

这当中用到了函数**_CFRunLoopGet0**,其实现如下:
objectivec
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//如果t不存在,则标记为主线程(即默认情况,默认是主线程)
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
//创建全局字典,标记为kCFAllocatorSystemDefault
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//通过主线程 创建主运行循环
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//利用dict,进行key-value绑定操作,即可以说明,线程和runloop是一一对应的
// dict : key value
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//通过其他线程获取runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
//如果没有获取到,则新建一个运行循环
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//将新建的runloop 与 线程进行key-value绑定
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
从上面的代码可以看出,线程和 RunLoop 之间是一一对应 的,其关系是保存在一个全局的 Dictionary 里 线程刚创建时并没有runloop
,我们需要主动获取,系统才会自动帮我们创建runloop
并加到字典中
Runloop对外的接口
通过前两个函数我们可以分别获得当前线程的CFRunLoop对象和主线程的CFRunLoop对象,但是只是获得了,如果想要让Runloop运行起来,就还需要一些别的操作。首先先看一下runloop的一些具体结构
在CoreFoundation里面关于RunLoop有5个类:
-
CFRunLoopRef
-
CFRunLoopModeRef
-
CFRunLoopSourceRef
-
CFRunLoopTimerRef
-
CFRunLoopObserverRef
一个Runloop可以包含多个Mode,CFRunLoopModeRef
类没有对外暴露,只是通过CFRunLoopRef
的接口进行了封装,他们的关系如下:

可以看到每个model中包含了Source/Timer/Observer的集合,每次调用RunLoop的主函数时,只能指定其中一个Mode ,这个Mode被称作CurrentMode,如果要切换Mode,必须退出Loop,再重新指定一个Mode进入,这样做可以分隔开不同组的Source/TImer/Observer,避免互相影响
接下来我们分别来看看Source/Timer/Observer这三种结构,首先是Source
CFRunLoopSourceRef
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。两个的区别主要是RunLoop事件源的不同:
-
Source0:处理非基于端口的事件(如程序内部自定义的事件)
-
Source1:处理基于端口的事件(如来自内核的Mach端口信息)
Source0 需要手动标记(CFRunLoopSourceSignal
)并唤醒 RunLoop 才能触发,而 Source1 会自动唤醒 RunLoop。
需要明确一个概念,RunLoop主要用来处理异步事件,如用户输入、定时器触发、网络响应等 ,这些事件通常被封装成事件源,然后由RunLoop在适当的时机调度和处理。
Source0
Source0 只包含了一个回调(函数指针),它并不能主动触发事件
使用时,先调用CFRunLoopSourceSignal(source0)
将 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop)
来唤醒 RunLoop,让其处理这个事件
objectivec
// 假设有一个方法,用于处理按钮点击
- (void)buttonClicked {
// 手动触发RunLoop的Source0
CFRunLoopSourceSignal(source0);
CFRunLoopWakeUp(CFRunLoopGetCurrent()); // 唤醒RunLoop来处理事件
}
// 配置Source0
- (void)setupSource0 {
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &callout};
source0 = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source0, kCFRunLoopDefaultMode);
}
// Source0的回调函数
void callout(void *info) {
NSLog(@"Source0 event triggered.");
}
Source1
Source1
包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。
objectivec
// 配置Source1
- (void)setupSource1 {
CFRunLoopSourceContext1 context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, &perform, NULL};
CFMessagePortRef localPort = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.example.app.port"), &callback, &context, false);
CFRunLoopSourceRef source1 = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, localPort, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source1, kCFRunLoopCommonModes);
}
// Source1的回调函数
CFDataRef callback(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void *info) {
NSLog(@"Received message: %d", msgid);
return NULL;
}
// Source1的事件执行
void perform(void *info) {
NSLog(@"Performing work in response to external event.");
}
使用场景: • 处理来自其他进程的数据或信号。 • 监听系统级事件或网络事件。
CFRunLoopTimer
这是一个基于时间的触发器,包含一个时间长度和一个回调,当其加入到 RunLoop
时,RunLoop
会注册对应的时间点,当时间点到时,RunLoop
会被唤醒以执行那个回调。
CFRunLoopObserver
这是用来监视和相应RunLoop的特定活动的一种对象,通过 CFRunLoopObserver
,开发者可以在 RunLoop
的不同阶段插入自定义的代码来执行特定的任务
可以观测的时间点有以下几个:

我们在上面讲的 Source/Timer/Observer
被统称为 mode item,一个item被重复添加到同一个mode时不会多次执行,但是如果一个mode中一个item都没有,runloop会自动退出,不会进出循环
RunLoop的Mode
刚才在上文讲了mode item,modeitem是被加到mode中的,我们现在讲一下Runloop的Mode
这里有个概念叫CommonModes ,一个Mode可以把自己标记成"Common "属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中),每当Runloop的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 Common标记的所有Mode里。
应用场景
比如说之前写项目时经常遇到的那个滑动时计数器停止计时的问题,用这个点就可以回答:
主线程的Runloop
中有两个预置的Mode
:
kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
DefalutMode是App平时所处的状态,TrackingMode是当滑动时所处的状态,当我们创建NSTimer添加到DefalutMode中,Timer会得到重复回调,但是当我们滚动我们的TableView时,Runloop会切换Mode,由DefalutMode切换为TrackingMode,此时Timer会停止同时不会进行回调,也不会影响到滑动的操作
这时想让滑动时NSTimer可以继续运作的话,有两个方法:
-
一种方法就是将Timer分别加入到两个Mode
-
另一种方法就是将NSTimer加到最顶层的RunLoop 的 commonModeItems,加入后的ModeItems类型会被Runloop加到具有common属性的Mode中去,也就是直接将Timer同时加到defaultMode与TrackMode中去
iOS中有5种Mode:
苹果公开的三种有:
-
NSDefaultRunLoopMode(kCFRunloopDefaultMode):默认状态,app通常在这个mode下运行
-
UITrackingRunLoopMode:界面跟踪mode(例如滑动scrollview时不被其他mode影响)
-
NSRunLoopCommonModes(kCFRunLoopCommonModes):是 前两个mode的集合,可以把自定义mode用CFRunLoopAddCommonMode函数加入到集合中
还有两种只需了解:
-
GSEventReceiveRunLoopMode
:接收系统内部mode,通常用不到 -
UIInitializationRunLoopMode
:私有,只在app启动时使用,使用完就不在集合中了
Runloop的内部逻辑
Runloop的逻辑有一张非常经典的图:

Runloop应用
tableView延迟加载图片,保证流畅
在快速滑动tableView时,滑动过的图片会一直加载,但滑动过的图片都不是我们想要呈现的图片,如果加载就浪费CPU资源,用RunLoop就可以避免滑动时加载图片
给ImgaeView的加载图片的方法指定只有在DefalutMode下才能加载,滑动时不加载图片
objectivec
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName.png"] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
Timer不被ScrollView的滑动影响
上面其实已经讲过这个问题了,除了刚才提到的两个方法,还有一种方法:
用GCD创建定时器,它不会受到RunLoop影响
AFNetworking
在多线程中,线程执行完任务就会退出,那么如果需要反复执行任务的话,就会频繁地创建与销毁线程,这样不仅效率低下,还增加了系统的开销,因此如果有一个常驻线程来处理这些任务就可以避免这种情况。
一个RunLoop中如果没有Observer/Timer/Source等items,Runloop会自动退出,因此我们创建一个空的port发送消息给Runloop,以至于Runloop不会退出而是一直常驻

PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay:
后,其内部会自动创建一个Timer加到Runloop中,当时间到了执行回调,如果当前线程没有Runloop,此方法也会失效