This part is mainly runloop source code parsing
The following diagram is probably the most classic illustration of runloops, from a deeper understanding of runloops:
Next, I will look at some of the details of the source code in detail, some parts of the code will be longer, pasted here to keep it intact.
Entry: CFRunLoopRun
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
// Start with DefaultMode.
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0 e10.false);
CHECK_FOR_FORK();
} while(kCFRunLoopRunStopped ! = result && kCFRunLoopRunFinished ! = result); }// Start with the specified mode, allowing you to set the timeout
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
Copy the code
The CFRunLoopRunSpecific function uses the second parameter CFStringRef modeName to specify the Mode in which the runloop will run. Here, the modeName argument is a string, not a wrapped RunLoopMode object. The seconds parameter determines how long the runloop can run when the external call is passed in. The external call is implemented using GCD timer or MK Timer internally.
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
// select a mode based on modeName
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
/// If mode is empty, that is, there is no mode Item (source, timer, observer).
/// So for runloop to survive, it must have a mode item or block to execute.
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
// _per_run_data is the data associated with each run of the runloop. This has been described in detail before.
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
// Notify the observer of the kCFRunLoopEntry status
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// Enter the runloop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
/// runloop exits, notifying the Observer of the status of kCFRunLoopExit
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
Copy the code
__CFRunLoopFindMode
The __CFRunLoopFindMode function is long, but its purpose is clear: find runloopMode objects based on runloop objects and modeName, and create a new one if none is found.
/* call with rl locked, returns mode locked */
static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) {
CHECK_FOR_FORK();
CFRunLoopModeRef rlm;
struct __CFRunLoopMode srlm;
// Set the specific length of the memory region to which the SRLM pointer points to 0
memset(&srlm, 0.sizeof(srlm));
_CFRuntimeSetInstanceTypeIDAndIsa(&srlm, __kCFRunLoopModeTypeID);
srlm._name = modeName;
// Query mode objects from the _modes container of the runloop object
rlm = (CFRunLoopModeRef)CFSetGetValue(rl->_modes, &srlm);
if (NULL! = rlm) { __CFRunLoopModeLock(rlm);return rlm;
}
if(! create) {return NULL;
}
// If there is no mode object, create a new one.
rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL);
if (NULL == rlm) {
return NULL;
}
__CFRunLoopLockInit(&rlm->_lock);
rlm->_name = CFStringCreateCopy(kCFAllocatorSystemDefault, modeName);
rlm->_stopped = false;
rlm->_portToV1SourceMap = NULL;
rlm->_sources0 = NULL;
rlm->_sources1 = NULL;
rlm->_observers = NULL;
rlm->_timers = NULL;
rlm->_observerMask = 0;
rlm->_portSet = __CFPortSetAllocate();
rlm->_timerSoftDeadline = UINT64_MAX;
rlm->_timerHardDeadline = UINT64_MAX;
kern_return_t ret = KERN_SUCCESS;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
rlm->_timerFired = false;
// This queue will be useful later?
rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue".0);
mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***".- 1);
rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0.0, rlm->_queue);
__block Boolean *timerFiredPointer = &(rlm->_timerFired);
dispatch_source_set_event_handler(rlm->_timerSource, ^{
*timerFiredPointer = true;
});
// Set timer to far out there. The unique leeway makes this timer easy to spot in debug output.
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321);
dispatch_resume(rlm->_timerSource);
ret = __CFPortSetInsert(queuePort, rlm->_portSet);
if(KERN_SUCCESS ! = ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
#if USE_MK_TIMER_TOO
// What does _timerPort do?
rlm->_timerPort = mk_timer_create();
ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
if(KERN_SUCCESS ! = ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
Where is the _wakeUpPort of the runloop object used?
ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
if(KERN_SUCCESS ! = ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);
#if DEPLOYMENT_TARGET_WINDOWS
rlm->_msgQMask = 0;
rlm->_msgPump = NULL;
#endif
CFSetAddValue(rl->_modes, rlm);
CFRelease(rlm);
__CFRunLoopModeLock(rlm); /* return mode locked */
return rlm;
}
Copy the code
__CFRunLoopModeIsEmpty
The __CFRunLoopModeIsEmpty function receives a runloop object and a runloopMode parameter to determine whether the runloopMode has a Source (also known as a runloopMode Item or block). If the runloop has a task to execute, there is no reason for the runloop to run without a task.
// expects rl and rlm locked
static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) {
CHECK_FOR_FORK();
if (NULL == rlm) return true;
#if DEPLOYMENT_TARGET_WINDOWS
if (0! = rlm->_msgQMask)return false;
#endif
/// The main thread will be special, and its mode item will definitely not be empty
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL== previousMode) || (! HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY &&0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue
// Determine source0, source1, and timer
if (NULL! = rlm->_sources0 &&0 < CFSetGetCount(rlm->_sources0)) return false;
if (NULL! = rlm->_sources1 &&0 < CFSetGetCount(rlm->_sources1)) return false;
if (NULL! = rlm->_timers &&0 < CFArrayGetCount(rlm->_timers)) return false;
// execute the block tasks in runloop.
struct _block_item *item = rl- > _blocks_head;
while (item) {
struct _block_item *curr = item;
item = item->_next;
Boolean doit = false;
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
} else {
doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
}
if (doit) return false;
}
return true;
}
Copy the code
This function determines if there is a block task.
doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
Copy the code
If condition 1 (the name of the current runloopMode is the same as that stored in the block) and condition 2 (the block task is specified to be executed in CommonModes and the name of the current runloopMode is in CommonModes) are met, the block task needs to be executed.
Runloop supports nesting
Notice this simplified process:
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
// run runloop for all processes from Entry to Exit
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
Copy the code
This code illustrates the following three points:
- Before runloop runs, previous data (previousrun) and mode (previousMode) are saved
- After the runloop is complete, previous data (previousPerRun) and mode (previousMode) will be recovered
- Runloops support nesting: When a runloop is run again (such as the callout function that is processing a task), the data and state of the current runloop is saved and then restored.
On the third point, apple’s official documentation is pretty clear:
Run loops can be run recursively. You can call CFRunLoopRun() or CFRunLoopRunInMode(::_:) from within any run loop Callout and create run loop activations on the current thread’s call stack. You are not restricted in which modes you can run from within a callout. You can create another run loop activation running in any available run loop mode, including any modes already running higher in the call stack.
PerRunData and runloopMode can only be restored by runloop.
summary
To summarize what the CFRunLoopRunSpecific() function does:
- __CFRunLoopFindMode finds the corresponding runloopMode
- __CFRunLoopModeIsEmpty checks if source is empty
- Save PerRunData and runloopMode for runloop
- __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry)
- __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
- __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
- Restore PerRunData and runloopMode to runloop
The __CFRunLoopDoObservers function synchronizes the kCFRunLoopEntry state to each Observer to execute its own callback function.
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
Copy the code
Let’s look at the actual runloop entry: __CFRunLoopRun.
The real entry: __CFRunLoopRun
__CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); The function takes a lot of arguments.
Its function prototype is as follows:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) __attribute__((noinline));
Copy the code
This function is very, very long… But that’s the core of how Runloop works. The entire code is posted here to keep it intact, and source code interpretation is added to the comments.
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
uint64_t startTSR = mach_absolute_time();
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}
mach_port_name_t dispatchPort = MACH_PORT_NULL;
// check whether the queue is main_queue.
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL== previousMode) || (! HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY &&0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
/// what is dispatchPort?
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
#if USE_DISPATCH_SOURCE_FOR_TIMERS
// Do you use GCD timer?
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
// This code indicates that the runloop interval is also implemented using the GCD timer. Is dispatch_source_t associated with runloop source??
/// GCD timer is kernel dependent, so it is very accurate. Not affected by 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;
} else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
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);
/// associate timeout_context with timer
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
// __CFRunLoopTimeout is a pointer to a function that calls CFRunLoopWakeUp(context->rl); Used to wake up runloop
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;
}
Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
do {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
voucher_t voucherCopy = NULL;
#endif
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
__CFPortSet waitSet = rlm->_portSet;
__CFRunLoopUnsetIgnoreWakeUps(rl);
/// Note: Runloop is about to start working, handling timer, source, block, etc.
// notify the Observer of the kCFRunLoopBeforeTimers status
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// notify the Observer of the status of kCFRunLoopBeforeSources
/// the source0 callback is about to trigger, not port
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// execute the block added to the runloop
__CFRunLoopDoBlocks(rl, rlm);
// Handle the Sources0 event, where is the Timer event handled?? Why put timer after runloop wake up?
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
/// there is a __CFRunLoopDoBlocks
__CFRunLoopDoBlocks(rl, rlm);
}
/* Each time the loop processes the source0 task, the poll value will be true. The immediate effect is not to doobservers-beforewaiting and doobservers-afterwaiting. That is, runloop will go straight to sleep and will not tell BeforeWaiting and AfterWaiting activities. So you see, there may be cases where the runloop passes through several loops, but the Observer you registered will not receive a callback. * /
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
if(MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) {#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
msg = (mach_msg_header_t *)msg_buffer;
// There is a Mach port which is the source1 event and corresponds to a Mach MSG.
/// All user operations and network operations are performed through port.
/// why is this dispatchPort?
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
#elif DEPLOYMENT_TARGET_WINDOWS
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0.0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}
/// This is what runloop does, after which runloop will go to sleep.
/ / / and if you have to use the port, then through the handle_msg jump to __CFRunLoopSetIgnoreWakeUps, namely the runloop sensei
didDispatchPortLastTime = false;
/// Note: Runloop is done and is about to go to sleep
/// hibernate, that is, do while(1) where mach_MSg_trap () is executed to switch to kernel state. After that, runloop can only be actively woken up by source1.
/// The user touch event is wrapped as a source1 event, passed to the Mach layer via port, mach_msg, and then the TRAP () function is called to complete the OS state switch.
/// notify the observer of the kCFRunLoopBeforeWaiting state
if(! poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);/// The runloop starts sleeping, i.e. the following do-while(1) loop.
__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);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
/// do while(1) is only a loop, so how does runloop sleep??
do {
if (kCFUseCollectableAllocator) {
// objc_clear_stack(0);
// <rdar://problem/16393959>
memset(msg_buffer, 0.sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
/* The __CFRunLoopServiceMachPort function is crucial. Call mach_msg and wait for the MSG of the Mach port to be received. The thread is about to go to sleep until it is woken up by one of the following events: 1, a port-based source1 event 2, a timer's trigger time 3, runloop's own timeout 4, and manually woken up using the WakeUp function call. After this function is executed, the runloop will sleep and the thread will sleep. Until the runloop is woken up, the thread is woken up!! This is important. What does waitSet, the first argument to the function, tell us? Hibernate, but can receive Mach MSG for waitSet port. * /
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
/// While (1) the runloop has gone to sleep.
if(modeQueuePort ! = MACH_PORT_NULL && livePort == modeQueuePort) {/// what does this while loop do? What is the homework for break inside, don't you understand?
// 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);
// <rdar://problem/16393959>
memset(msg_buffer, 0.sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
// call mach_msg and wait for the MSG of the Mach port to be received. Thread to sleep
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
#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
/// Once you exit the above do-while(1), runloop is awakened
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
/// The runloop sleep time can be used to do something.
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
// 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);
// __CFRunLoopSetSleeping(rl); With __CFRunLoopUnsetSleeping (rl); One-to-one correspondence.
// Hibernate and wake up runloop
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
/// How the thread is woken up is irrelevant. The kernel handles it itself.
// synchronize kCFRunLoopAfterWaiting to the Observer: it just woke up from sleep
if(! poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);// When the message is received, the message is processed. That is, to wake up the runloop through port, you can go directly to the goto statement.
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
else if(modeQueuePort ! = MACH_PORT_NULL && livePort == modeQueuePort) {/// The runloop is woken up and continues to do things. Why not reuse the code above?
/// The timer is woken up
CFRUNLOOP_WAKEUP_FOR_TIMER();
/// triggers the timer callback function
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
if(! __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {// Re-arm the next timer__CFArmNextTimerInMode(rlm, rl); }}#endif
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
/// dispatchPort is the main thread. Yes!
/// Execute if there are blocks to the main queue.
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
// register a callback
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0.NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else {
/// where is source0 or source1? Source1, based on port
/// can be active wake up
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
// Despite the name, this works for windows handles as well
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
/// Handle the source1 event
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
}
// Restore the previous voucher
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
if(msg && msg ! = (mach_msg_header_t *)msg_buffer) free(msg);
#endif
// execute the block added to the runloop.
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
/// When entering runloop, the argument says to return as soon as the event is processed
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
/// The runloop timed out
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
/// stopped by an external caller
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
/// stopped by an external caller
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
/// all mode items are missing
retVal = kCFRunLoopRunFinished;
}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
voucher_mach_msg_revert(voucherState);
os_release(voucherCopy);
#endif
// retVal is a runloop that has finished processing all the events and then continues the loop, do-while(1).
} while (0 == retVal);
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}
return retVal;
}
Copy the code
This function does look tired, and it’s easy to tease out a few core points.
Outer loop do-while(0 == retVal)
The outer loop is the complete flow of a loop of runloop.
Once retVal is non-zero, the outer do-while loop exits and the __CFRunLoopRun function returns a non-zero value, indicating the end of the runloop. RetVal becomes non-zero only in the following ways:
/* Reasons for CFRunLoopRunInMode() to Return */
typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
kCFRunLoopRunFinished = 1,
kCFRunLoopRunStopped = 2,
kCFRunLoopRunTimedOut = 3,
kCFRunLoopRunHandledSource = 4
};
Copy the code
Take a close look at the source code below
if (sourceHandledThisLoop && stopAfterHandle) {
// When entering runloop, the argument says to return when the event is processed
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// The runloop timed out
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
// Stopped by an external caller
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
// Stopped by an external caller
// What's the difference?
// __CFRunLoopIsStopped return (rl->_perRunData->stopped)? true : false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// No mode item is displayed
retVal = kCFRunLoopRunFinished;
}
Copy the code
The conclusion is obvious:
- StopAfterHandle is the fourth argument to the __CFRunLoopRun function and the fourth argument to the CFRunLoopRunSpecific function returnAfterSourceHandled. This parameter is false when the CFRunLoopRun function is called, so the run: of NSRunLoop cannot be stopped once executed. When the CFRunLoopRunInMode function is called, this parameter is set when runUntilDate: or runMode:beforeDate: of NSRunLoop is called. So if the conditions are met, the loop task is finished (sourceHandledThisLoop) and then exits.
- Runloop timeouts are determined by timeout_context.
- __CFRunLoopIsStopped and RLM ->_stopped are __CFRunLoopIsStopped. Is it an external force stop? Temporarily not clear how to stop??
- Call __CFRunLoopModeIsEmpty again to determine whether the source of runloopMode is empty or terminate the runloop if it is not.
Inner loop do-while(1)
The inner do-while(1) loop, the runloop, goes to sleep. The thing to do is to call the __CFRunLoopServiceMachPort function.
The following code is executed to enter hibernation (kernel mode). Once the inner do-while(1) exits the loop, the runloop wakes up
do {
if (kCFUseCollectableAllocator) {
// objc_clear_stack(0);
// <rdar://problem/16393959>
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, &voucherState, &voucherCopy);
// While (1) the runloop has gone to sleep.
if(modeQueuePort ! = MACH_PORT_NULL && livePort == modeQueuePort) {// What does this while loop do? What is the homework for break inside, don't you understand?
// 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);
Copy the code
The __CFRunLoopServiceMachPort function is key. Call mach_msg and wait for the MSG of the Mach port to be received. The thread is about to go to sleep until it is awakened by one of the following events:
- A port-based source1 event
- The trigger time of a timer
- Runloop’s own timeout is up
- Manual wake up.
Runloop goes to sleep
__CFRunLoopServiceMachPort is where Mach MSG comes in. The mach_msg function is executed to switch the system from user state to kernel state. And the mach_msg call code is pretty impressive…
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, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
// Another loop
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 port direction is from _dispatch_get_main_queue_port_4CF to MACH_PORT_NULL.
msg->msgh_local_port = port;
msg->msgh_remote_port = MACH_PORT_NULL;
msg->msgh_size = buffer_size;
msg->msgh_id = 0;
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|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);
// Take care of all voucher-related work right after mach_msg.
// If we don't release the previous voucher we're going to leak it.
voucher_mach_msg_revert(*voucherState);
// Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
*voucherState = voucher_mach_msg_adopt(msg);
if (voucherCopy) {
if(*voucherState ! = VOUCHER_MACH_MSG_STATE_UNCHANGED) {// Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy.
// CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged.
*voucherCopy = voucher_copy();
} else {
*voucherCopy = 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;
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
Copy the code
This code has a for loop that keeps building mach_msg and executing the mach_msg() function. The mach_msg_trap() system call is actually called inside mach_msg(), so this function triggers a change in the operating system state from user mode to kernel mode, and runloop goes to sleep.
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|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);
Copy the code
If you have MSG to process, jump to handle_msg. This step corresponds to having Source1 to deal with.
DidDispatchPortLastTime is true for the first time and will not be executed
if(MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) { msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
gotohandle_msg; }}Copy the code
One thing to note about hibernation is that when runloop goes to sleep, the thread goes to sleep, and the CPU doesn’t do anything. Once the mach_msg() function is used to send the sleep message to the Mach kernel and the system is in kernel state, all you can do is wait. Just wait for the runloop to wake up.
Runloop awakened
Exit the inner loop and the runloop is woken up. There are two situations??
ModeQueuePort and livePort judgments, or RLM ->_timerFired = false; Why??
// While (1) the runloop has gone to sleep.
if(modeQueuePort ! = MACH_PORT_NULL && livePort == modeQueuePort) {// What does this while loop do? What is the homework for break inside, don't you understand?
// 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;
}
Copy the code
Wake up condition:
- Source1 event to port
- Time is up
- Runloop timeout??
- Awakened by the caller?
summary
After knowing that the inner do-while(1) is the key point that the system triggers the state switch and enters the sleep state, the respective functions of the code before and after the inner loop can be easily understood according to the runloop flow chart.
- Inner loop do-while(1) : the Timer, Source, and Block tasks are processed, after which the runloop goes to sleep
- After the inner loop do-while(1) : Runloop wakes up to handle some task (in this case, the task for Source1)
Inner loop before do-while(1)
This is where the runloop performs its tasks, before the runloop goes to sleep, and the process is as follows:
- Set the state of runloop
- Notify the BeforeTimers and BeforeSources states to the Observer
- Executing a Block Task
- Execute the Sources0 event
- Notify the BeforeWaiting state to the Observer
- Set the state of runloop
There is a question about this phase: the BeforeTimers state is sent to the Observer, but the Timer event is not executed. Why?
In fact, the Timer event is executed after the runloop has gone to sleep and then woken up, after the inner loop do-while(1). This can be seen in the timing of the __CFRunLoopDoTimers call.
A more detailed explanation of what Runloop does will come in a later section in __CFRunLoopDoXXX, where things really do happen.
Inner loop after do-while(1)
This is the phase where the runloop is awakened to perform the task.
Rl ->_sleepTime += (poll? 0.0: (CFAbsoluteTimeGetCurrent() -sleepstart)); * * *. And then set wake up after some of the state (__CFRunLoopSetIgnoreWakeUps, __CFRunLoopUnsetSleeping). Notify the Observer of AfterWaiting status:
if(! poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);Copy the code
Next, it’s time for the runloop to process the task once it’s woken up.
- Determine the livePort and modeQueuePort
- Determine the _timerPort and perform the timer callback
- Judge dispatchPort
- If none of the above, then the Source1 event. Find the corresponding Source1 based on runloop, runloopMode, and livePort, and then call __CFRunLoopDoSource1 to perform the task. If there is a Mach MSG reply, the mach_msg function will send the reply message to the specified Mach port. This is how the Mach core communicates with threads.
- Finally, the block task is executed again. The outer do-while loop is then continued for the next loop.
__CFRunLoopDoXXX where things are actually done
The runloop source code has a set of functions starting with __CFRunLoopDo:
__CFRunLoopDoBlocks(rl, rlm);
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
Copy the code
These functions are key to what Runloop does, as described in more detail below. Note that each function receives a runloop object and a runloopMode object as parameters, which confirms the basic fact that runloopModes are isolated from each other and handle their own tasks.
The order in which these functions are explained is independent of the order in which they are actually executed.
Process Block task __CFRunLoopDoBlocks
__CFRunLoopDoBlocks(RL, RLM) executes the block task added to the runloop, traversing the _block_item list structure, and firing CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK(block); To execute a block.
// Call with rl and rlm locked
static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) {
if(! rl->_blocks_head)return false;
if(! rlm || ! rlm->_name)return false;
Boolean did = false;
struct _block_item *head = rl- > _blocks_head;
struct _block_item *tail = rl- > _blocks_tail;
rl->_blocks_head = NULL;
rl->_blocks_tail = NULL;
CFSetRef commonModes = rl->_commonModes;
CFStringRef curMode = rlm->_name;
// select * from rL and RLM
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
struct _block_item *prev = NULL;
struct _block_item *item = head;
while (item) {
struct _block_item *curr = item;
item = item->_next;
Boolean doit = false;
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
} else {
doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
}
if(! doit) prev = curr;if (doit) {
if (prev) prev->_next = item;
if (curr == head) head = item;
if (curr == tail) tail = prev;
// _block_item->_block
void (^block)(void) = curr->_block;
CFRelease(curr->_mode);
free(curr);
if (doit) {
/// what does runloop mean by block??
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
did = true;
}
Block_release(block); // do this before relocking to prevent deadlocks where some yahoo wants to run the run loop reentrantly from their dealloc
}
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
if (head) {
tail->_next = rl->_blocks_head;
rl->_blocks_head = head;
if(! rl->_blocks_tail) rl->_blocks_tail = tail; }return did;
}
Copy the code
A prototype callout function that actually executes a block is as follows:
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void)) {
if (block) {
block();
}
asm __volatile__(""); // thwart tail-call optimization
}
Copy the code
_block_item
Each runloop object has two fields, _blocks_head and _blocks_tail, which are both _block_item structures. The _block_item structure in runloop clearly forms a linked list, with each node storing an _block object.
struct _block_item {
struct _block_item* _next;
CFTypeRef _mode; // CFString or CFSet
void (^_block)(void);
};
Copy the code
So, where are these two fields of the Runloop object populated?
How are blocks added to runloops
The CFRunLoopPerformBlock function wraps the external block as a _block_item structure and places it in the _blocks_head and _blocks_tail fields of the Runloop object.
void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void)) { CHECK_FOR_FORK(); if (CFStringGetTypeID() == CFGetTypeID(mode)) { mode = CFStringCreateCopy(kCFAllocatorSystemDefault, (CFStringRef)mode); __CFRunLoopLock(rl); // ensure mode exists CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, (CFStringRef)mode, true); if (currentMode) __CFRunLoopModeUnlock(currentMode); __CFRunLoopUnlock(rl); } else if (CFArrayGetTypeID() == CFGetTypeID(mode)) { CFIndex cnt = CFArrayGetCount((CFArrayRef)mode); const void **values = (const void **)malloc(sizeof(const void *) * cnt); CFArrayGetValues((CFArrayRef)mode, CFRangeMake(0, cnt), values); mode = CFSetCreate(kCFAllocatorSystemDefault, values, cnt, &kCFTypeSetCallBacks); __CFRunLoopLock(rl); // ensure modes exist for (CFIndex idx = 0; idx < cnt; idx++) { CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, (CFStringRef)values[idx], true); if (currentMode) __CFRunLoopModeUnlock(currentMode); } __CFRunLoopUnlock(rl); free(values); } else if (CFSetGetTypeID() == CFGetTypeID(mode)) { CFIndex cnt = CFSetGetCount((CFSetRef)mode); const void **values = (const void **)malloc(sizeof(const void *) * cnt); CFSetGetValues((CFSetRef)mode, values); mode = CFSetCreate(kCFAllocatorSystemDefault, values, cnt, &kCFTypeSetCallBacks); __CFRunLoopLock(rl); // ensure modes exist for (CFIndex idx = 0; idx < cnt; idx++) { CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, (CFStringRef)values[idx], true); if (currentMode) __CFRunLoopModeUnlock(currentMode); } __CFRunLoopUnlock(rl); free(values); } else { mode = NULL; } /// Copy the block. block = Block_copy(block); if (! mode || ! block) { if (mode) CFRelease(mode); if (block) Block_release(block); return; } __CFRunLoopLock(rl); struct _block_item *new_item = (struct _block_item *)malloc(sizeof(struct _block_item)); new_item->_next = NULL; new_item->_mode = mode; new_item->_block = block; _block_item in runloop forms the list structure, and new blocks are added to the end of the list. if (! rl->_blocks_tail) { rl->_blocks_head = new_item; } else { rl->_blocks_tail->_next = new_item; } rl->_blocks_tail = new_item; __CFRunLoopUnlock(rl); }Copy the code
Looking at the last part of the code, _block_item in runloop forms the list structure. The list starts with _blocks_head and ends with _blocks_tail. New blocks are added to the end of the list.
When is the CFRunLoopPerformBlock() function called? The answer is CFRunLoopPerformBlock, the external interface:
CFRunLoopPerformBlock -> Add block to runloop -> runloop wake up task -> __CFRunLoopDoBlocks -> __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block) -> block()Copy the code
Then everything was clear. Runloop can be used directly to execute blocks, but it is rarely used.
/// CFRunLoopPerformBlock copies blocks. void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void));Copy the code
Enqueues a block object on a given runloop to be executed as the runloop cycles in specified modes.
When the runloop runs in the specified mode, the block object is executed. You can use thisFunction as a means to offload work to another thread similar to Cocoa's performSelector:onThread:withObject:waitUntilDone:and related methods. You can also use it as an alternative to mechanisms such as putting a CFRunLoopTimer in the other thread's run loop, or using CFMessagePort to pass information between threads.
This method enqueues the block only and does not automatically wake up the specified run loop. Therefore, execution of the block occurs the next time the run loop wakes up to handle another input source. If you want the work performed right away, you must explicitly wake up that thread using the CFRunLoopWakeUp function.
Copy the code
CFRunLoopPerformBlock simply drops the block to the specified queue, but does not actively wake up the runloop. If the runloop is sleeping, the block will wait until the runloop is woken up by another source. You can of course wake up the runloop using the CFRunLoopWakeUp function.
CFRunLoopPerformBlock() is rarely used in common scenarios. It can be used to drop a block into the specified Mode of the runloop.
CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{
NSLog(@"CFRunLoopPerformBlock kCFRunLoopCommonModes");
});
Copy the code
Process the Timer task __CFRunLoopDoTimers
The __CFRunLoopDoTimers function iterates through all timers added to the runloopMode, filters out the timers whose execution time has expired, and calls the __CFRunLoopDoTimer(RL, RLM, RLT) function to execute the timer callback function.
// rl and rlm are locked on entry and exit
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) { /* DOES CALLOUT */
Boolean timerHandled = false;
CFMutableArrayRef timers = NULL;
for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx);
if(__CFIsValid(rlt) && ! __CFRunLoopTimerIsFiring(rlt)) {if (rlt->_fireTSR <= limitTSR) {
if(! timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault,0, &kCFTypeArrayCallBacks); CFArrayAppendValue(timers, rlt); }}}for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
timerHandled = timerHandled || did;
}
if (timers) CFRelease(timers);
return timerHandled;
}
Copy the code
__CFRunLoopDoTimer
The __CFRunLoopDoTimer function has more code.
// mode and rl are locked on entry and exit
static Boolean __CFRunLoopDoTimer(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt) { /* DOES CALLOUT */
Boolean timerHandled = false;
uint64_t oldFireTSR = 0;
/* Fire a timer */
CFRetain(rlt);
__CFRunLoopTimerLock(rlt);
if(__CFIsValid(rlt) && rlt->_fireTSR <= mach_absolute_time() && ! __CFRunLoopTimerIsFiring(rlt) && rlt->_runLoop == rl) {void *context_info = NULL;
void (*context_release)(const void *) = NULL;
if (rlt->_context.retain) {
context_info = (void *)rlt->_context.retain(rlt->_context.info);
context_release = rlt->_context.release;
} else {
context_info = rlt->_context.info;
}
Boolean doInvalidate = (0.0 == rlt->_interval);
__CFRunLoopTimerSetFiring(rlt);
// Just in case the next timer has exactly the same deadlines as this one, we reset these values so that the arm next timer code can correctly find the next timer in the list and arm the underlying timer.
rlm->_timerSoftDeadline = UINT64_MAX;
rlm->_timerHardDeadline = UINT64_MAX;
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopTimerFireTSRLock();
oldFireTSR = rlt->_fireTSR;
__CFRunLoopTimerFireTSRUnlock();
__CFArmNextTimerInMode(rlm, rl);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);
CHECK_FOR_FORK();
if (doInvalidate) {
CFRunLoopTimerInvalidate(rlt); /* DOES CALLOUT */
}
if (context_release) {
context_release(context_info);
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
__CFRunLoopTimerLock(rlt);
timerHandled = true;
__CFRunLoopTimerUnsetFiring(rlt);
}
if (__CFIsValid(rlt) && timerHandled) {
/* This is just a little bit tricky: we want to support calling * CFRunLoopTimerSetNextFireDate() from within the callout and * honor that new time here if it is a later date, otherwise * it is completely ignored. */
if (oldFireTSR < rlt->_fireTSR) {
/* Next fire TSR was set, and set to a date after the previous * fire date, so we honor it. */
__CFRunLoopTimerUnlock(rlt);
// The timer was adjusted and repositioned, during the
// callout, but if it was still the min timer, it was
// skipped because it was firing. Need to redo the
// min timer calculation in case rlt should now be that
// timer instead of whatever was chosen.
__CFArmNextTimerInMode(rlm, rl);
} else {
uint64_t nextFireTSR = 0LL;
uint64_t intervalTSR = 0LL;
if (rlt->_interval <= 0.0) {}else if (TIMER_INTERVAL_LIMIT < rlt->_interval) {
intervalTSR = __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT);
} else {
intervalTSR = __CFTimeIntervalToTSR(rlt->_interval);
}
if (LLONG_MAX - intervalTSR <= oldFireTSR) {
nextFireTSR = LLONG_MAX;
} else {
if (intervalTSR == 0) {
// 15304159: Make sure we don't accidentally loop forever here
CRSetCrashLogMessage("A CFRunLoopTimer with an interval of 0 is set to repeat");
HALT;
}
uint64_t currentTSR = mach_absolute_time();
nextFireTSR = oldFireTSR;
while (nextFireTSR <= currentTSR) {
nextFireTSR += intervalTSR;
}
}
CFRunLoopRef rlt_rl = rlt->_runLoop;
if (rlt_rl) {
CFRetain(rlt_rl);
CFIndex cnt = CFSetGetCount(rlt->_rlModes);
STACK_BUFFER_DECL(CFTypeRef, modes, cnt);
CFSetGetValues(rlt->_rlModes, (const void **)modes);
// To avoid A->B, B->A lock ordering issues when coming up
// towards the run loop from a source, the timer has to be
// unlocked, which means we have to protect from object
// invalidation, although that's somewhat expensive.
for (CFIndex idx = 0; idx < cnt; idx++) {
CFRetain(modes[idx]);
}
__CFRunLoopTimerUnlock(rlt);
for (CFIndex idx = 0; idx < cnt; idx++) {
CFStringRef name = (CFStringRef)modes[idx];
modes[idx] = (CFTypeRef)__CFRunLoopFindMode(rlt_rl, name, false);
CFRelease(name);
}
__CFRunLoopTimerFireTSRLock();
rlt->_fireTSR = nextFireTSR;
rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR);
for (CFIndex idx = 0; idx < cnt; idx++) {
CFRunLoopModeRef rlm = (CFRunLoopModeRef)modes[idx];
if (rlm) {
__CFRepositionTimerInMode(rlm, rlt, true);
}
}
__CFRunLoopTimerFireTSRUnlock();
for (CFIndex idx = 0; idx < cnt; idx++) {
__CFRunLoopModeUnlock((CFRunLoopModeRef)modes[idx]);
}
CFRelease(rlt_rl);
} else{ __CFRunLoopTimerUnlock(rlt); __CFRunLoopTimerFireTSRLock(); rlt->_fireTSR = nextFireTSR; rlt->_nextFireDate = CFAbsoluteTimeGetCurrent() + __CFTimeIntervalUntilTSR(nextFireTSR); __CFRunLoopTimerFireTSRUnlock(); }}}else {
__CFRunLoopTimerUnlock(rlt);
}
CFRelease(rlt);
return timerHandled;
}
Copy the code
Here, similar to how Runloop handles blocks, There is also a compelling function call to CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION(RLT ->_callout, RLT, context_info); , its function prototype is:
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
if (func) {
func(timer, info);
}
asm __volatile__(""); // thwart tail-call optimization
}
Copy the code
The first parameter, CFRunLoopTimerCallBack func, is the callback function specified for the timer. The third argument, info, can be used to pass arguments.
How is timer added to runloop
The CFRunLoopAddTimer function is used to add a timer to the runloop, that is, to the mode item container of the specified runloopMode.
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if(! __CFIsValid(rlt) || (NULL! = rlt->_runLoop && rlt->_runLoop ! = rl))return;
__CFRunLoopLock(rl);
/ / if commonMode
if (modeName == kCFRunLoopCommonModes) {
// First extract runloop commonModes, strings
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
// Set runloop commonModeItems to lazy loading
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
// Add timer to commonModeItems collection.
CFSetAddValue(rl->_commonModeItems, rlt);
if (NULL! =set) {
CFTypeRef context[2] = {rl, rlt};
/* add new item to all common-modes */
/ / to each element in the commonModes __CFRunLoopAddItemToCommonModes all the function
// Add the same timer to multiple modes.
// That is, use commonMode to synchronize timer to multiple modes.
// The commonMode flags are DefaultMode and UITrackingMode.
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set); }}else {
// Not commonMode, but an actual mode marked with commonMode
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL! = rlm) {if (NULL == rlm->_timers) {
CFArrayCallBacks cb = kCFTypeArrayCallBacks;
cb.equal = NULL;
// Add the timer to the timers array corresponding to the mode of the final runloop.
rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb); }}if (NULL! = rlm && ! CFSetContainsValue(rlt->_rlModes, rlm->_name)) { __CFRunLoopTimerLock(rlt);if (NULL == rlt->_runLoop) {
rlt->_runLoop = rl;
} else if(rl ! = rlt->_runLoop) { __CFRunLoopTimerUnlock(rlt); __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl);return;
}
CFSetAddValue(rlt->_rlModes, rlm->_name);
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopTimerFireTSRLock();
__CFRepositionTimerInMode(rlm, rlt, false);
__CFRunLoopTimerFireTSRUnlock();
if(! _CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {// Normally we don't do this on behalf of clients, but for
// backwards compatibility due to the change in timer handling...
if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
}
}
if (NULL! = rlm) { __CFRunLoopModeUnlock(rlm); } } __CFRunLoopUnlock(rl); }Copy the code
Using CFSetAddValue (RLT – > _rlModes RLM – > _name); And __CFRepositionTimerInMode (RLM, RLT, false); The timer function adds the timer to the specified runloopMode. __CFRepositionTimerInMode function executes CFArrayInsertValueAtIndex (timerArray newIdx, RLT); Add timer to the _timers container of runloopMode and call __CFArmNextTimerInMode(RLM, RLT ->_runLoop); The function loads the timer.
So the call chain of a Runloop timer is straightforward:
add timer to runloop forMode -> runloop is awakened to execute the task -> __CFRunLoopDoTimer -> __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(RLT ->_callout, RLT, context_info) -> Execute timer callback func(timer, info)Copy the code
This function involves quite a bit of CommonModes code and is quite helpful in understanding CommonModes. At the same time, it can be seen that the timer does not care about the activity of runloop, but only triggers the callback after the runloop is woken up. After the callback is executed, there is an operation to set when the next trigger will occur.
Process the Sources0 task __CFRunLoopDoSources0
__CFRunLoopDoSources0(rl, rlm, stopAfterHandle); Used to process Source0 tasks.
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// Why is there a DoBlocks??
__CFRunLoopDoBlocks(rl, rlm);
}
Copy the code
Its function source code is:
/* rl is locked, rlm is locked on entrance and exit */
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) __attribute__((noinline));
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) { /* DOES CALLOUT */
CHECK_FOR_FORK();
CFTypeRef sources = NULL;
Boolean sourceHandled = false;
/* Fire the version 0 sources */
if (NULL! = rlm->_sources0 &&0 < CFSetGetCount(rlm->_sources0)) {
// Execute __CFRunLoopCollectSources0 for sources0 in the mode of the runloop.
CFSetApplyFunction(rlm->_sources0, (__CFRunLoopCollectSources0), &sources);
}
if (NULL! = sources) { __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl);// sources is either a single (retained) CFRunLoopSourceRef or an array of (retained) CFRunLoopSourceRef
if (CFGetTypeID(sources) == CFRunLoopSourceGetTypeID()) {
CFRunLoopSourceRef rls = (CFRunLoopSourceRef)sources;
__CFRunLoopSourceLock(rls);
if (__CFRunLoopSourceIsSignaled(rls)) {
__CFRunLoopSourceUnsetSignaled(rls);
if (__CFIsValid(rls)) {
__CFRunLoopSourceUnlock(rls);
// Call the perform callback set in source0__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info); CHECK_FOR_FORK(); sourceHandled =true;
} else{ __CFRunLoopSourceUnlock(rls); }}else{ __CFRunLoopSourceUnlock(rls); }}else {
CFIndex cnt = CFArrayGetCount((CFArrayRef)sources);
CFArraySortValues((CFMutableArrayRef)sources, CFRangeMake(0, cnt), (__CFRunLoopSourceComparator), NULL);
for (CFIndex idx = 0; idx < cnt; idx++) {
CFRunLoopSourceRef rls = (CFRunLoopSourceRef)CFArrayGetValueAtIndex((CFArrayRef)sources, idx);
__CFRunLoopSourceLock(rls);
if (__CFRunLoopSourceIsSignaled(rls)) {
__CFRunLoopSourceUnsetSignaled(rls);
if(__CFIsValid(rls)) { __CFRunLoopSourceUnlock(rls); __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info); CHECK_FOR_FORK(); sourceHandled =true;
} else{ __CFRunLoopSourceUnlock(rls); }}else {
__CFRunLoopSourceUnlock(rls);
}
if (stopAfterHandle && sourceHandled) {
break;
}
}
}
CFRelease(sources);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
}
return sourceHandled;
}
Copy the code
CFRunLoopSourceContext stores the source callback, which can receive the parameter info.
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
Copy the code
Which invokes the CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION (RLS – > _context. Version0. Perform, rls->_context.version0.info); To carry out the mission. The function’s parameter Perform is the callback function, and the parameter info is the parameter accepted by the callback function.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
if (perform) {
perform(info);
}
asm __volatile__(""); // thwart tail-call optimization
}
Copy the code
How does Source0 wake up Runloop
To execute a task for source0, source0 must be set to Signal and then manually call wakeUp to wakeUp runloop in order to qualify for its callout function.
void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) {
CHECK_FOR_FORK();
__CFRunLoopSourceLock(rls);
if (__CFIsValid(rls)) {
__CFRunLoopSourceSetSignaled(rls);
}
__CFRunLoopSourceUnlock(rls);
}
Copy the code
The source0 task can be executed by calling CFRunLoopSourceSignal, setting the source0 to Signal, and calling wakeUp to wakeUp runloop. In the cfsocket.c file there is code like this:
if (shared->_source) {
CFRunLoopSourceSignal(shared->_source);
_CFRunLoopSourceWakeUpRunLoops(shared->_source);
}
Copy the code
And _CFRunLoopSourceWakeUpRunLoops is runloop the contents of the source code, including call __CFRunLoopSourceWakeUpLoop function, which in turn calls CFRunLoopWakeUp function.
CF_PRIVATE void _CFRunLoopSourceWakeUpRunLoops(CFRunLoopSourceRef rls) {
CFBagRef loops = NULL;
__CFRunLoopSourceLock(rls);
if (__CFIsValid(rls) && NULL! = rls->_runLoops) { loops = CFBagCreateCopy(kCFAllocatorSystemDefault, rls->_runLoops); } __CFRunLoopSourceUnlock(rls);if (loops) {
CFBagApplyFunction(loops, __CFRunLoopSourceWakeUpLoop, NULL); CFRelease(loops); }}static void __CFRunLoopSourceWakeUpLoop(const void *value, void *context) {
CFRunLoopWakeUp((CFRunLoopRef)value);
}
Copy the code
As stated in the CFRunLoopWakeUp function, the wakeup operation is placed in a queue of length 1. Therefore, the wakeup operation is not repeated. This is done by calling __CFSendTrivialMachMessage(rL ->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0) to send a message to the _wakeUpPort of the runloop. Inside the __CFSendTrivialMachMessage function, mach_msg() is actually called.
void CFRunLoopWakeUp(CFRunLoopRef rl) {
CHECK_FOR_FORK();
// This lock is crucial to ignorable wakeups, do not remove it.
__CFRunLoopLock(rl);
if (__CFRunLoopIsIgnoringWakeUps(rl)) {
__CFRunLoopUnlock(rl);
return;
}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
kern_return_t ret;
/* We unconditionally try to send the message, since we don't want * to lose a wakeup, but the send may fail if there is already a * wakeup pending, since the queue length is 1. */
ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
if(ret ! = MACH_MSG_SUCCESS && ret ! = MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret);
#elif DEPLOYMENT_TARGET_WINDOWS
SetEvent(rl->_wakeUpPort);
#endif
__CFRunLoopUnlock(rl);
}
static uint32_t __CFSendTrivialMachMessage(mach_port_t port, uint32_t msg_id, CFOptionFlags options, uint32_t timeout) {
kern_return_t result;
mach_msg_header_t header;
header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
header.msgh_size = sizeof(mach_msg_header_t);
header.msgh_remote_port = port;
header.msgh_local_port = MACH_PORT_NULL;
header.msgh_id = msg_id;
result = mach_msg(&header, MACH_SEND_MSG|options, header.msgh_size, 0, MACH_PORT_NULL, timeout, MACH_PORT_NULL);
if (result == MACH_SEND_TIMED_OUT) mach_msg_destroy(&header);
return result;
}
Copy the code
Source0 is also used to wake up the runloop using Mach MSG, the Mach port being the _wakeUpPort of the Runloop object. Set Source0 to Singal and call CFRunLoopWakeUp with mach_msg(). This is consistent with the way runloop is finally woken up in __CFRunLoopServiceMachPort, and it does lead to the same thing. This is obviously reasonable, because the operation of waking up runloop is actually a state switch of the Mach kernel, i.e. user => kernel state, where communication is only possible through Mach MSG.
How is Source0 added to runloop
The CFRunLoopAddSource function adds the Source of the runloop to the specified runloopMode.
Where is the perform function pointer specified? Info is the parameter.
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if(! __CFIsValid(rls))return;
Boolean doVer0Callout = false;
__CFRunLoopLock(rl);
if (modeName == kCFRunLoopCommonModes) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rls);
if (NULL! =set) {
CFTypeRef context[2] = {rl, rls};
/* add new item to all common-modes */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set); }}else {
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL! = rlm &&NULL == rlm->_sources0) {
rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0.NULL.NULL);
}
if (NULL! = rlm && ! CFSetContainsValue(rlm->_sources0, rls) && ! CFSetContainsValue(rlm->_sources1, rls)) {if (0 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources0, rls);
} else if (1 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources1, rls);
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if(CFPORT_NULL ! = src_port) { CFDictionarySetValue(rlm->_portToV1SourceMap, (const void(*)uintptr_t)src_port, rls);
__CFPortSetInsert(src_port, rlm->_portSet);
}
}
__CFRunLoopSourceLock(rls);
if (NULL == rls->_runLoops) {
rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
}
CFBagAddValue(rls->_runLoops, rl);
__CFRunLoopSourceUnlock(rls);
if (0 == rls->_context.version0.version) {
if (NULL! = rls->_context.version0.schedule) { doVer0Callout =true; }}}if (NULL! = rlm) { __CFRunLoopModeUnlock(rlm); } } __CFRunLoopUnlock(rl);if (doVer0Callout) {
// although it looses some protection for the source, we have no choice but
// to do this after unlocking the run loop and mode locks, to avoid deadlocks
// where the source wants to take a lock which is already held in another
// thread which is itself waiting for a run loop/mode lock
rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */}}Copy the code
The CFRunLoopAddSource function adds this source to the Mode items of runloopMode. For common mode, call CFSetAddValue(rl->_commonModeItems, RLS); ; If it is source0, CFSetAddValue(RLM ->_sources0, RLS) is called; ; If it is source1, call ***CFSetAddValue(RLM ->_sources1, RLS); ***, as well as port Settings.
For source0, there is a Schedule callback that is triggered when source is added to runloop, Use the RLS – > _context. Version0. The schedule (RLS – > _context. Version0. Info, rl, modeName); / CALLOUT /.
Process the Sources1 task __CFRunLoopDoSource1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; Is called after runloop has been woken up.
// msg, size and reply are unused on Windows
static Boolean __CFRunLoopDoSource1() __attribute__((noinline));
static Boolean __CFRunLoopDoSource1(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopSourceRef rls
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
, mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply
#endif
) { /* DOES CALLOUT */
CHECK_FOR_FORK();
Boolean sourceHandled = false;
/* Fire a version 1 source */
CFRetain(rls);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
__CFRunLoopSourceLock(rls);
if (__CFIsValid(rls)) {
__CFRunLoopSourceUnsetSignaled(rls);
__CFRunLoopSourceUnlock(rls);
__CFRunLoopDebugInfoForRunLoopSource(rls);
Perform the perform callback set in source1.
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(rls->_context.version1.perform,
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
msg, size, reply,
#endif
rls->_context.version1.info);
CHECK_FOR_FORK();
sourceHandled = true;
} else {
if (_LogCFRunLoop) { CFLog(kCFLogLevelDebug, CFSTR("%p (%s) __CFRunLoopDoSource1 rls %p is invalid"), CFRunLoopGetCurrent(), *_CFGetProgname(), rls); }
__CFRunLoopSourceUnlock(rls);
}
CFRelease(rls);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
return sourceHandled;
}
Copy the code
The code to execute the task is:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(rls->_context.version1.perform,
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
msg, size, reply,
#endif
rls->_context.version1.info);
Copy the code
The __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ function prototype is as follows:
// Runloop is woken up to handle the event, handling the source1 event.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
#else
void (*perform)(void *),
#endif
void *info) {
if (perform) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
*reply = perform(msg, size, kCFAllocatorSystemDefault, info);
#else
perform(info);
#endif
}
asm __volatile__(""); // thwart tail-call optimization
}
Copy the code
The function has many arguments, MSG is a Mach MSG object and reply is the reply message after the Mach MSG message communication is executed. Perform is a callback pointer that contains four parameters, and info is also used to pass parameters to the callback function.
How does Source1 wake up Runloop
Call __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(MSG_buffer), &livePort, poll? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy, rl, rlm); Internally, the mach_msg() function is called to complete the Mach kernel’s state change, even if runloop is asleep.
How is Source1 added to runloop
Source1 adds the same code to runloop as source0, except for the port setting.
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if(CFPORT_NULL ! = src_port) { CFDictionarySetValue(rlm->_portToV1SourceMap, (const void(*)uintptr_t)src_port, rls);
__CFPortSetInsert(src_port, rlm->_portSet);
}
Copy the code
Handles the Observer task __CFRunLoopDoObservers
The __CFRunLoopDoObservers function notifies the Observer of six states of runloop.
- __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); About to enter runloop
- __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); About to process the timer
- __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); Source0 is about to be processed
- __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); About to go to sleep
- __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); To awaken from hibernation
- __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); Exit runloop
If Source1 wakes up runloop directly, the goTO statement is used to jump to handLE_msg execution without notification of AfterWaiting status! So AfterWaiting notifications are only for non-source1 wake up scenarios, where the runloop is woken up manually via UITouch events, timers, etc.
/* rl is locked, rlm is locked on entrance and exit */
static void __CFRunLoopDoObservers() __attribute__((noinline));
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity) { /* DOES CALLOUT */CHECK_FOR_FORK(); Get the maximum number of observers for runloop mode1024A? CFIndex cnt = rlm->_observers ? CFArrayGetCount(rlm->_observers) :0;
if (cnt < 1) return;
/* Fire the observers */
STACK_BUFFER_DECL(CFRunLoopObserverRef, buffer, (cnt <= 1024)? cnt :1);
CFRunLoopObserverRef *collectedObservers = (cnt <= 1024)? buffer : (CFRunLoopObserverRef *)malloc(cnt * sizeof(CFRunLoopObserverRef));
// Get the CFRunLoopObserverRef object by iterating through all observers of runloop mode. Collect valid observers.
CFIndex obs_cnt = 0;
for (CFIndex idx = 0; idx < cnt; idx++) {
CFRunLoopObserverRef rlo = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
if (0! = (rlo->_activities & activity) && __CFIsValid(rlo) && ! __CFRunLoopObserverIsFiring(rlo)) { collectedObservers[obs_cnt++] = (CFRunLoopObserverRef)CFRetain(rlo); } } __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl);// Perform a callback to all observers
for (CFIndex idx = 0; idx < obs_cnt; idx++) {
CFRunLoopObserverRef rlo = collectedObservers[idx];
__CFRunLoopObserverLock(rlo);
if(__CFIsValid(rlo)) { Boolean doInvalidate = ! __CFRunLoopObserverRepeats(rlo); __CFRunLoopObserverSetFiring(rlo); __CFRunLoopObserverUnlock(rlo);// Execute the observer callback rlo->_callout to wake up the runloop and set it to the __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ state.
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info);
if (doInvalidate) {
CFRunLoopObserverInvalidate(rlo);
}
__CFRunLoopObserverUnsetFiring(rlo);
} else {
__CFRunLoopObserverUnlock(rlo);
}
CFRelease(rlo);
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
if(collectedObservers ! = buffer)free(collectedObservers);
}
Copy the code
For each valid Observer in this function, CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION(rLO ->_callout, rLO, activity, rLO ->_context.info); . This function takes four parameters: func, the observer callback, the Observer itself, the runloop state, the activity object, and the info object, which can be used to pass parameters.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (func) {
func(observer, activity, info);
}
asm __volatile__(""); // thwart tail-call optimization
}
Copy the code
How is an Observer added to runloop
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef rlo, CFStringRef modeName) {
CHECK_FOR_FORK();
CFRunLoopModeRef rlm;
if (__CFRunLoopIsDeallocating(rl)) return;
if(! __CFIsValid(rlo) || (NULL! = rlo->_runLoop && rlo->_runLoop ! = rl))return;
__CFRunLoopLock(rl);
if (modeName == kCFRunLoopCommonModes) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlo);
if (NULL! =set) {
CFTypeRef context[2] = {rl, rlo};
/* add new item to all common-modes */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set); }}else {
rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL! = rlm &&NULL == rlm->_observers) {
rlm->_observers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
}
if (NULL! = rlm && ! CFArrayContainsValue(rlm->_observers, CFRangeMake(0, CFArrayGetCount(rlm->_observers)), rlo)) {
Boolean inserted = false;
for (CFIndex idx = CFArrayGetCount(rlm->_observers); idx--; ) {
CFRunLoopObserverRef obs = (CFRunLoopObserverRef)CFArrayGetValueAtIndex(rlm->_observers, idx);
if (obs->_order <= rlo->_order) {
CFArrayInsertValueAtIndex(rlm->_observers, idx + 1, rlo);
inserted = true;
break; }}if(! inserted) { CFArrayInsertValueAtIndex(rlm->_observers,0, rlo);
}
rlm->_observerMask |= rlo->_activities;
__CFRunLoopObserverSchedule(rlo, rl, rlm);
}
if (NULL! = rlm) { __CFRunLoopModeUnlock(rlm); } } __CFRunLoopUnlock(rl); }Copy the code
Adding an Observer to a runloop is similar to adding a timer or source to a runloop, with commonModes considered (CFSetAddValue(rl->_commonModeItems, rlo);). . Not under the commonMode, direct call CFArrayInsertValueAtIndex (RLM – > _observers, independence idx + 1, rlo); Add them to _observers of runloopMode.
Obsever and Timer can only be added to one runloop, while sources can be added to multiple runloops. CFRunLoopTimerRef and CFRunLoopObserverRef have only one CFRunLoopRef _runLoop; CFRunLoopSourceRef has a CFMutableBagRef _runLoops; The container.
Something about the main thread
The __CFRunLoopRun function has a main queue:
mach_port_name_t dispatchPort = MACH_PORT_NULL;
// check whether the queue is main_queue.
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL== previousMode) || (! HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY &&0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
// dispatchPort is the Mach port required by the main thread task dispatch
// If it is a runloop of the main thread and is CommonModes
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) {
dispatchPort = _dispatch_get_main_queue_port_4CF();
}
Copy the code
Otherwise, dispatchPort is MACH_PORT_NULL. Only in the main thread runloop does dispatchPort have a value, meaning that dispatchPort is used exclusively in the main thread.
DispatchPort, modeQueuePort
The dispatchPort is the host thread’s port for sending and receiving Mach MSG. The other part of the port should be libDispatch. Dispatch_async (dispatch_get_main_queue(), ^{XXX}).
Dispatch_async (dispatch_get_main_queue(), ^{XXX})) Is dispatchPort? When dispatch_async(dispatch_get_main_queue(), block) is called, the main queue will put the block in the corresponding thread (which happens to be the main thread), and the main thread’s RunLoop will wake up. Get the block from the message and call CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() to execute the block:
The _dispatch_get_main_queue_port_4CF() function is as follows:
dispatch_runloop_handle_t
_dispatch_get_main_queue_port_4CF(void)
{
return _dispatch_get_main_queue_handle_4CF();
}
dispatch_runloop_handle_t
_dispatch_get_main_queue_handle_4CF(void)
{
dispatch_queue_main_t dq = &_dispatch_main_q;
dispatch_once_f(&_dispatch_main_q_handle_pred, dq,
_dispatch_runloop_queue_handle_init);
return _dispatch_runloop_queue_get_handle(dq->_as_dl);
}
DISPATCH_ALWAYS_INLINE
static inline dispatch_runloop_handle_t
_dispatch_runloop_queue_get_handle(dispatch_lane_t dq)
{
#if TARGET_OS_MAC
return ((dispatch_runloop_handle_t) (uintptr_t)dq->do_ctxt);
#elif defined(__linux__)
// decode: 0 is a valid fd, so offset by 1 to distinguish from NULL
return ((dispatch_runloop_handle_t) (uintptr_t)dq->do_ctxt) - 1;
#else
#error "runloop support not implemented on this platform"
#endif
}
Copy the code
On the MAC platform, dispatch_RUNloop_HANDLE_t is actually mach_port_t.
#if TARGET_OS_MAC
typedef mach_port_t dispatch_runloop_handle_t;
#elif defined(__linux__)
typedef int dispatch_runloop_handle_t;
#else
#error "runloop support not implemented on this platform"
#endif
Copy the code
In addition, there is a modeQueuePort only available on MACOSX, which is the mach_port_name_t object of the queue where the current runloop resides.
#if TARGET_OS_MAC
dispatch_runloop_handle_t
_dispatch_runloop_root_queue_get_port_4CF(dispatch_queue_t dq)
{
if(unlikely(dx_type(dq) ! = DISPATCH_QUEUE_RUNLOOP_TYPE)) { DISPATCH_CLIENT_CRASH(dx_type(dq),"Not a runloop queue");
}
return _dispatch_runloop_queue_get_handle(upcast(dq)._dl);
}
#endif
Copy the code
__CFRUNLOOP_IS_CALLING and __CFRUNLOOP_IS_SERVICING, which are called callout functions in runloop. The actual code is pretty simple, but it makes sense that the function names get so much attention because they’re where the real work is.
The main thread – dependent callout function
After the runloop is awakened, if the livePort of the awakened runloop is the dispatchPort of the main thread, the callout function CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE is executed.
if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6.NULL);
/// dispatchPort is the main thread. Yes!
/// Execute if there are blocks to the main queue.
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0.NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
}
Copy the code
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE is based on Mach MSG and its function prototype is:
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() __attribute__((noinline));
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg) {
_dispatch_main_queue_callback_4CF(msg);
asm __volatile__(""); // thwart tail-call optimization
}
Copy the code
Mach MSG is required to execute the GCD event of the main thread. The MSG in this case is the MSG used when runloop sleep is awakened.
For the _dispatch_main_queue_callback_4CF() function, see the libdispatch source file in queue.c. The internal implementation of this function is obviously a topic for another day. All you know is that it schedules and executes tasks in the Main Queue.
void
_dispatch_main_queue_callback_4CF(
void *ignored DISPATCH_UNUSED)
{
// the main queue cannot be suspended and no-one looks at this bit
// so abuse it to avoid dirtying more memory
if (_dispatch_main_q.dq_side_suspend_cnt) {
return;
}
_dispatch_main_q.dq_side_suspend_cnt = true;
_dispatch_main_queue_drain(&_dispatch_main_q);
_dispatch_main_q.dq_side_suspend_cnt = false;
}
Copy the code
However, the Mach MSG is passed to the GCD without using…
Mach MSG before runloop sleep
We notice that the __CFRunLoopServiceMachPort function is called once before entering the inner loop do-while(1), when runloop is about to sleep.
if(MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) {#if TARGET_OS_MAC
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL, rl, rlm)) {
gotohandle_msg; }}Copy the code
This step calls __CFRunLoopServiceMachPort to ensure that blocks from GCD to main Queue are always executed as quickly as possible. However, it will not be called on the first outer loop run by runloop, or the last runloop wake up that was not due to the GCD main thread.
Something about GCD
// Usually use GCD, {NSLog(@"dispatch_get_global_queue"); ^{"dispatch_get_global_queue"); });Copy the code
The child thread uses GCD and breakpoints do not appear in runloop callout. Since this is handled by GCD itself, it has nothing to do with Runloop.
The main thread is different:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_get_main_queue");
});
Copy the code
Dispatch_async (dispatch_get_global_queue(0, 0), ^{dispatch_async(dispatch_get_main_queue()), ^{ Runloop callout will appear, why? // With GCD asynchrony, we need to rely on RunLoop to return to the main thread after a child thread has finished processing something. The __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ function is called internally. NSLog(@"dispatch_get_main_queue"); }); });Copy the code
The main thread is executed by runloop.
How does GCD wake up Runloop
Other things you might need to pay attention to
In addition to the main flow of runloop above, there are a few other things you might want to notice.
Runloop-related state Settings
Some of the state of the runloop itself is stored by _per_run_data, such as RL ->_perRunData-> Stopped and RL ->_perRunData->ignoreWakeUps, as described in the previous data structures section.
In addition, runloop’s _cfinfo[CF_INFO_BITS] is used to query and set some states of runloop, such as isSleeping and isDeallocating.
CF_INLINE Boolean __CFRunLoopIsSleeping(CFRunLoopRef rl) {
return (Boolean)__CFBitfieldGetValue(((const CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 1.1);
}
CF_INLINE void __CFRunLoopSetSleeping(CFRunLoopRef rl) {
__CFBitfieldSetValue(((CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 1.1.1);
}
CF_INLINE void __CFRunLoopUnsetSleeping(CFRunLoopRef rl) {
__CFBitfieldSetValue(((CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 1.1.0);
}
CF_INLINE Boolean __CFRunLoopIsDeallocating(CFRunLoopRef rl) {
return (Boolean)__CFBitfieldGetValue(((const CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 2.2);
}
CF_INLINE void __CFRunLoopSetDeallocating(CFRunLoopRef rl) {
__CFBitfieldSetValue(((CFRuntimeBase *)rl)->_cfinfo[CF_INFO_BITS], 2.2.1);
}
Copy the code
Runloop’s Source0 contains only a callback, and isSignaled is used to indicate whether Source0 can be processed.
/* Bit 1 of the base reserved bits is used for signalled state */
CF_INLINE Boolean __CFRunLoopSourceIsSignaled(CFRunLoopSourceRef rls) {
return (Boolean)__CFBitfieldGetValue(rls->_bits, 1.1);
}
CF_INLINE void __CFRunLoopSourceSetSignaled(CFRunLoopSourceRef rls) {
__CFBitfieldSetValue(rls->_bits, 1.1.1);
}
CF_INLINE void __CFRunLoopSourceUnsetSignaled(CFRunLoopSourceRef rls) {
__CFBitfieldSetValue(rls->_bits, 1.1.0);
}
Copy the code
In addition, there are isFiring of observer and isFiring of timer, etc., which are not discussed in detail here.
Runloop How to switch mode
There are many lock and unlock operations in the source code.
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
Copy the code
In __CFRunLoopRun, there are three unlock mode operations:
- Runloop is about to hibernate
- When runloop is woken up, it processes the task of GCD Main
- After runloop exits
The Runloop may switch the runloopMode at this time.
Manual wake up of runloop
By manual wakeup, we mean calling the CFRunLoopWakeUp function.
The CFRunLoopWakeUp function was explained earlier. The wakeup operation is placed in a queue of length 1. Therefore, the wakeup operation is not repeated. This is done by calling __CFSendTrivialMachMessage(rL ->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0) to send a message to the _wakeUpPort of the runloop. Inside the __CFSendTrivialMachMessage function, mach_msg() is actually called.
The conclusion is that runloop wakeup is essentially mach_msg, which means that switching state of the Mach kernel is really only possible with Mach MSG.
Runloop timeout
The runloop’s running time is passed in externally, and the correct running time is ensured internally by a GCD timer or mk timer. The runloop timeout callback is __CFRunLoopTimeout. Runloop cancels the __CFRunLoopTimeoutCancel callback function.
#if __HAS_DISPATCH__
The __CFRunLoopTimeout function is called when the runloop timer expires
dispatch_source_t timeout_timer = NULL;
#endif
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;
} else if (seconds <= TIMER_INTERVAL_LIMIT) {
#if __HAS_DISPATCH__
dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0.0.queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
#endif
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
#if __HAS_DISPATCH__
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);
#endif
} else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}
Copy the code
__timeout_context will hold a runloop object and possibly a GCD timer object dispatch_source_t ds.
struct __timeout_context {
#if __HAS_DISPATCH__
dispatch_source_t ds;
#endif
CFRunLoopRef rl;
_Atomic(uint64_t) termTSR;
};
Copy the code
How are runloop objects destroyed
Earlier, we mentioned that when the Thread object is destroyed, the TSD runloop object is also destroyed, using the __CFFinalizeRunLoop() function.
// Called for each thread as it exits
CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data) {
CFRunLoopRef rl = NULL;
if (data <= 1) {
__CFLock(&loopsLock);
if (__CFRunLoops) {
rl = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(pthread_self()));
if (rl) CFRetain(rl);
// Remove the corresponding runloop object from __CFRunLoops
CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self()));
}
__CFUnlock(&loopsLock);
} else {
// There is a recursive call
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void(*) (void *))__CFFinalizeRunLoop);
}
if(rl && CFRunLoopGetMain() ! = rl) {// protect against cooperative threads
if (NULL! = rl->_counterpart) { CFRelease(rl->_counterpart); rl->_counterpart =NULL;
}
// purge all sources before deallocation
CFArrayRef array = CFRunLoopCopyAllModes(rl);
for (CFIndex idx = CFArrayGetCount(array); idx--;) {
CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx);
// Remove sources from runloopMode
__CFRunLoopRemoveAllSources(rl, modeName);
}
__CFRunLoopRemoveAllSources(rl, kCFRunLoopCommonModes);
CFRelease(array);
}
// When a thread is destroyed, its runloop is also destroyed.
if (rl) CFRelease(rl);
}
Copy the code
Before destroying a Runloop object, remove it from the __CFRunLoops global dictionary, traverse all its modes, remove all source objects in each mode, and finally destroy the Runloop object.
What is the data argument to the __CFFinalizeRunLoop() function? This is simply the reference count of the runloop object, corresponding to the slot __CFTSDKeyRunLoopCntr. If data is greater than 1, only the value of __CFTSDKeyRunLoopCntr is reduced by 1. When data <= 1, the runloop destruction code is actually executed.
There are three calls to the __CFTSDKeyRunLoopCntr slot:
- _CFRunLoopGet0() : ***_CFSetTSD(__CFTSDKeyRunLoopCntr, (void))(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void ()(void ))__CFFinalizeRunLoop);**
- __CFFinalizeRunLoop() : ***_CFSetTSD(__CFTSDKeyRunLoopCntr, (void))(data – 1), (void ()(void ))__CFFinalizeRunLoop);**
- _CFRunLoopSetCurrent() : **_CFSetTSD(__CFTSDKeyRunLoopCntr, 0, (void ()(void ))__CFFinalizeRunLoop);**
When the runloopMode object is destroyed, its mode item and other objects are released accordingly:
static void __CFRunLoopModeDeallocate(CFTypeRef cf) {
CFRunLoopModeRef rlm = (CFRunLoopModeRef)cf;
if (NULL! = rlm->_sources0) CFRelease(rlm->_sources0);if (NULL! = rlm->_sources1) CFRelease(rlm->_sources1);if (NULL! = rlm->_observers) CFRelease(rlm->_observers);if (NULL! = rlm->_timers) CFRelease(rlm->_timers);if (NULL! = rlm->_portToV1SourceMap) CFRelease(rlm->_portToV1SourceMap); CFRelease(rlm->_name); __CFPortSetFree(rlm->_portSet);#if USE_DISPATCH_SOURCE_FOR_TIMERS
if (rlm->_timerSource) {
dispatch_source_cancel(rlm->_timerSource);
dispatch_release(rlm->_timerSource);
}
if (rlm->_queue) {
dispatch_release(rlm->_queue);
}
#endif
#if USE_MK_TIMER_TOO
if(MACH_PORT_NULL ! = rlm->_timerPort) mk_timer_destroy(rlm->_timerPort);#endif
pthread_mutex_destroy(&rlm->_lock);
memset((char *)cf + sizeof(CFRuntimeBase), 0x7C.sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase));
}
Copy the code
A special variable poll
This variable poll is given after __CFRunLoopDoSources0 is executed at the start of a run of each runloop.
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
Copy the code
There is something strange about this poll judgment and I don’t know why it is done.
Each time the loop processes the source0 task, the poll value is true and the immediate effect is that doobservers-beforewaiting and doobservers-afterwaiting are not enabled. That is, runloop will go straight to sleep and will not tell BeforeWaiting and AfterWaiting activities. So you see, there may be cases where the runloop passes through several loops, but the Observer you registered will not receive a callback.
The resources
- Event loop
- Run Loops
- NSRunLoop
- CFRunLoop
- CFRunLoopPerformBlock
- CFRunLoopPerformBlock vs dispatch_async
- RunLoop.subproj
- Kernel Programming Guide: Mach Overview
- mach_msg
- CFPlatform.c
- AsyncDisplayKit has been renamed to Texture
- Understand RunLoop in depth
- Decryption Runloop
- Get to the bottom of iOS – Understand RunLoop in depth
- Optimizing the UITableViewCell height calculation stuff
- Revisit the RunLoop principle
- IOS RunLoop,