1、RunLoop's Concept and Components
1.1、What is RunLoop?
A RunLoop is a mechanism that manages the event loop in an application. A RunLoop is a event processing roop that you use to schedule work and coorinate the receipt of incoming events.The purpose of run loop is to keep your thread busy when there is work to do and put your thread to sleep where is none.
1.2、The Relationship Between RunLoop and Thread
Run loop management is not entirely automatic.You must still design your thread's code to start the run loop at appropriate times and respond to incoming envents.Both cocoa and Core Foundtaion provide run loop objects to help you configure and manage your thread's run loop.Your application does not need to create these objects explicitly;each thread,including the application's main thread,has an associated run loop object.Only secondary threads need to run their run loop explicitly,however.The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process.
1.3、The Structure of RunLoop
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
From the structure of the RunLoop source code, we can see that a RunLoop object contains a thread,several modes,and several common modes.Both mode and commonMode are of type CFRunLoopMode,but they are handled differently.
1.4、The Structure of CFRunLoopModeRef
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
//the key is mach_port_t, the value is CFRunLoopSourceRef
CFMutableDictionaryRef _portToV1SourceMap;
//save all ports that need to be monitored.such as _wakeUpPort and _timerPort which are all stored in this array.
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
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 */
};
From the structure of the CFRunLoopMode source code, we can see that a CFRunLoopMode object has a unique name,several source0 envents,several source1 events,several timer events,several observer events and several ports.The RunLoop always runs in a specific CFRunLoopMode,which is referred to as currentMode.Based on the definition of CFRunLoopRef we know that a RunLoop object contains multiple modes.
Regarding CFRunLoopMode,Apple mentions five types of modes,such as NSDefaultRunLoopMode、NSConnectionReplyMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode、NSRunLoopCommonModes.In iOS only NSDefaultRunLoopMode and NSRunLoopCommonModes are publicly exposed.
NSDefaultRunLoopMode:
Purpose: This is the default mode for the main thread’s RunLoop. It's used for most common operations, such as handling user input, processing events, and updating the UI.
Usage: Typically used when you need to perform standard tasks on the main thread without interruptions from other modes.
NSConnectionReplyMode:
Purpose: This mode is used for handling replies to messages sent by NSConnection objects, which are part of inter-process communication (IPC).
Usage: Primarily used when managing responses to messages sent between different parts of a distributed system (for example, between apps or processes).
NSModalPanelRunLoopMode:
Purpose: This mode is specifically used when a modal panel, such as a dialog box, is active.
Usage: Typically used when you need to prevent user interaction with the rest of the app while the modal panel is displayed, isolating its input processing from other tasks.
NSEventTrackingRunLoopMode:
Purpose: This mode is used for event tracking, such as when the user is interacting with the UI by dragging the mouse or interacting with touch events.
Usage: Often used during drag-and-drop or scroll gestures to handle events without interference from other modes.
NSRunLoopCommonModes:
Purpose: This mode is a placeholder for common modes. You can associate input sources with `NSRunLoopCommonModes`, and those input sources will be processed in any mode that is considered "common."
Usage: This is used to combine different modes (like `NSDefaultRunLoopMode` and `NSEventTrackingRunLoopMode`) so that input sources added to common modes are processed in any of the included modes. For example, a timer added to `NSRunLoopCommonModes` will work in both default and event tracking modes.
1.5、The Structure of CFRunLoopSourceRef
struct __CFRunLoopSource {
CFRuntimeBase _base;
//It is used to mark the singaled state.The source0 event will only be processed when it is marked as being in the singaled state.
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
//联合体
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
According to Apple's official definition,CFRunLoopSource is an abstraction of input sources,and it is divided into two versions:source0 and source1.
source0: It is an in-house event of the app.It only contains a callback function
pointer and cannot trigger events by it self.You need to call CFRunLoopSourcesSignal(source) to signal the source as pending,before using it.
After that,you must manually call CFRunLoopWakeUp(runloop) to wake up the RunLoop and allow it to handle this event.
source1:It contains a mach_port and a callback funtion pointer.It is based on ports,and it reads messages from the message queues of a port to decide which task to execute,
then assigns the task to sourceo for handling.Source1 is used only by system,and is not exposed to developers.
1.6、The Structure of CFRunLoopTimerRef
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
//associated with timer
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
//The next execution time
CFAbsoluteTime _nextFireDate;
//The timer interval
CFTimeInterval _interval; /* immutable */
//The timer's allowed tolerance
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
//priority
CFIndex _order; /* immutable */
//task callback
CFRunLoopTimerCallBack _callout; /* immutable */
//contextual environment
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
From the structure of CFRunLoopTimer source code,we can see that a timer relies on a runloop and has a callback function pointer.This allows the callback to be trigged at a pre-set time to execute a task.Additionally,Apple's offcial documention mentions that CFRunLoopTimer is toll-free bridge with NStimer, meaning the two can be used interchangebly.
1.7、The Structure of CFRunLoopObserverRef
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
//runloop的状态
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
//contextual environment
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
The SourceRef of runloop is used to mornitor whether there are tasks that need to be executed.However,the observer mornitors the various states of the runloop itself. when a state change occurs, the observer triggers a callback to handle different types of tasks.The states observed by the run loop are as follows:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//will enter runloop
kCFRunLoopBeforeTimers = (1UL << 1),//about to handle timer events
kCFRunLoopBeforeSources = (1UL << 2),//about to handle source events
kCFRunLoopBeforeWaiting = (1UL << 5),//about to enter sleep
kCFRunLoopAfterWaiting = (1UL << 6),//about to wake up
kCFRunLoopExit = (1UL << 7),//runloop exit
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
2、RunLoop Underlying Implementation Principles
2.1、RunLoop start
There are two methods available for developers to start a RunLoop:CFRunLoopRun and CFRunLoopRunInMode.Let's take a look at the source code of these two methods:
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);
}
From the implementation of the two methods above, we can see that the CFRunLoopRun method starts the RunLoop in the kCFRunLoopDefaultMode, meaning the RunLoop runs in the default mode when started this way. However, the CFRunLoopRunInMode method requires specifying a mode to run. This shows that, although RunLoop has many modes, it can only operate in one mode at a time. Both methods call the CFRunLoopRunSpecific method, which is the actual method responsible for starting the RunLoop. The first parameter of this method is the current RunLoop, so before analyzing CFRunLoopRunSpecific, let's first look at how to obtain the RunLoop.
2.2、Get the RunLoop
Apple provides two methods for developers to obtain a RunLoop object: CFRunLoopGetCurrent and CFRunLoopGetMain. These methods are used to get the RunLoop object of the current thread and the main thread, respectively.
2.2.1、Source code of CFRunLoopGetCurrent
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
The code above shows the implementation of CFRunLoopGetCurrent. From this, we can see that it internally calls the _CFRunLoopGet0 method, passing the current thread (pthread_self()) as a parameter. This indicates that the CFRunLoopGetCurrent function must be called within a thread to retrieve its RunLoop object.
2.2.2、Source code of 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;
}
The code above shows the implementation of CFRunLoopGetMain.From this,we can see that it internally calls the _CFRunLoopGet0 method,passing the main thread(pthread_main_thread_np())as a parameter.This indicates that whether in main thread or in secondary thread we can retrieve the main thread's RunLoop.
2.2.3、Source code of _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);
//create a dictionary
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//create the main thread's RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//Store the main thread's RunLoop in a dictionary,where the key is thread
//value is RunLoop.
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//Check if _CFRunLoops is currently NULL and atomically swap it with dict.
//If successful,_CFRunLoops will point to dict.
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void *volatile *)&__CFRunLoops)) {
//If the swap fails(meaning _CFRunLoops was not NULL),release dict to avoid
//memory leak.
CFRelease(dict);
}
//release mainLoop memory
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
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) {
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)) {
//Register a callback to destroy the corresponding RunLoop when the current
//thread is destroyed.
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS - 1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
The code above shows:
1. The RunLoop is associated with a thread and is stored in a global dictionary, where the thread acts as the key.
2. The RunLoop of the main thread is initialized when the global dictionary is created.
3. The RunLoop of secondary threads is created the first time it is accessed.
4. The RunLoop is destroyed when the corresponding thread is destroyed.
2.3、Source code of CFRunLoopRunSpecific
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) /* DOES CALLOUT */
{
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
//If no mode is found or the found mode has no registed events,the RunLoop will
//exit without entering the loop.
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);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
//Notify the observer that the RunLoop is about to enter.
if (currentMode->_observerMask & kCFRunLoopEntry)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//Notify the observer that the RunLoop has exited
if (currentMode->_observerMask & kCFRunLoopExit)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
2.4、Source code of _CFRunLoopRun
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
//Obtain the cpu runtime since the system booted,used for controlling the timeout
//duration
uint64_t startTSR = mach_absolute_time();
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}
//mach ports,message are passed between ports in the kernel.Initially set to 0.
mach_port_name_t dispatchPort = MACH_PORT_NULL;
//Check if it is main thread
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
// If it is the main thread && the RunLoop is the main thread's RunLoop && the
//mode is commonMode, assign the port used by the main thread for receiving and
//sending messages to the mach port.
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) {
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
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 represents the timeout duration.When the timeout occurs,the
//__CFRunLoopTimeOut function will be excuted.
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);
}
//never timeout
else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}
//The flag's default value is true
Boolean didDispatchPortLastTime = true;
//Records the last state of runloop,which will be used for returning.
int32_t retVal = 0;
do {
//a buffer pool for storing kernel messages
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
//access all ports that need to be mornitored
__CFPortSet waitSet = rlm->_portSet;
//Set the RunLoop to a state where it can be woken up.
__CFRunLoopUnsetIgnoreWakeUps(rl);
//2.Notify the observer that the timer callback is about to be triggered and
//handle the timer event
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//3.Notify the observer that the source0 callback is about to be triggered
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//Excute the block that has beed added to current runloop
__CFRunLoopDoBlocks(rl, rlm);
//4.handle the source0 event.
//if there are events that need to be handled return true;
//otherwise, return false
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
//if there are no source0 events to handle and no timeout,poll is false
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
//The first do...while loop will not enter this branch,because didDispatchPortLastTime is initialized as true
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
//read messages form buffer pool
msg = (mach_msg_header_t *)msg_buffer;
//5.receive source1 events
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
goto handle_msg;
}
#elif DEPLOYMENT_TARGET_WINDOWS
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}
didDispatchPortLastTime = false;
//6.Notify the observer that the runloop is about to enter a sleeping state
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//Set the run loop to a sleeping state
__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
//there is an inner loop used to receive messages form waiting port
//The thread will enter a sleeping state when it enters this loop.It will exit
//the loop only when it receives new messages and continue excuting the run
//loop.
do {
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
//7.receive messages from waitset port
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
//When a messages is received,the value of liveport is set to
//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
//Cancel the sleeping state of the run loop
__CFRunLoopUnsetSleeping(rl);
//8.Notify the observer that runloop has been woken up
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//9.Handle the received messages
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
//if the event is timer
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
//9.1 Handle timer event
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
//if the event is timer
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.1Handle timer event
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
//If it is a block dispatched to the main queue
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 excute 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);
// There are source1 events that need to be handled
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
//9.2 Handle soutce1 events
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) {
//It will be passed as a parameter when entering the RunLoop,and will
//return after handling all events
retVal = kCFRunLoopRunHandledSource;
}else if (timeout_context->termTSR < mach_absolute_time()) {
//timeout
retVal = kCFRunLoopRunTimedOut;
}else if (__CFRunLoopIsStopped(rl)) {
//The run loop is manually terminated.
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
}else if (rlm->_stopped) {
//The mode is terminated
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
}else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
//There is nothing need to be handled in the current mode.
retVal = kCFRunLoopRunFinished;
}
//Except for the cases mentioned above,continue the loop
} while (0 == retVal);
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}
return retVal;
}
The source code of __CFRunLoopRun is quite long.In fact,it is essentially a do...while loop internally,when this function is called,the thread will remain in this loop until it times out or is manually stoped.Within the loop,the thread is in a sleeping state when it is idle,and it processes events when there are tasks that need to be handled.
1 Notify the observer that the RunLoop has started.
2 Notify the observer that the timer is about to trigger.
3 Notify the observer that any input source not based on ports will trigger.
4 Trigger all non-port-based input source that are ready to trigger.
5 If a port-based input source is ready and waiting to start,handle the event immediately;then proceed to step 9.
6 Notify the observer that the thread is entering a sleeping state.
7 Put the thread into a sleeping state unitl one of following events occurs:
·An event arrives at a port-based source.
·The timer triggers.
·The timeout set by the RunLoop has elapsed.
·The RunLoop is woken up.
8 Notify the observer that the thread is about to wake up.
9 Handle any unprocessed events.
If a user-defined timer starts, handle the timer event and restart the RunLoop. Proceed to step 2.
If an input source starts, pass the corresponding message.
If the RunLoop is woken up and the time has not yet timed out, restart the RunLoop. Proceed to step 2.
10 Notify the observer that the RunLoop has ended.
2.5、source code of __CFRunLoopServiceMachPort
If you scrutinize the source coe of __CFRunLoopRun method,you will find that there is an internalloop within the method.This loop puts thread into a sleeping state until it receives new messages, at which point it exits the loop and continues excuting the RunLoop.These messages are communicated between processes baseed on Mach ports.
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; //the flag of the message's header
msg->msgh_local_port = port; //Source(the message beging sent)or Target(the message being received)
msg->msgh_remote_port = MACH_PORT_NULL; //Target (the message beging received)or Source(the message being sent)
msg->msgh_size = buffer_size; //The size of the message buffer,measured in bytes
msg->msgh_id = 0; //unique id
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
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);
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
if (MACH_RCV_TOO_LARGE != ret) break;
//here assgin a larger memory to the 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;
}
From the source code above we can see that this method receives messages form a specified kernel port,then stores them in a buffer for external retrieval.The core of this method is the mach_msg function,which handles message sending and receiving.The RunLoop calls this funtion to receive messages,and if no message is received from the port,the kernel will place the thread to a waiting state.
2.6RunLoop handles events
In the previous section, we explored the souce code of the RunLoop's core function,__CFRunLoopRun.Based on the offcial documentation, we summarized the event handling process.The source code shows that handling events primarily involves the following functions:
__CFRunLoopDoObservers: handle notification events.
__CFRunLoopDoBlocks: handle block events.
__CFRunLoopDoSource0: handle source0 events.
__CFRunLoopDoSource1: handle source1 events.
__CFRunLoopDoTimers: handle timer events.
CFRUNLOOP_IS_SERVICEING_THE_MAIN_DISPATCH_QUEUE: GCD main queue
We don't need to be concerned with the implementation of these functions.However, what we should focus on is how these functions call back to higher level after handing events.For example, __CFRunLoopDoSource0 handles system events,so if we trigger a UIButton click event ,examining the function call stack of function should reveal how the callback to higher level occurs.
As shown in the image above, the call stack of a UIButton click event clearly reveals the invocation of the __CFRunLoopDoSources0 method,followed by the call to _CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE_PERFORM_FUNCTION,which then returns control to th UIKit level.
Regarding the method mentioned above, the callback to the higher-level method is illustrated in the image below:
2.7、Summary
- RunLoop's run must sepecify a mode,and it also needs to register task events for this mode.
- RunLoop runs in the default mode;of course, you can alse sepecify a mode to run,but you can only run in one mode at a time.
- In fact,the RunLoop internally maintains a do-while loop.The thread will always stay inside this loop until it times out or is manually stopped.
- The core of the RunLoop is the mach_msg().The RunLoop calls this function to receive messages.if no one sends port messages to the RunLoop,the kernel will place the thread in a waiting state;othewise the thread will handle events.
3、Applications of RunLoop
3.1、NSTimer
If you have used NSTimer for timer-based development,you'll know that NSTiemr object need to be added to a RunLoop to execute properly.As mentioned earlier,CFRunLoopTimer and NSTimer is toll-free-bridge.When an NSTimer is registered with a RunLoop,the RunLoop schedules events at its repeat intervals.This is especially relevant when using NSTimer within a scroll view,where mode changes can cause the timer to stop working.To fix this issue, you should register the NSTimer in NSRunLoopCommonModes of the RunLoop.
GCD timers works differently,Thread management in GCD is handled directly by the system. The GCD timer sends messages to the RunLoop via dispatch ports to trigger the associated block. If there’s no RunLoop in the current thread, GCD temporarily creates a thread to execute the block and destroys it afterward. Therefore, GCD timers do not rely on the RunLoop.
3.2、Stutter Detection
We can detect stutter in page regreshing by mornitoring the different states of the RunLoop.
- (void)start{
[self registerObserver];
[self startMonitor];
}
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
DSBlockMonitor *monitor = (__bridge DSBlockMonitor *)info;
monitor->activity = activity;
// send a signal
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : minimum priority
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
- (void)startMonitor{
// create a signal
_semaphore = dispatch_semaphore_create(0);
// monitor the duration on a secondary thread
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
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(@"detect stutter");
}
}
self->_timeoutCount = 0;
}
});
}
As shown in the code above,it is a simple piece of code for detecting page stuttering.The principle is to use the time interval between the RunLoop entering the KCFRunLoopBeforeSources state(processing source events)and transitioniong to the KCFRunLoopAfterWaiting state as the basis for judgment.Of course,this is just a basic demo,but the various state transitions of the RunLoop provide a theoretical foundation for many excellent third-party libraries for stutter detection.
3.3 Persistent thread
Sometimes, we need to create a thread in the background to continuously perform tasks. However, a regular thread will be destroyed immediately after completing its task. Therefore, we need a persistent thread to keep it running indefinitely.
@interface ViewController ()
@property (nonatomic, strong) NSThread *workerThread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//Create and start a persistent thread
self.workerThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntryPoint) object:nil];
[self.workerThread start];
}
- (void)threadEntryPoint {
@autoreleasepool {
// Add a port to the runloop of the child thread to keep the thread alive
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
NSLog(@"Persistent thread stopped");
}
}
- (void)run2 {
NSLog(@"Task in a persistent thread");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.workerThread) {
//perform the task on the persistent thread
[self performSelector:@selector(run2) onThread:self.workerThread withObject:nil waitUntilDone:NO];
}
}
- (void)dealloc {
// ensure the thread stops properly
if (self.workerThread) {
[self.workerThread cancel];
self.workerThread = nil;
}
}
@end
The code above demonstrates a persistent thread. By adding the thread workerThread to the RunLoop, we ensure the thread remains active. Before starting the RunLoop, we must set a mode and add at least one Source, Timer, or Observer to the mode. In this case, a port is added. Although messages can be sent to the RunLoop through the port, no messages are actually sent here. As a result, the RunLoop does not exit, achieving a persistent thread.