什么是runloop
RunLoop实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后,就会处于这个函数内部的循环中,直到循环结束,函数返回。
RunLoop是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
RunLoop和线程是绑定在一起的,每条线程都有唯一一个与之对应的RunLoop对象。
不能自己创建RunLoop对象,但是可以获取系统提供的RunLoop对象。
主线程的RunLoop对象是由系统自动创建好的,在应用程序启动的时候会自动完成启动,而子线程中的RunLoop对象需要我们手动获取并启动。
Runloop解决了什么问题?
比如使用UIScrollView时:默认情况下,主RunLoop始终在应用程序中运行;它处理来自系统的消息并将其传输到应用程序。例如用户单击屏幕时的事件。
应用程序中的所有计时器都在运行循环上执行。
它是如何工作的?
RunLoop是一个循环,它有几种操作模式,可以帮助我们了解何时运行特定任务。
RunLoop可以采用以下模式:
-
Default 模式:在这种模式下,RunLoop是自由的,可以处理各种常规事件,比如用户触摸、计时器、网络请求回调等。因为它适合处理各种任务,所以称为“默认模式”。例如,在这个模式下,可以安全地执行耗时的计算或其他需要占用大量资源的操作。
-
Tracking 模式:当用户在进行一些需要快速响应的操作时(如滚动视图),RunLoop会切换到Tracking模式。这种模式下,RunLoop会暂时停止处理其他不重要的事件,以确保用户的操作能够得到迅速响应,从而避免卡顿。例如,当用户拖动列表时,这种模式会保证滚动的流畅性,而不被其他后台任务打断。
-
Initialization 模式:Initialization模式仅在初始化阶段使用。比如在应用程序刚启动或者线程刚创建时,RunLoop会进入这个模式来执行初始化工作。一旦初始化完成,RunLoop会退出这个模式。一般情况下,开发者不需要直接处理这个模式。
-
EventReceive 模式:这是一个系统级别的内部模式,用于接收和处理系统事件。开发者通常不会直接与这个模式交互,它主要用于系统内部的事件管理,例如处理硬件中断或系统通知等。
-
Common 模式:Common模式是一个占位符,用于将多个模式组合在一起。在实际使用中,开发者可以将某些事件源(如输入源和定时器)加入到Common模式,这样这些事件源可以在多个模式下共享。例如,可以将定时器添加到Common模式,使其在Default和Tracking模式下都能被触发。这有助于简化事件管理,避免在不同模式下重复添加事件源。
在主RunLoop上,这些模式会自动切换;开发人员可以使用它们来执行耗时的任务,这样用户就不会注意到界面挂起。
runloop的获取
两种获取方式:
// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象
runloop的三种启动方式
//该方法会让 RunLoop 一直运行,除非有特定条件让它停止。
- (void)run;
//该方法设置了超时时间,如果超过了这个时间就会停止。但是如果提前把所有事件都处理完毕也会提前退出。
- (void)runUntilDate:(NSDate *)limitDate;
//方法RunLoop会运行一次,超时时间到达或者第一个source被处理,则RunLoop就会退出。
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
runloop和线程
- RunLoop和线程是绑定在一起的,每条线程都有唯一一个与之对应的RunLoop对象。
- 不能自己创建RunLoop对象,但是可以获取系统提供的RunLoop对象。
- 主线程的RunLoop对象是由系统自动创建好的,在应用程序启动的时候会自动完成启动,而子线程中的RunLoop对象需要我们手动获取并启动。
runloop和线程的关系:

RunLoop在线程中不断检测,通过input source和timer source接受事件,然后通知线程进行处理事件。
runloop的结构
struct __CFRunLoop {
CFRuntimeBase _base;// 基本的运行时结构,包含对象的引用计数等信息
_CFRecursiveMutex _lock;// 递归锁,用于保护模式列表的访问
__CFPort _wakeUpPort;// 用于CFRunLoopWakeUp的端口
volatile _per_run_data *_perRunData;// 每次运行RunLoop时重置的数据
_CFThreadRef _pthread;// 线程引用
uint32_t _winthread; // Windows线程引用
CFMutableSetRef _commonModes;// 通用模式集合
CFMutableSetRef _commonModeItems; // 通用模式的项目集合
CFRunLoopModeRef _currentMode; // 当前运行的模式
CFMutableSetRef _modes;// 所有模式的集合
struct _block_item *_blocks_head;// 块项链表的头部
struct _block_item *_blocks_tail;// 块项链表的尾部
CFAbsoluteTime _runTime;// RunLoop的运行时间
CFAbsoluteTime _sleepTime;// RunLoop的休眠时间
CFTypeRef _counterpart;// 对应的类型引用
_Atomic(uint8_t) _fromTSD;// 原子变量,用于线程本地存储数据
Boolean _perCalloutARP;// 每次回调的自动参考计数标志
CFLock_t _timerTSRLock;// 定时器TSR锁
};
一个RunLoop对象包含一个线程(_pthread)
,若干个mode(_modes)
,若干个commonMode(_commonModes)
。 不管是mode还是commonMode其类型都是CFRunLoopMode,只是在处理上有所不同。
CFRunLoopModeRef
struct __CFRunLoopMode {
CFRuntimeBase _base;
//锁, 必须runloop加锁后才能加锁
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
//mode的名称
CFStringRef _name;
//mode是否停止
Boolean _stopped;
char _padding[3];
//sources0事件
CFMutableSetRef _sources0;
//sources1事件
CFMutableSetRef _sources1;
//observers事件
CFMutableArrayRef _observers;
//timers事件
CFMutableArrayRef _timers;
//字典 key是mach_port_t,value是CFRunLoopSourceRef
CFMutableDictionaryRef _portToV1SourceMap;
//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
//GCD定时器
dispatch_source_t _timerSource;
//GCD队列
dispatch_queue_t _queue;
// 当定时器触发时设置为true
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
//MK_TIMER的port
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
从CFRunLoopMode的源码不难看出,一个CFRunLoopMode对象包含以下内容:
- 唯一的一个name: 每个CFRunLoopMode对象都有一个唯一的名字来标识它。
- 若干个sources0事件: sources0是指不需要内核参与的事件源,通常是一些用户定义的事件。
- 若干个sources1事件: sources1是指需要内核参与的事件源,比如基于端口的事件。
- 若干个timer事件: 定时器事件,用于在特定时间点或周期性地触发。
- 若干个observer事件: 观察者事件,用于监听RunLoop状态的变化,比如即将进入休眠、即将退出等。
- 若干个port: 端口,用于线程间的通信。
RunLoop总是在某种特定的CFRunLoopMode下运行,这个特定的模式就是_currentMode
。每次运行RunLoop时,都必须指定一个模式,这个模式决定了RunLoop在本次运行中会处理哪些事件。
而从CFRunloopRef对应结构体的定义可以知道,一个RunLoop对象包含若干个模式。也就是说,一个RunLoop可以在不同的模式下运行,每种模式包含不同的事件和端口。当切换模式时,RunLoop必须退出当前模式,然后重新进入另一个模式,以保证不同模式中的事件源、定时器和观察者互不影响。
通过这种机制,RunLoop可以在不同的上下文中运行,处理不同类型的事件,提供灵活的事件处理能力。
CFRunLoopMode有五种,其中在iOS中公开暴露只有NSDefaultRunLoopMode
和NSRunLoopCommonModes
。
NSDefaultRunLoopMode
:默认模式是用于大多数操作的模式。大多数时候使用此模式来启动RunLoop并配置输入源。NSConnectionReplyMode
:Cocoa将此模式与NSConnection对象结合使用以监测回应。几乎不需要自己使用此模式。NSModalPanelRunLoopMode
:Cocoa使用此模式来识别用于模式面板的事件。NSEventTrackingRunLoopMode
:Cocoa使用此模式来限制鼠标拖动loop和其他类型的用户界面跟踪loop期间的传入事件。通常用不到。NSRunLoopCommonModes
:是NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
集合,在这种模式下RunLoop分别注册了NSDefaultRunLoopMode
和UITrackingRunLoopMode
。当然也可以通过调用CFRunLoopAddCommonMode()
方法将自定义Mode放到CFRunLoopCommonModes
组合。
CFRunLoopSourceRef-事件源
struct __CFRunLoopSource {
CFRuntimeBase _base;
//用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
//联合体
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
CFRunLoopSource是输入源的抽象,分为source0和source1两个版本。
- source0:是App内部事件,只包含一个函数指针回调,并不能主动触发事件,使用时,你需要先调用
CFRunLoopSourceSignal(source)
,将这个source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop,让其处理这个事件。 - source1:source1包含一个mach_port和一个函数回调指针。source1是基于port的,通过读取某个port上内核消息队列上的消息来决定执行的任务,然后再分发到sources0中处理的。source1只供系统使用,并不对开发者开放。
CFRunLoopTimerRef–Timer事件
CFRunLoopTimer是定时器。下面是CFRunLoopTimer的源码:
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
//timer对应的runloop
CFRunLoopRef _runLoop;
//timer对应的mode
CFMutableSetRef _rlModes;
//下一次触发的时间
CFAbsoluteTime _nextFireDate;
//定时的间隔
CFTimeInterval _interval; /* immutable */
//定时器允许的误差
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
//优先级
CFIndex _order; /* immutable */
//任务回调
CFRunLoopTimerCallBack _callout; /* immutable */
//上下文
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
从上面的代码可以看出,timer是依赖于runloop的,而且有函数指针回调,那么便可以在设定的时间点抛出回调执行任务。同时苹果官方文档也有提到CFRunLoopTimer和NSTimer是toll-free bridged(指在 Core Foundation 框架和 Foundation 框架中某些类型可以直接互换使用。)的,这就意味着两者之间可以相互转换。
CFRunLoopObserverRef–观察者
CFRunLoopObserver是观察者,监测RunLoop的各种状态的变化。
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
//对应的runLoop对象
CFRunLoopRef _runLoop;
// 当前的观察的runloop个数
CFIndex _rlCount;
//runloop的状态
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
//回调
CFRunLoopObserverCallBack _callout; /* immutable */
//上下文
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
RunLoop的source事件源来监测是否有需要执行的任务,而observer则是监测RunLoop本身的各种状态的变化,在合适的时机抛出回调,执行不同类型的任务。
runloop用于观察的状态:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//即将进入runloop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理timer事件
kCFRunLoopBeforeSources = (1UL << 2),//即将处理source事件
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//即将唤醒
kCFRunLoopExit = (1UL << 7),//runloop退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

runloop实现原理
runloop启动
RunLoop启动有两个方法可供调用,分别是CFRunLoopRun
和CFRunLoopRunInMode
//启动当前线程的RunLoop,并使其在默认模式下运行,直到RunLoop被停止或完成。
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
//在指定模式下运行RunLoop,并允许设定超时时间和是否在处理完一个source事件后返回。
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
CFRunLoopRun:
- 调用
CFRunLoopRunSpecific
函数,将当前线程的RunLoop(通过CFRunLoopGetCurrent()
获取)运行在默认模式下(kCFRunLoopDefaultMode
),并设置一个极大的超时时间(1.0e10
),表示基本上不会超时,且不在处理完一个source事件后立即返回(false
)。 - 调用
CHECK_FOR_FORK()
检查是否有进程fork(这是一种保护措施,确保fork后RunLoop的状态是安全的)。 - 如果
result
不等于kCFRunLoopRunStopped
和kCFRunLoopRunFinished
,则继续运行。kCFRunLoopRunStopped
和kCFRunLoopRunFinished
是RunLoop运行结束的标志。
获取runloop
- CFRunLoopGetCurrent:获取当前线程的runloop对象
- CFRunLoopGetMain:获取主线程的runloop对象
CFRunLoopGetCurrent
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
该方法内部调用了_CFRunLoopGet0
方法,传入的参数是当前线程pthread_self()
,由此可见CFRunLoopGetCurrent
函数必须要在线程内部调用才能获取到RunLoop对象。
CFRunLoopGetMain
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
该方法内部调用了_CFRunLoopGet0
方法,传入的参数是主线程pthread_main_thread_np()
,由此可见CFRunLoopGetCurrent
函数不管是在主线程中还是在子线程中都可以获取到主线程的RunLoop。
_CFRunLoopGet0
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)
{
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
//创建一个字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//创建主线程的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//把主线程的RunLoop保存到dict中,key是线程,value是RunLoop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void *volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
//释放主线程RunLoop
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 根据线程从__CFRunLoops获取RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
//如果在__CFRunLoops中没有找到
if (!loop) {
//创建一个新的RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//把新创建的RunLoop存放到__CFRunLoops中
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)) {
//注册一个回调,当当前线程销毁时销毁对应的RunLoop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS - 1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
- 检查传入线程是否为主线程,如果是,将其转换为主线程。
- 加锁并初始化全局字典,如果字典尚未初始化,则创建并初始化它。
- 从字典中获取当前线程的 CFRunLoop,如果未找到则创建新的并存入字典。
- 为当前线程设置特定数据,以便在线程销毁时清理对应的 CFRunLoop。
- 返回获取或创建的 CFRunLoop。
由上面这段代码可知:
RunLoop和线程是一一对应的,是以线程为key,RunLoop对象为value存放在一个全局字典中的。
主线程的RunLoop会在初始化全局化字典时创建。
子线程的RunLoop会在第一次获取时创建。
当线程销毁时,对应的RunLoop也会随之销毁。
CFRunLoopRunSpecific
CFRunLoop 的核心运行逻辑。CFRunLoopRunSpecific
函数根据指定的模式和超时时间运行
unLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
{
// 检查是否在 fork 之后的子进程中运行
CHECK_FOR_FORK();
// 如果 rl 正在被释放,返回 kCFRunLoopRunFinished
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
// 锁定 CFRunLoop 实例,确保线程安全
__CFRunLoopLock(rl);
// 根据 modeName 找到本次运行的 mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 如果没有找到 mode 或者找到的 mode 中没有注册事件则退出,不进入循环
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
// 如果找到了 mode,则解锁该 mode
if (currentMode)
__CFRunLoopModeUnlock(currentMode);
// 解锁 CFRunLoop 实例
__CFRunLoopUnlock(rl);
// 根据是否处理了事件返回相应的结果
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
// 保存当前的运行数据
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
// 取上一次运行的 mode
CFRunLoopModeRef previousMode = rl->_currentMode;
// 设置当前模式为找到的 mode
rl->_currentMode = currentMode;
// 初始化结果为 kCFRunLoopRunFinished
int32_t result = kCFRunLoopRunFinished;
// 如果当前模式有 kCFRunLoopEntry 类型的观察者,通知这些观察者即将进入 RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 调用实际的 RunLoop 运行函数,返回运行结果
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 如果当前模式有 kCFRunLoopExit 类型的观察者,通知这些观察者即将退出 RunLoop
if (currentMode->_observerMask & kCFRunLoopExit)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
// 解锁当前模式
__CFRunLoopModeUnlock(currentMode);
// 恢复之前的运行数据
__CFRunLoopPopPerRunData(rl, previousPerRun);
// 恢复之前的模式
rl->_currentMode = previousMode;
// 解锁 CFRunLoop 实例
__CFRunLoopUnlock(rl);
// 返回运行结果
return result;
}
传入四个参数:
- rl:当前运行的RunLoop对象。
- modeName:指定RunLoop对象的mode的名称。
- seconds:RunLoop的超时时间
- returnAfterSourceHandled:是否在处理完事件之后返回。
从上面的代码我们可以获取到如下几点信息:
- RunLoop运行必须要指定一个mode,否则不会运行RunLoop。
- 如果指定的mode没有注册时间任务,RunLoop不会运行。
- 通知observer进入runloop,调用__CFRunLoopRun方法处理任务,通知observer退出runloop。
__CFRunLoopRun
// __CFRunLoopRun函数,启动RunLoop
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
// 获取系统启动后的CPU运行时间,用于控制超时时间
uint64_t startTSR = mach_absolute_time();
// 如果RunLoop或者mode是stop状态,则直接return,不进入循环
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}
// mach端口,在内核中,消息在端口之间传递。初始为0
mach_port_name_t dispatchPort = MACH_PORT_NULL;
// 判断是否为主线程
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
// 如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
#if USE_DISPATCH_SOURCE_FOR_TIMERS
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
if (rlm->_queue) {
// mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (!modeQueuePort) {
CRASH("Unable to get port for run loop mode queue (%d)", -1);
}
}
#endif
// GCD管理的定时器,用于实现runloop超时机制
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
// 立即超时
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
}
// seconds为超时时间,超时时执行__CFRunLoopTimeout函数
else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
}
// 永不超时
else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}
// 标志位默认为true
Boolean didDispatchPortLastTime = true;
// 记录最后runloop状态,用于return
int32_t retVal = 0;
do {
// 初始化一个存放内核消息的缓冲池
uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
HANDLE livePort = NULL;
Boolean windowsMessageReceived = false;
#endif
// 取所有需要监听的port
__CFPortSet waitSet = rlm->_portSet;
// 设置RunLoop为可以被唤醒状态
__CFRunLoopUnsetIgnoreWakeUps(rl);
// 2.通知observer,即将触发timer回调,处理timer事件
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 3.通知observer,即将触发Source0回调
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 执行加入当前runloop的block
__CFRunLoopDoBlocks(rl, rlm);
// 4.处理source0事件,有事件处理返回true,没有事件返回false
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// 执行加入当前runloop的block
__CFRunLoopDoBlocks(rl, rlm);
}
// 如果没有Sources0事件处理 并且 没有超时,poll为false
// 如果有Sources0事件处理 或者 超时,poll都为true
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 第一次do..while循环不会走该分支,因为didDispatchPortLastTime初始化是true
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
// 从缓冲区读取消息
msg = (mach_msg_header_t *)msg_buffer;
// 5.接收dispatchPort端口的消息,(接收source1事件)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
// 如果接收到了消息的话,前往第9步开始处理msg
goto handle_msg;
}
#elif DEPLOYMENT_TARGET_WINDOWS
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}
didDispatchPortLastTime = false;
// 6.通知观察者RunLoop即将进入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 设置RunLoop为休眠状态
__CFRunLoopSetSleeping(rl);
// do not do any user callouts after this point (after notifying of sleeping)
// Must push the local-to-this-activation ports in on every loop
// iteration, as this mode could be run re-entrantly and we don't
// want these ports to get serviced.
__CFPortSetInsert(dispatchPort, waitSet);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
// 这里有个内循环,用于接收等待端口的消息
// 进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
do {
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
// 7.接收waitSet端口的消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
// 收到消息之后,livePort的值为msg->msgh_local_port,
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {
// Leave livePort as the queue port, and service timers below
rlm->_timerFired = false;
break;
} else {
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner loop.
break;
}
} while (1);
#else
// 7.接收waitSet端口的消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
#elif DEPLOYMENT_TARGET_WINDOWS
// 7.接收waitSet端口的消息
__CFRunLoopWaitForMultipleObjects(rl, rlm->_ports, (poll ? 0 : INFINITE), &livePort, &windowsMessageReceived);
#endif
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
__CFRunLoopUnsetSleeping(rl);
// 8.通知观察者RunLoop被唤醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
__CFPortSetRemove(dispatchPort, waitSet);
if (MACH_PORT_NULL != livePort) {
// 通知port已被使用
didDispatchPortLastTime = (livePort == dispatchPort);
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
handle_msg:;
// 9.处理消息,触发Source1回调
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg);
__CFRunLoopDoSource1(rl, rlm, rls, msg, livePort);
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#elif DEPLOYMENT_TARGET_WINDOWS
if (windowsMessageReceived) {
// 9.处理消息
__CFRunLoopProcessWindowsMessage(rl, rlm);
} else {
__CFRunLoopDoSource1(rl, rlm, rls, msg, livePort);
}
#endif
}
__CFRunLoopDoBlocks(rl, rlm);
// 返回条件:如果存在以下条件,返回相应的状态值
// 1. runloop是否被显式唤醒退出
// 2. 是否执行到超时
// 3. 是否有source0事件需要处理
// 4. 是否有source1事件需要处理
// 5. 是否被显式唤醒退出
// 6. 是否被模式退出
// 7. 是否被timer退出
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
} else if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
}
} while (0 == retVal);
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
}
free(timeout_context);
return retVal;
}
该方法内部就是一个 do-while 循环,当调用该方法时,线程就会一直留在这个循环里面,直到超时或者手动被停止,该方法才会返回。在这里循环里面,线程在空闲的时候处于休眠状态,在有事件需要处理的时候,处理事件。该方法是整个RunLoop运行的核心方法。
- 通知观察者RunLoop已经启动:当RunLoop启动时,通知所有的观察者,RunLoop已经开始运行。可以在此时进行一些初始化操作。
- 通知观察者定时器即将触发:在RunLoop即将处理定时器事件时,通知观察者。观察者可以在此时进行一些操作,如更新UI或记录日志。
- 通知观察者任何不基于端口的输入源都将触发:在RunLoop即将处理非基于端口的输入源(Source0)事件时,通知观察者。这些输入源通常是应用程序内部事件。
- 触发所有准备触发的非基于端口的输入源:处理所有已经准备好的Source0事件,执行相应的回调函数。
- 如果基于端口的输入源已准备好并等待启动,立即处理事件;并进入步骤9:检查是否有基于端口的输入源(Source1)已经有事件需要处理。如果有,则处理这些事件,并跳到步骤9继续处理其他事件。
- 通知观察者线程进入休眠状态:在RunLoop即将进入休眠状态之前,通知所有的观察者,让他们知道RunLoop即将进入等待状态。可以在此时进行一些准备工作,如记录日志。
- 使线程进入睡眠状态,直到发生以下事件之一:
- 某一事件到达基于端口的源:如果有基于端口的事件到达(如文件描述符可读,网络套接字有数据等),唤醒RunLoop。
- 定时器触发:如果定时器到期,唤醒RunLoop。
- RunLoop设置的时间已经超时:如果等待超时,唤醒RunLoop。
- RunLoop被唤醒:如果RunLoop被显式唤醒(如调用
CFRunLoopWakeUp
),唤醒RunLoop。
- 通知观察者线程即将被唤醒:在RunLoop从睡眠状态被唤醒之前,通知所有的观察者。观察者可以在此时进行一些操作,如更新UI或记录日志。
- 处理未处理的事件:唤醒后,处理所有未处理的事件:
- 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2:如果定时器到期,处理定时器事件并重新进入RunLoop循环。
- 如果输入源启动,传递相应的消息:处理输入源的事件,传递相应的消息。
- 如果RunLoop被唤醒而且时间还没超时,重启RunLoop。进入步骤2:如果RunLoop被显式唤醒且时间未超时,重新进入RunLoop循环。
- 通知观察者RunLoop结束:RunLoop即将结束时,通知所有的观察者。观察者可以在此时进行一些收尾工作,如释放资源或记录日志。

非基于端口的输入源(Source0):这些源通常不依赖于内核端口机制,直接调用某个函数来处理事件。例如,手动添加的事件处理函数。
基于端口的输入源(Source1):这些源依赖于内核端口机制,监听来自内核的消息。例如,文件描述符、网络套接字、Mach端口等。
__CFRunLoopServiceMachPort
在__CFRunLoopRun内部有一个内置的循环,这个循环会让线程进入休眠状态,直到收到新消息才跳出该循环,继续执行RunLoop。这些消息是基于mach port
来进行进程之间的通讯的。
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
Boolean originalBuffer = true; // 标记缓冲区是否是原始缓冲区
kern_return_t ret = KERN_SUCCESS; // 初始化返回值
for (;;) { // 无限循环,直到接收到消息或发生错误
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer; // 将缓冲区指针转换为消息头指针
msg->msgh_bits = 0; // 重置消息头的标志位
msg->msgh_local_port = port; // 设置本地端口为指定的端口
msg->msgh_remote_port = MACH_PORT_NULL; // 设置远程端口为空
msg->msgh_size = buffer_size; // 设置消息大小为缓冲区大小
msg->msgh_id = 0; // 重置消息ID
// 根据是否无限超时,调用相应的休眠或轮询函数
if (TIMEOUT_INFINITY == timeout) {
CFRUNLOOP_SLEEP(); // 如果是无限超时,调用休眠函数
} else {
CFRUNLOOP_POLL(); // 如果有超时时间,调用轮询函数
}
// 调用mach_msg函数接收消息
ret = mach_msg(msg,
MACH_RCV_MSG | MACH_RCV_LARGE | ((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0) | MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) | MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
0, // 不发送消息
msg->msgh_size, // 接收消息的大小
port, // 使用的端口
timeout, // 超时时间
MACH_PORT_NULL); // 没有通知端口
CFRUNLOOP_WAKEUP(ret); // 唤醒RunLoop
// 如果接收消息成功
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL; // 设置livePort为本地端口
return true; // 返回true表示成功
}
// 如果超时未接收到消息
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg); // 如果不是原始缓冲区,则释放内存
*buffer = NULL; // 将缓冲区指针设为NULL
*livePort = MACH_PORT_NULL; // 将livePort设为NULL
return false; // 返回false表示失败
}
// 如果缓冲区太小,接收到过大的消息
if (MACH_RCV_TOO_LARGE != ret) break; // 如果不是缓冲区太小的错误,跳出循环
// 重新分配更大的缓冲区
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE); // 计算新的缓冲区大小
if (originalBuffer) *buffer = NULL; // 如果是原始缓冲区,将其设为NULL
originalBuffer = false; // 标记缓冲区不再是原始缓冲区
*buffer = realloc(*buffer, buffer_size); // 重新分配内存
}
HALT; // 发生严重错误,终止程序
return false; // 返回false表示失败
}
休眠:
当没有指定超时时间(即 timeout 为 TIMEOUT_INFINITY)时,线程进入休眠状态。
线程在休眠状态下不会主动消耗 CPU 资源,直到有事件发生唤醒线程。
适用于需要长时间等待事件的场景。
轮询:
当指定了超时时间时,线程不会一直挂起,而是会定期进行检查。
线程在轮询期间会消耗一定的 CPU 资源,但可以更及时地响应事件。
适用于需要在有限时间内频繁检查事件的场景。
该方法接收指定内核端口的消息,并将消息缓存在缓存区,供外界获取。该方法的核心是mach_msg方法,该方法实现消息的发送和接收。RunLoop调用这个函数去接收消息,如果没有接收到port的消息,内核会将线程置于等待状态。
RunLoop事件处理
处理事件主要涉及到如下几个方法:
__CFRunLoopDoObservers
:处理通知事件。__CFRunLoopDoBlocks
:处理block事件。__CFRunLoopDoSources0
:处理source0事件。__CFRunLoopDoSource1
:处理source1事件。__CFRunLoopDoTimers
:处理定时器。CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
:GCD主队列
上述方法回调上层的方法对应如下图所示:

小结
RunLoop的运行必定要指定一种mode,并且该mode必须注册任务事件。
RunLoop是在默认mode下运行的,当然也可以指定一种mode运行,但是只能在一种mode下运行。
RunLoop内部实际上是维护了一个do-while循环,线程就会一直留在这个循环里面,直到超时或者手动被停止。
RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态,否则线程处理事件。
RunLoop应用
NSTimer
我们创建timer时,之所以timer能运行,是因为创建timer时,一般情况下,是在主线程中创建,这时会默认将timer以defaultRunloopModel
的类型加入主线程,而主线程的runloop对象默认是打开的,从而timer可以运行。
系统会将NSTimer自动加入NSDefaultRunLoopMode
模式中,所以以下两段代码含义相同:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
NSTimer在滑动时停止工作的问题
当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode
下
当我们进行拖拽时,RunLoop就结束NSDefaultRunLoopMode
,切换到了UITrackingRunLoopMode
模式下,这个模式下没有添加该NSTimer以及其事件,所以我们的NSTimer就不工作了
当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode
模式,又切换回NSDefaultRunLoopMode
模式,所以NSTimer就又开始正常工作了。
如何解决NSTimer在滑动时停止工作
根据前面学的,NSRunLoopCommonModes
是NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
的集合,因此如果想要让timer时刻都不会停止工作,可以使用NSRunLoopCommonModes
,即直接让NSTimer在两种mode下都能工作。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
或者:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
NSTimer不准确的问题
- 在RunLoop循环过程中,被NSTimer触发事件阻塞了,导致循环不能及时进行下去,延误之后NSTimer触发时间。
- 在RunLoop循环过程中,在某一时刻主线程发生了阻塞情况,导致循环不能及时进行下去,厌恶NSTimer触发时间。
- 在RunLoop循环过程中,发生了模式的转换,(比如UIScrollView的滑动) 导致原有模式下的NSTimer不会正常触发。(虽然可以指定NSTimer所处模式为
NSRunLoopCommonModes
,但是这种解决方法并不能改变run loops在特定模式下不能处理其余模式事件的本质。)
以上情况都是由于NSTimer所依赖的run loops会被多种原因干扰正常循环,所以要想解决NSTimer精度问题,就要避免所依赖的run loops被外界干扰。
解决办法:
- 尽量避免将NSTimer放入容易受到影响的主线程runloops中。
- 尽量避免将耗时操作放入NSTimer依赖的线程中。
- 尽量避免在NSTimer触发事件中进行耗时操作,如果不能避免,将耗时操作移至其余线程进行。
GCD计时器
不过GCD定时器不同,GCD的线程管理是通过系统来直接管理的。GCD Timer是通过dispatch port
给 RunLoop发送消息,来使RunLoop执行相应的block,如果所在线程没有RunLoop,那么GCD 会临时创建一个线程去执行block,执行完之后再销毁掉,因此GCD的Timer是不依赖RunLoop的。
RunLoop 响应用户操作的大致流程:
- 当用户进行某个操作(例如点击按钮)时,操作系统会将该事件发送给应用程序。
- 应用程序主线程会收到这个事件,并将其添加当前线程的 RunLoop 中。
- RunLoop 开始运行,并进入一个循环状态,不断地检查是否事件需要处理。
- 如果有事件需要处理,RunLoop 会将事件从队列中取出,并将其分发给相应的处理器(例如事件响应方法)。
- 处理器执行相应的代码处理事件,可能包括更新用户界面、执行业务逻辑等操作。
- 处理完事件后,RunLoop 继续循环,等下一个事件的到来。
- RunLoop 的关键是在循环中不断检查事件,并及时分发给处理器这样可以保证用户操作的响应速度,并且免阻塞主线程。
AutoReleasePool与RunLoop
App启动后,苹果会给RunLoop注册很多个observers
,其中有两个是跟自动释放池相关的,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
。
-
第一个observer监听的是activities=0x1(kCFRunLoopEntry),也就是在即将进入loop时,其回调会调用
_objc_autoreleasePoolPush()
创建自动释放池; -
第二个observer监听的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit),即监听的是准备进入睡眠和即将退出loop两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用
_objc_autoreleasePoolPop()
释放旧的释放池,并调用_objc_autoreleasePoolPush()
创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出loop时则会调用_objc_autoreleasePoolPop()
释放池子。再即将进入loop时,会调用
_objc_autoreleasePoolPush()
创建自动释放池;
在准备睡眠时,先调用_objc_autoreleasePoolPop()
释放旧的释放池,后调用_objc_autoreleasePoolPush()
创建新建一个新的,用来装载被唤醒后要处理的事件对象;
在即将退出loop时,会调用_objc_autoreleasePoolPop()
释放池子。
卡顿检测
可以通过RunLoop的不同状态来做页面刷新的卡顿检测。
线程常驻与线程保活
线程常驻
有的时候我们需要创建一个线程在后台一直做一些任务(比如比如后台播放音乐、下载文件等等,我们希望这条线程永远常驻内存),但是常规的线程在任务完成后就会立即销毁,因此我们需要一个常驻线程来让线程一直都存在。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run {
NSRunLoop *currentRl = [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//上面这段代码和下面的代码实现的效果相同,让runloop无限期运行下去
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
[currentRl run];
}
- (void)run2
{
NSLog(@"常驻线程");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
上面的代码就是一个常驻线程。把线程thread添加在到RunLoop中,通常RunLoop启动前必须要设置一个mode,并且为mode至少设置一个Source/Timer/Observer,在这里是添加了一个port,虽然消息可以通过port发送到RunLoop内,但是这里并没有发送任何的消息,所以这样便可以保持RunLoop不退出,s实现线程常驻。
NSRunLoop 需要至少一个输入源(如 NSPort)才能保持运行状态。如果 NSRunLoop 没有任何输入源,它会立即退出。通过添加一个 NSPort,可以确保 NSRunLoop 不会退出,从而使线程保持活跃状态。
线程保活
定义:线程保活是指一个线程在空闲时不立即销毁,而是保留一段时间以备后续任务使用。如果在保留时间内有新任务到来,线程可以重新被利用;如果超时未被利用,线程将被销毁。
我们不应该使用 run 方法来启动 RunLoop,因为它创建的是一个不会退出的循环,使用这个方法的子线程自然无法被销毁。我们可以像run 一样利用runMode:beforeDate: 方法来创建一个符合我们条件的子线程:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
把它放到一个 while 循环中,利用一个是否停止 RunLoop 的全局标记来辅助处理线程的生命周期问题
__weak typeof(self) weakSelf = self;
self.thread = [[LSThread alloc] initWithBlock:^{
NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] run];
while (!weakSelf.isStopedThread) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"--- 结束 ---");
}];
停止 RunLoop
- (void)stop {
self.isStopedThread = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
}
停止操作一定要在我们的目标线程执行,比如我们直接调用 stop 方法并不能达到我们预期的效果,这是因为stop 默认在主线程执行,没有拿到目标线程,停止无效。
两者的区别
生命周期管理:
线程常驻:线程一旦创建后会一直保持活动状态,不会退出,直到明确调用退出方法。适用于需要长期运行的后台任务。
线程保活:线程在空闲时会保留一段时间,等待新任务。在超时后,线程会被销毁。适用于短期任务较多但不需要长期保持线程的场景。
资源消耗:
线程常驻:会持续占用系统资源,即使在没有任务执行时也是如此。因此需要确保此类线程确实有长期任务以避免浪费资源。
线程保活:在空闲时资源占用较低,只有在有任务执行时才会占用系统资源。
实现复杂度:
线程常驻:相对简单,通过 NSRunLoop 或者循环机制可以轻松实现。
线程保活:需要更多的逻辑来管理线程的生命周期,如线程池、超时机制等。