1、Runloop 基础
1、Runloop 概念
iOS 的程序启动后是靠一个处于阻塞状态的 do/while 循环驱动的。该循环区别于其他的循环在于当有事件时,就会进入循环体中执行(如:点击)。当没有事件执行的时候,会进入休眠状态以等待下次事件到来时再唤醒执行。
2、Runloop 获取
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
3、Runloop 特点
1、线程和 Runloop 一一对应。
2、主线程的 Runloop 默认是开启的,子线程的 Runloop 需要我们手动开启。
3、子线程的 Runloop 在首次获取的时候创建,如果不去获取的话,将不会有该线程的 runloop,在线程结束时销毁,并且只能在线程的内部才能获取它的 runloop。
4、Runloop 运行流程
Runloop 流程5、事件循环
Runloop 由两种状态用户态和内核态。
用户态:权限低;CPU 访问权限有限,不能直接使用系统资源,也不能改变 CPU 工作状态,只能访问这个用户程序本身的存储空间。
内核态:权限高;CPU 可以访问计算机的任何资源。如:操作系统,外设等。
当没有消息进行处理时,会进入休眠以避免占用资源。此时会由用户态切换到内核态。
当有消息需要处理时,立刻被唤醒进行消息处理。此时会由内核态切换到用户态。
2、Runloop 结构
Runloop 结构struct __CFRunLoop {
CFRuntimeBase _base;
//获取mode列表的锁
pthread_mutex_t _lock; /* locked for accessing mode list */
//唤醒端口
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
//重置RunLoop数据
volatile _per_run_data *_perRunData; // reset for runs of the run loop
//RunLoop所对应的线程
pthread_t _pthread;
uint32_t _winthread;
//标记为common的mode的集合
CFMutableSetRef _commonModes;
//commonMode的item集合
CFMutableSetRef _commonModeItems;
//当前的mode
CFRunLoopModeRef _currentMode;
//存储的是CFRunLoopModeRef
CFMutableSetRef _modes;
// _block_item链表表头指针
struct _block_item *_blocks_head;
// _block_item链表表尾指针
struct _block_item *_blocks_tail;
//运行时间点
CFAbsoluteTime _runTime;
//睡眠时间点
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
一个 runloop 包含若干个 mode,每个 mode 包含若干个 source/timer/observer。每次调用 runloop 主函数时只能指定其中的一个 mode,该 mode 称为 CurrentMode,如果要切换 mode 只能退出 runloop 然后再重新指定 mode。这样的目的是分隔开不同 mode 的 source/timer/observer 让其不受影响。
3、Core Foundation 中关于 Runloop 的相关类:CFRunLoopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef。
1、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 */
};
1、CFRunLoopMode 对象有唯一一个 name、若干个 sources0 事件、若干个 sources1 事件、若干个 timer 事件、若干个 observer 事件和若干 port。RunLoop 总是在某种特定的 CFRunLoopMode 下运行的,这个特定的 mode 便是 _currentMode。而 CFRunloopRef 对应结构体的定义知道一个 RunLoop 对象包含有若干个 mode。
2、系统默认注册了5个 Model 即:kCFRunLoopDefaultMode、UITrackingRunLoopMode、UIInitializationRunLoopMode、GSEventReceiveRunLoopMode、kCFRunLoopCommonModes,但苹果公开提供的 mode 只有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode(界面跟踪)。
kCFRunLoopDefaultMode:App 的默认 model,通常主线程是在这个 model 下进行。
UITrackingRunLoopMode:界面跟踪 model,用于 scrolView 追踪触摸滑动,保证界面滑动时不受其他 model 影响。
UIInitializationRunLoopMode:在刚启动 App 时进入的第一个 Mode,启动完成后不再使用。
GSEventReceiveRunLoopMode:接受系统事件的内部 mode,通常用不到。
kCFRunLoopCommonModes:这是一个占位 mode,不是一个真正的 mode。
2、CFRunLoopSourceRef:事件源
//CFRunLoopSource 源码
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 内部事件,只包含一个函数指针回调并不能主动触发事件,添加 Source0 到 RunLoop 并不会主动唤醒线程,需要手动唤醒,使用时需要先调用 CFRunLoopSourceSignal(source),将这个 source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。(如: 触摸事件处理, performSelector:onThread:)
source1:具备唤醒线程的能力,source1 包含一个 mach_port 和一个函数回调指针。source1 是基于 port 的,通过读取某个 port 上内核消息队列上的消息来决定执行的任务,然后再分发到 sources0 中处理的。
3、CFRunLoopTimerRef:Timer事件
//源码
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 */
};
是基于时间的触发器,包含一个时间长度和一个回调。当其加入到 runloop 时会注册时间点,时间点到时 runloop 会被唤醒执行。
4、CFRunLoopObserverRef:观察者
//源码
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
};
5、小结
1、RunLoop 是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
2、RunLoop 和线程是绑定在一起的,每条线程都有唯一一个与之对应的 RunLoop 对象。
3、每个 RunLoop 对象都会包含有若干个 mode,每个 mode 包含有唯一一个 name,若干个 sources0 事件,若干个 sources1 事件,若干个 timer 事件,若干个 observer 事件和若干 port,RunLoop 总是在某种特定的 mode 下运行的,这个特定的 mode 便是_currentMode。
image.png
4、RunLoop 底层
1、Runloop 启动
RunLoop 启动方法有两种:CFRunLoopRun 和 CFRunLoopRunInMode。
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
从上面这两个方法的实现可以看出,CFRunLoopRun 方法启动的 RunLoop 是运行在 kCFRunLoopDefaultMode 模式下的,即以这种方式启动的 runloop 是在默认模式下运行的。而 CFRunLoopRunInMode 方法则是需要指定运行的 mode。从这里也可以看出来 RunLoop 虽然有很多的 mode,但是 RunLoop 的运行时只能是在一种 mode 下进行。同时这两个方法都调用了 CFRunLoopRunSpecific 方法,该方法就是具体启动 RunLoop 的方法,这个方法的第一个参数就是当前的 RunLoop,所以在分析 CFRunLoopRunSpecific 方法之前,先来看下是怎么获取到 RunLoop 的。
2、Runloop 获取
CFRunLoopGetCurrent 和 CFRunLoopGetMain,它们分别代表着获取当前线程的 Runloop 对象和获取主线程的 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;
}
由以上可知:
1、RunLoop 和线程是一一对应的,是以线程为 key,RunLoop 对象为 value 存放在一个全局字典中的。
2、主线程的 RunLoop 会在初始化全局化字典时创建。
3、子线程的 RunLoop 会在第一次获取时创建。
4、当线程销毁时,对应的 RunLoop 也会随之销毁。
3、CFRunLoopRunSpecific
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) /* DOES CALLOUT */
{
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//根据modeName找到本次运行的mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
//如果没有找到mode或者找到的mode中没有注册事件则退出,不进入循环
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode)
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
//取上一次运行的mode
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
//通知observer即将进入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知observer已经退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
参数说明:
rl:当前运行的RunLoop对象。
modeName:指定RunLoop对象的mode的名称。
seconds:RunLoop的超时时间
returnAfterSourceHandled:是否在处理完事件之后返回。
由以上代码可知:
1. RunLoop 运行必须要指定一个 mode,否则不会运行 RunLoop。
2. 如果指定的 mode 没有注册时间任务,RunLoop 不会运行。
3. 通知 observer 进入 runloop,调用 __CFRunLoopRun 方法处理任务,通知 observer 退出 runloop。
4、__CFRunLoopRun
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..whil循环不会走该分支,因为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
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
#elif DEPLOYMENT_TARGET_WINDOWS
// Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
// Must remove 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. Also, we don't want them left
// in there if this function returns.
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopSetIgnoreWakeUps(rl);
// user callouts now OK again
//取消runloop的休眠状态
__CFRunLoopUnsetSleeping(rl);
//8.通知观察者runloop被唤醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//9.处理收到的消息
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
#if DEPLOYMENT_TARGET_WINDOWS
if (windowsMessageReceived) {
// These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
if (rlm->_msgPump) {
rlm->_msgPump();
} else {
MSG msg;
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
// To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
// Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
// NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
__CFRunLoopSetSleeping(rl);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
__CFRunLoopUnsetSleeping(rl);
// If we have a new live port then it will be handled below as normal
}
#endif
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
//通过CFRunloopWake唤醒
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
//什么都不干,跳回2重新循环
// do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
// Always reset the wake up port, or risk spinning forever
ResetEvent(rl->_wakeUpPort);
#endif
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
//如果是定时器事件
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
//9.1 处理timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
#if USE_MK_TIMER_TOO
//如果是定时器事件
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
// In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
//9.1处理timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
//如果是dispatch到main queue的block
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
void *msg = 0;
#endif
//9.2执行block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// Despite the name, this works for windows handles as well
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
// 有source1事件待处理
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
//9.2 处理source1事件
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);
}
#elif DEPLOYMENT_TARGET_WINDOWS
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
}
}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
//进入run loop时传入的参数,处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
}else if (timeout_context->termTSR < mach_absolute_time()) {
//run loop超时
retVal = kCFRunLoopRunTimedOut;
}else if (__CFRunLoopIsStopped(rl)) {
//run loop被手动终止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
}else if (rlm->_stopped) {
//mode被终止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
}else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
//mode中没有要处理的事件
retVal = kCFRunLoopRunFinished;
}
//除了上面这几种情况,都继续循环
} while (0 == retVal);
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}
return retVal;
}
以上代码简单概述一下:
该方法内部是一个 do-while 循环,当调用该方法时,线程就会一直留在这个循环里面,直到超时或者手动被停止,该方法才会返回。
在循环里面,线程在空闲的时候处于休眠状态,在有事件需要处理的时候,处理事件。该方法是整个 RunLoop 运行的核心方法。
其大概流程如下
1. 通知观察者 RunLoop 已经启动。
2. 通知观察者定时器即将触发。
3. 通知观察者任何不基于端口的输入源都将触发。
4. 触发所有准备触发的非基于端口的输入源。
5. 如果基于端口的输入源已准备好并等待启动,立即处理事件;并进入步骤9。
6. 通知观察者线程进入休眠状态。
7. 使线程进入睡眠状态,直到发生以下事件之一:
某一事件到达基于端口的源。
定时器触发。
RunLoop 设置的时间已经超时。
RunLoop 被唤醒。
8. 通知观察者线程即将被唤醒。
9. 处理未处理的事件。
如果用户定义的定时器启动,处理定时器事件并重启 RunLoop。进入步骤2。
如果输入源启动,传递相应的消息。
如果 RunLoop 被唤醒而且时间还没超时,重启 RunLoop。进入步骤2。
10. 通知观察者 RunLoop 结束。
5、__CFRunLoopServiceMachPort
Runloop 方法内部有一个内置的循环,这个循环会让线程进入休眠状态,直到收到新消息才跳出该循环,继续执行 RunLoop。这些消息是基于 mach port 来进行进程之间的通讯的。
__CFRunLoopServiceMachPort 源码
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 (;;) { /* In that sleep of death what nightmares may come ... */
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发送或者接收的消息都是指针,
//如果直接发送或者接收消息体,会频繁进行内存复制,损耗性能
//所以XNU使用了单一内核的方式来解决该问题,所有内核组件都共享同一个地址空间,因此传递消息时候只需要传递消息的指针
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);
//接收/发送消息成功,给livePort赋值为msgh_local_port
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
//MACH_RCV_TIMEOUT
//超出timeout时间没有收到消息,返回MACH_RCV_TIMED_OUT
//此时释放缓冲区,把livePort赋值为MACH_PORT_NULL
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
//MACH_RCV_LARGE
//如果接收缓冲区太小,则将过大的消息放在队列中,并且出错返回MACH_RCV_TOO_LARGE,
//这种情况下,只返回消息头,调用者可以分配更多的内存
if (MACH_RCV_TOO_LARGE != ret) break;
//此处给buffer分配更大内存
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
该方法接收指定内核端口的消息并将消息缓存在缓存区供外界获取。该方法的核心是 mach_msg 方法,该方法实现消息的发送和接收。RunLoop 调用这个函数去接收消息,如果没有接收到 port 的消息,内核会将线程置于等待状态。
6、RunLoop 事件处理
__CFRunLoopDoObservers:处理通知事件。
__CFRunLoopDoBlocks:处理block事件。
__CFRunLoopDoSources0:处理source0事件。
__CFRunLoopDoSource1:处理source1事件。
__CFRunLoopDoTimers:处理定时器。
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE:GCD主队列
7、小结
1、RunLoop 的运行必定要指定一种 mode 并且该 mode 必须注册任务事件。
2、RunLoop 可以在默认 mode 下运行也可以指定一种 mode 运行,但是只能在一种 mode 下运行。
3、RunLoop 内部实际上是维护了一个 do-while 循环,线程就会一直留在这个循环里面,直到超时或者手动被停止。
4、RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态,否则线程处理事件。
5、RunLoop 应用
1、NSTimer
NSTimer 定时器:需要注意的是注册到 RunLoop 的 NSRunLoopCommonModes 下。
GCD Timer 定时器:GCD 的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。
2、AutoReleasepool
App启动后,会给 RunLoop 注册很多个 observers,其中有两个是跟自动释放池相关的,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 observer 监听的是 activities=0x1(kCFRunLoopEntry),也就是在即将进入 loop 时,其回调会调用 _objc_autoreleasePoolPush() 创建自动释放池;
第二个 observe r监听的是 activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit),即监听的是准备进入睡眠和即将退出 loop 两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用 _objc_autoreleasePoolPop() 释放旧的释放池,并调用 _objc_autoreleasePoolPush() 创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出loop时则会 _objc_autoreleasePoolPop() 释放池子。
3、卡顿检测
- (void)start{
[self registerObserver];
[self startMonitor];
}
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
DSBlockMonitor *monitor = (__bridge DSBlockMonitor *)info;
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 优先级最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
- (void)startMonitor{
// 创建信号
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
//超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (st != 0)
{
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
NSLog(@"检测到卡顿");
}
}
self->_timeoutCount = 0;
}
});
}
其原理就是根据 RunLoop 进入 kCFRunLoopBeforeSources 状态处理 source 事件到 kCFRunLoopAfterWaiting 状态变化之间的时间间隔来做判断依据。
4、常驻线程
//后台保活线程
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run {
NSRunLoop *currentRl = [NSRunLoop currentRunLoop];
[currentRl addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[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 不退出,实现线程常驻。
示例
1、main 为什么不会退出?
在 main 函数中会调用 UIApplicationMain 函数,UIApplicationMain 函数会启动主线程的 Runloop ,Runloop 的事件循环机制可以做到有消息需要处理时,立刻被唤醒进行消息处理;没有消息进行处理时,会进入休眠以避免占用资源(此刻会发生状态切换由用户态切换到内核态)。
2、一个 timer/observer/source 怎么添加到两个 mode 中?
使用 NSRunloopCommonModes,并不是实际存在的一个模式,是同步 source/timer/observer 到多个 mode 的一种技术方案。
3、一个处于休眠状态的 runloop 有哪些事件可以唤醒 runloop?
Source1,Timer事件,外部手动唤醒
4、APP 从点击图标,到程序启动,运行直至退出,在这个过程当中系统都发生了什么?
答:main 函数启动后会调用 UIApplicationMain 函数,UIApplicationMain 函数内部会启动主线程的 runloop ,该 runloop 处理过一些事件之后会进入休眠状态。此时点击 APP 图标事件会转化为一个 Source1 事件,唤醒主线程,运行并处理事件,事件完成后再次进入休眠状态。退出程序的时候会退出 runloop。
5、定时器添加到含有滑动视图的视图上,滚动滑动视图的时候,定时器停止?
答:定时器默认是添加到 kCFRnloopDefaultMode 模式下,滚动视图的时候,当前 runloop 的 mode 会切换到 UITrackingRunLoopMode。示例中的定时器可以设置 mode 为 kCFRunLoopCommonModes。kCFRunLoopCommonModes 可以把同一事件同步到不同的 mode 中。
6、怎样实现一个常驻线程?
1、为当前线程开启一个 runloop;
2、向 runloop 添加一个 port/source 等维持 runloop 的事件循环;
3、启动 runloop。
示例代码
7、怎样保证子线程回来更新UI的时候不打断用户的滑动操作?
1、用户滑动的时候,此时的 runloopMode 为 UITrackingRunLoopMode。
2、网络数据一般由子线程切换到主线程来更新 UI。
3、可以将数据包装后提交到主线程 runloop 的 default 模式下。当用户滑动操作还在进行的情况下,在 default 模式下的数据不会用来更新当前模式为 UITrackingRunLoopMode 的数据。
4、滑动停止的时候,mode 会切换为 default 模式,从而可以更新数据。
网友评论