RunLoop principle

When the APP starts, the system automatically creates a thread to execute tasks, which is called the master thread or UI thread. When the main thread is created, the system also creates and starts a mechanism for the main thread (essentially an object that is related to the application’s life cycle) called a RunLoop, which is called a RunLoop. This loop is more advanced. While a normal while loop causes the CPU to enter a busy wait, a RunLoop is an “idle” wait. When no event occurs, the RunLoop goes to sleep. When an event occurs, the RunLoop goes to the corresponding Handler to handle the event. RunLoop keeps threads busy when they need to do something, and puts them to sleep when they don’t.

Function loop(){initialize() do {var message = get_next_message() process_message(message)// process message} while (the message! = quit); }Copy the code

This Loop model is called the Event Loop model. The key points to implement this model are how to manage events/messages and how to make threads sleep when they are not processing messages to avoid resource usage and wake up as soon as a message arrives. The Event Loop model is implemented in many systems. A RunLoop is actually an object that manages the events and messages it needs to process and provides an entry function to execute the logic of the Event loop above. Once the thread executes the function, it will remain in the “receive message -> wait -> process” loop inside the function until the loop ends (such as the incoming quit message) and the function returns.

RunLoop nature

Runloop has two key judgment points, one is to determine whether the Runloop is waiting by MSG, and the other is to determine whether the Runloop is loop by determining the exit condition.

The loop mechanism of RunLoop relies entirely on the kernel, specifically Mach in Darwin, the core component of Apple’s operating system. Mach is at the heart of Darwin, the core of the kernel, if you will. In Mach, communication between processes and threads is done in the form of messages (Mach MSG), Messages are passed between (or through) two Mach ports (which is why Source1 is called a Port-based Source, Because it relies on the Mach MSG to send a message to the specified Mach port to wake up the Run loop.

Runloop loops involve a switch from user to kernel mode and a switch from kernel to user mode. It maintains an event loop that can be used to continuously process messages or events and manage them. When no message is processed, the system calls from the user mode to the kernel mode, and transfers the control to the kernel mode to avoid resource occupation and enter the hibernation state. When a message needs to be processed, a switch occurs from kernel to user mode, and the current user thread is woken up. So state switching is the key to Runloop.

Runloop sends the message through the mach_msg() function, and if there is no port message, the kernel puts the thread in a wait state mach_MSg_trap (). If there is a message, the event is processed by determining the message type and through the callback of the modeItem.

RunLoop with thread

Runloops correspond to threads one by one — a runloop corresponds to a core thread, which is core because runloops can be nested, but there can only be one core, and their relationship is stored in a global dictionary. Runloop is used to manage threads — when a thread’s Runloop is enabled, the thread goes to sleep after executing a task and is woken up to perform the task. The runloop is created on the first fetch and destroyed at the end of the thread.

For the main thread, the runloop is created by default as soon as the program starts. For child threads, runloops are lazily loaded — they’re only created when we’re using them, so be careful when using timers on child threads: make sure the child’s runloop is created or the timer doesn’t call back.

  • Runloops correspond to threads one by one in a key-value manner stored in a global dictionary.
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);Copy the code
  • The RunLoop for the main thread is created when the global dictionary is initialized.
/ / create a temporary dictionary dict CFMutableDictionaryRef dict = CFDictionaryCreateMutable (kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); // Save the main thread RunLoop to the dict, key is the thread, Value is RunLoop CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);Copy the code
  • The RunLoop of the child thread is created when it is first fetched and never created if it is not fetched.
  • The RunLoop is destroyed when the thread is destroyed.
// Register a callback when the thread is destroyed, Destroy the corresponding RunLoop _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);Copy the code

The Runloop for the main thread will start when the application starts. The Runloop for the other threads will not start by default and will need to be started manually.

RunLoop and AutoreleasePool

In ARC projects, Apple adds autoreleasePool where appropriate to retain or release objects. So when is autoreleasePool created and when is it released? Automatic release pool creation and release timing:

  • First creation: when runloop is started. Corresponding to the kCFRunLoopEntry method
  • Last destruction: When runloop exits. Corresponding to the kCFRunLoopExit method
  • Creation and destruction at other times: When runloop is about to go to sleep, the old release pool is destroyed and a new release pool is created. Corresponds to the kCFRunLoopBeforeWaiting method

(Please read the AutoreleasePool principle if you find it difficult to understand.)

Two Observers are registered after the iOS application is launched to manage and maintain the AutoreleasePool. Might as well in the application has just started to print currentRunLoop can see the system default registered a lot of the Observer, there are two of the Observer the callout is * *, * * _ wrapRunLoopWithAutoreleasePoolHandler These are the two listeners associated with auto-release pools.

  • The first Observer listens for the RunLoop to enter by calling objc_autoreleasePoolPush() to add a sentinel object flag to the current AutoreleasePoolPage to create the automatic release pool. The order of this Observer is -2147483647, the highest priority, ensuring that it occurs before any callback operation.
  • The second Observer listens for the RunLoop when it is asleep and when it is about to exit. When going to sleep, objc_autoreleasePoolPop() and objc_autoreleasePoolPush() are called to clean up from the newly added object until the sentry object is encountered. When exiting RunLoop, objc_autoreleasePoolPop() is called torelease objects in the pool automatically. The order of this Observer is 2147483647, which has the lowest priority and is guaranteed to occur after all callbacks.

Other operations on the main thread are usually kept within the AutoreleasePool (in the main function) to minimize memory maintenance operations (although you can create AutoreleasePool yourself if you want to explicitly free (e.g. loop) otherwise you don’t need to). Actually after application startup system also registered the other Observer (such as entering hibernation and execute register callback _UIGestureRecognizerUpdateObserver used to deal with the gesture, the callback for _ZN2CA11Transaction17observer_call The Observer of backEP19__CFRunLoopObservermPv is used to draw updates to the interface in real time) and multiple source1s (e.g. Source1 whose context is CFMachPort is used to receive hardware event responses and distribute them to the application all the way to the UIEvent).

RunLoop role

  • Keep the program immortal (via the main function)
  • Handle all kinds of events in the APP: touch interaction, timer, performSelect, port interaction, etc
  • Save CPU resources –> Wake up when there is work, sleep when there is finished/no work

RunLoop structure – source code interpretation

1. RunLoop

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* locked for accessing mode list */
    __CFPort _wakeUpPort;   // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
    Boolean _unused;
    volatile _per_run_data *_perRunData; // reset for runs of the run loop
    pthread_t _pthread;             //RunLoop对应的线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;    //存储的是字符串,记录所有标记为common的mode
    CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;   //当前运行的mode
    CFMutableSetRef _modes;          //存储的是CFRunLoopModeRef
    struct _block_item *_blocks_head;//doblocks的时候用到
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

Copy the code

A RunLoop object consists mainly of one thread, several modes, several commonModes, and one currently running Mode. Runloop can only run in one Mode at a time, handling the Source, Timer, and Observer in that Mode.

2. CFRunLoopMode

struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; /* must have the run loop locked before locking this */ CFStringRef _name; //mode name Boolean _stopped; // Whether mode is terminated char _padding[3]; CFMutableSetRef _sources0; //sources0 CFMutableSetRef _sources1; //sources1 CFMutableArrayRef _observers; // Notify CFMutableArrayRef _timers; // CFMutableDictionaryRef _portToV1SourceMap; // the dictionary key is mach_port_t and the value is CFRunLoopSourceRef __CFPortSet _portSet; _wakeUpPort and _timerPort are stored in the array 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 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 */ };Copy the code

A CFRunLoopMode object has a name, source0s, source1s, timers, observers, and ports. Visible events are managed by Mode, while RunLoop manages Mode.

There are five modes

  • NSDefaultRunLoopMode

  • NSConnectionReplyMode

  • NSModalPanelRunLoopMode

  • NSEventTrackingRunLoopMode

  • NSRunLoopCommonModes

The only publicly exposed modes in iOS are NSDefaultRunLoopMode and NSRunLoopCommonModes. NSRunLoopCommonModes is actually a collection of the Mode, including the default NSDefaultRunLoopMode and NSEventTrackingRunLoopMode (note: This does not mean that Runloop will run in kCFRunLoopCommonMode, but rather that it is registered with NSDefaultRunLoopMode and UITrackingRunLoopMode respectively.

3. ModeItem

The Source/Timer/Observer in Mode is collectively referred to as ModeItem. An item can be added to multiple modes at the same time. However, it does not work if an item is repeatedly added to the same mode. If there is no item in a mode, the RunLoop exits without entering the loop. When adding an item, if modeName is passed kCFRunLoopCommonModes or NSRunLoopCommonModes, the source will be saved to the _commonModeItems in the RunLoop. Will be added to all modes in commonModes. In fact, whenever the contents of the RunLoop change, the RunLoop automatically synchronizes the Source/Observer/Timer in _commonModeItems to all modes with the Common flag.

4. CFRunLoopSource

CFRunLoopSource has two versions: source0 and source1.

  • Source0 is non-port-based. Contains only one callback (function pointer), which does not actively fire events. To use this, you need to call CFRunLoopSourceSignal(source), mark the source as pending, and then manually call CFRunLoopWakeUp(runloop) to wake up the Runloop to handle the event.
  • Source1 contains a Mach port in addition to the callback pointer. Source1 can listen to system ports and communicate with other threads through the kernel, receiving and distributing system events, and it can actively wake up Runloops (managed by the operating system kernel, such as CFMessagePort messages).

5. CFRunLoopObserver

The CFRunLoopObserver is an observer that can observe various RunLoop states and throw callbacks.

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;      /* immutable */
    CFIndex _order;         /* immutable */
    CFRunLoopObserverCallBack _callout; /* immutable */
    CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};
Copy the code

The CFRunLoopObserver can observe the following six states:

/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // About to enter run loop kCFRunLoopBeforeTimers = (1UL << 1), // About to process timer kCFRunLoopBeforeSources = (1UL << 2),// About to process source kCFRunLoopBeforeWaiting = (1UL << 5),// About to enter sleep KCFRunLoopAfterWaiting = (1UL << 6), kCFRunLoopExit = (1UL << 7),// Run loop has exited kCFRunLoopAllActivities =  0x0FFFFFFFU };Copy the code

We can use the Runloop Observer to monitor the status of the Runloop itself. The Runloop Observer can monitor Runloop events as shown in the following figure.

6. CFRunLoopTimer

CFRunLoopTimer is a timer that can throw a callback at a set point in time. Its structure is as follows:

struct __CFRunLoopTimer { CFRuntimeBase _base; uint16_t _bits; Pthread_mutex_t _lock; CFRunLoopRef _runLoop; // Add runloop CFMutableSetRef _rlModes for this timer; CFAbsoluteTime _nextFireDate; CFAbsoluteTime _nextFireDate; CFTimeInterval _interval; /* immutable */ CFTimeInterval _tolerance; // uint64_t _fireTSR; /* TSR units */ CFIndex _order; /* immutable */ CFRunLoopTimerCallBack _callout; /* immutable */ CFRunLoopTimerContext _context; /* immutable, except invalidation */ };Copy the code

CFRunLoopTimer has the following features

  • CFRunLoopTimer is a timer that can throw a callback at a set point in time
  • CFRunLoopTimer and NSTimer are toll-free bridged and can be exchanged with each other

RunLoop implementation – source code interpretation

The implementation of RunLoop is described in the following three aspects.

  • Get RunLoop
  • Add Mode
  • Add a Run Loop Source

1. Obtain RunLoop

Apple’s open API does not allow us to create RunLoop objects directly. We can only get runloops through the following functions:

  • CFRunLoopRef CFRunLoopGetCurrent(void)
  • CFRunLoopRef CFRunLoopGetMain(void)
  • +(NSRunLoop *)currentRunLoop
  • +(NSRunLoop *)mainRunLoop

The first two are apis from Core Foundation, and the last two are apis from Foundation.

1) CFRunLoopGetCurrent
RunLoop CFRunLoopRef CFRunLoopGetCurrent(void) {CHECK_FOR_FORK(); // Run CFRunLoopRef CFRunLoopGetCurrent(void) {CHECK_FOR_FORK(); CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); if (rl) return rl; Return _CFRunLoopGet0(pthread_self()); }Copy the code

_CFRunLoopGet0() is called inside the CFRunLoopGetCurrent function, passing in the current thread pthread_self(). As you can see, the CFRunLoopGetCurrent function must be called inside the thread to get the current thread’s RunLoop. This means that the RunLoop of the child thread must be retrieved within the child thread.

(2) CFRunLoopGetMain
RunLoop CFRunLoopRef CFRunLoopGetMain(void) {CHECK_FOR_FORK(); // run 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; }Copy the code

_CFRunLoopGet0() is also called inside the CFRunLoopGetMain function, passing in the main thread pthread_main_thread_NP (). As you can see, CFRunLoopGetMain() gets the main thread RunLoop whether it is called in the main thread or in a child thread.

(3) CFRunLoopGet0

The first two functions are using CFRunLoopGet0 to achieve the function of the incoming thread, the source code is as follows

static CFMutableDictionaryRef __CFRunLoops = NULL; static CFSpinLock_t loopsLock = CFSpinLockInit; RunLoop CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t) t) { if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFSpinLock(&loopsLock); // If (! __CFRunLoops) { __CFSpinUnlock(&loopsLock); / / create a temporary dictionary dict CFMutableDictionaryRef dict = CFDictionaryCreateMutable (kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); // Save the main thread RunLoop to the dict, key is the thread, Value is RunLoop CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); // Both NULL and __CFRunLoops point to NULL, so write dict to __CFRunLoops if (! OSAtomicCompareAndSwapPtrBarrier (NULL, dict, volatile (void * *) & __CFRunLoops)) {/ / release dict CFRelease (dict); } // release mainrunloop CFRelease(mainLoop); __CFSpinLock(&loopsLock); } getMainRunloop (getMainRunloop, getRunLoop); CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops,  pthreadPointer(t)); __CFSpinUnlock(&loopsLock); // If (! Loop) {// Create a runloop CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFSpinLock(&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 __CFSpinUnlock(&loopsLock); CFRelease(newLoop); If (pthread_equal(t, pthread_self())) {_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); If (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {// Register a callback when the thread is destroyed, Destroy the corresponding RunLoop _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; }Copy the code

This code leads to the following conclusions:

  • Runloops correspond to threads one by one in a key-value manner stored in a global dictionary
  • The RunLoop for the main thread is created when the global dictionary is initialized
  • The RunLoop of the child thread is created when it is first fetched and never created if it is not fetched
  • The RunLoop is destroyed when the thread is destroyed

2. Add Mode

CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl)
Copy the code
  • CFRunLoopAddCommonMode Adds a mode to the common modes of the current RunLoop.
  • CFRunLoopCopyCurrentMode Returns the name of the currently running mode
  • CFRunLoopCopyAllModes returns all modes of the current RunLoop

There is no way to create a CFRunLoopMode object directly, but we can call CFRunLoopAddCommonMode and pass in a string to add Mode to the RunLoop. The string is the name of the Mode. The Mode object should be created inside the RunLoop at this point. Take a look at the source code.

1) CFRunLoopAddCommonMode
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) { CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return; __CFRunLoopLock(rl); // Check if rL already has this mode, if yes, do nothing. CFSetContainsValue(rl->_commonModes, modeName)) { CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL; CFSetAddValue(rl->_commonModes, modeName); if (NULL ! = set) { CFTypeRef context[2] = {rl, modeName}; / * the add all common modes - the items to the new mode * / / / this call CFRunLoopAddSource CFRunLoopAddObserver/CFRunLoopAddTimer will call / / __CFRunLoopFindMode (rl, modeName, true), CFRunLoopMode object is created at this time CFSetApplyFunction (set, (__CFRunLoopAddItemsToCommonMode), (void *) context). CFRelease(set); } } else { } __CFRunLoopUnlock(rl); }Copy the code
  • ModeName cannot be repeated. ModeName is the unique identifier of mode
  • The _commonModes array of RunLoop holds the names of all modes marked common
  • Adding commonMode synchronizes all the sources in the commonModeItems array to the newly added mode
  • CFRunLoopMode object is created when CFRunLoopFindMode CFRunLoopAddItemsToCommonMode function call
(2) CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes

CFRunLoopCopyCurrentMode and CFRunLoopCopyAllModes internal logic is simpler, directly return RunLoop _currentMode and _modes, not post source code

Add Run Loop Source (ModeItem)

We can add/remove various events through the following interface:

  • void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
  • void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
  • void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
  • void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
  • void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
  • void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
1) CFRunLoopAddSource
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) {// If (modeName == kCFRunLoopCommonModes) { CFSetRef set = rl->_commonModes? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; If (NULL == rl->_commonModeItems) {// Initialize rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); } // add the passed CFRunLoopSourceRef to _commonModeItems CFSetAddValue(rl->_commonModeItems, RLS); // If (NULL! = set) { CFTypeRef context[2] = {rl, rls}; / * the add new item to all common modes - * / / / is all the mode of execution in the set again CFSetApplyFunction __CFRunLoopAddItemToCommonModes function (the set, (__CFRunLoopAddItemToCommonModes), (void *)context); CFRelease(set); } // If you add a source to kCFRunLoopCommonModes, Else {// Find mode CFRunLoopModeRef RLM = __CFRunLoopFindMode(rl, modeName, true); // Initialize _sources0, _sources0, and _portToV1SourceMap 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 neither _sources0 nor _sources1 contains the source passed in 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) {souce = mach_port_t = mach_port_t; Runloop CFDictionarySetValue(RLM ->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls); __CFPortSetInsert(src_port, rlm->_portSet); } } __CFRunLoopSourceLock(rls); // Add runloop to _runLoops 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

By adding the source code, you can conclude the following:

  • If modeName is passed to kCFRunLoopCommonModes, the source is saved to the _commonModeItems of RunLoop
  • If modeName passes in kCFRunLoopCommonModes, the source will be added to all CommonModes
  • If modeName does not pass in a KCFRunLoopCommonMode, it looks for that Mode first, or creates one if it does not
  • The same source can only be added once in a mode
(2) CFRunLoopRemoveSource

The logic of the remove and add operations is basically the same and easy to understand

Void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef RLS, CFStringRef modeName) { /* DOES CALLOUT */ CHECK_FOR_FORK(); Boolean doVer0Callout = false, doRLSRelease = false; __CFRunLoopLock(rl); Source if (modeName == kCFRunLoopCommonModes) {if (NULL! = rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) { CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; CFSetRemoveValue(rl->_commonModeItems, rls); if (NULL ! = set) { CFTypeRef context[2] = {rl, rls}; /* remove new item from all common-modes */ CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context); CFRelease(set); }} else {}} else {// Find mode according to modeName, if not present, return NULL cfrunloopFindMode (rl, modeName, false); if (NULL ! = rlm && ((NULL ! = rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL ! = rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) { CFRetain(rls); If (1 == RLS ->_context.version0.version) {__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info); if (CFPORT_NULL ! = src_port) { CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port); __CFPortSetRemove(src_port, rlm->_portSet); } } CFSetRemoveValue(rlm->_sources0, rls); CFSetRemoveValue(rlm->_sources1, rls); __CFRunLoopSourceLock(rls); if (NULL ! = rls->_runLoops) { CFBagRemoveValue(rls->_runLoops, rl); } __CFRunLoopSourceUnlock(rls); if (0 == rls->_context.version0.version) { if (NULL ! = rls->_context.version0.cancel) { doVer0Callout = true; } } doRLSRelease = 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.cancel(rls->_context.version0.info, rl, modeName); /* CALLOUT */ } if (doRLSRelease) CFRelease(rls); }Copy the code
③ Add an Observer and a Timer

The internal logic of adding an observer and timer is similar to adding a source. The difference is that the observer and timer can only be added to one or more modes of a RunLoop. For example, if a timer is added to the RunLoop of the main thread, it cannot be added to the RunLoop of the child thread. Source does not have this limitation. It doesn’t matter which RunLoop is in mode, you can add it. This difference can also be found in the structure at the beginning of this article. The CFRunLoopSource structure has an array of RunLoop objects, while CFRunLoopObserver and CFRunLoopTimer have only a single RunLoop object.

RunLoop run – source code interpretation

There are two apis in Core Foundation that allow RunLoop to run:

  • Void CFRunLoopRun(void) Runs the current thread’s RunLoop in the default mode.

  • CFRunLoopRunResult CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled) runs the RunLoop of the current thread in the specified mode.

1. CFRunLoopRun

// kCFRunLoopDefaultMode void CFRunLoopRun(void) {/* DOES CALLOUT */ int32_t result; Do {// Run runloop result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0 e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped ! = result && kCFRunLoopRunFinished ! = result); }Copy the code

The CFRunLoopRunSpecific function is called in the CFRunLoopRun function, with the runloop argument passed to the current runloop object and the modeName argument passed to kCFRunLoopDefaultMode. Verify the explanation of the previous documentation.

2. CFRunLoopRunInMode

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 is also called in the CFRunLoopRunInMode function. The runloop argument is passed to the current runloop object, and the modeName argument continues to pass the modeName passed by CFRunLoopRunInMode. The explanation of the previous documentation is also verified.

A RunLoop must specify only one mode when running. When running, the specified mode is currentMode. Neither of these functions shows how RunLoop runs. Let’s explore what’s going on inside the CFRunLoopRunSpecific function to see how RunLoop runs.

3. CFRunLoopRunSpecific

@param rl Specifies the current runloop * @param modeName Specifies the name of the mode to be run * @param seconds Specifies the timeout of the runloop * @param Return */ SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; __CFRunLoopLock(rl); CurrentMode = __CFRunLoopFindMode(rl, modeName, false); / / if you don't find | | mode not registered in any event, the end, Not to circulate the 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; //2.3 Initialize a result as kCFRunLoopRunFinished int32_t result = kCFRunLoopRunFinished; __CFRunLoopRun is the core function if (currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); Runloop if (currentMode->_observerMask & kCFRunLoopExit) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); __CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result; }Copy the code

Through the internal logic of CFRunLoopRunSpecific, we can conclude:

  • If you specify a non-existent mode to run the RunLoop, it will fail and the mode will not be created, so the mode passed in here must exist
  • If you specify a mode that does not contain any modeItem, the RunLoop will not run either, so you must * pass in a mode that contains at least one modeItem
  • Notify the Observer before entering the Run loop that the status is kCFRunLoopEntry
  • Notify the Observer after exiting the Run loop that the status is kCFRunLoopExit

4. __CFRunLoopRun

In CFRunLoopRunSpecific,__CFRunLoopRun is the key function

/* rl, rlm are locked on entrance and exit */
/**
 *  运行run loop
 *
 *  @param rl              运行的RunLoop对象
 *  @param rlm             运行的mode
 *  @param seconds         run loop超时时间
 *  @param stopAfterHandle true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  @param previousMode    上一次运行的mode
 *
 *  @return 返回4种状态
 */

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    //获取系统启动后的CPU运行时间,用于控制超时时间
    uint64_t startTSR = mach_absolute_time();
    
    // 判断当前runloop的状态是否关闭
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    //mach端口,在内核中,消息在端口之间传递。 初始为0
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    //判断是否为主线程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    //如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        //mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    
    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;
        // 1.0e10 == 1* 10^10
    } else if (seconds <= TIMER_INTERVAL_LIMIT) {
        //seconds为超时时间,超时时执行__CFRunLoopTimeout函数
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    } else { // infinite timeout
        //永不超时 - 永动机
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
    
    //标志位默认为true
    Boolean didDispatchPortLastTime = true;
    //记录最后runloop状态,用于return
    int32_t retVal = 0;
    
    // itmes
 
    do {
        //初始化一个存放内核消息的缓冲池
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
        //取所有需要监听的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //设置RunLoop为可以被唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        if (rlm->_observerMask & kCFRunLoopBeforeSources)
            /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        /// 执行被加入的block
        __CFRunLoopDoBlocks(rl, rlm);
        /// 4. RunLoop 触发 Source0 (非port) 回调。
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            /// 执行被加入的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //如果没有Sources0事件处理 并且 没有超时,poll为false
        //如果有Sources0事件处理 或者 超时,poll都为true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        //第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //从缓冲区读取消息
            msg = (mach_msg_header_t *)msg_buffer;
            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                //如果接收到了消息的话,前往第9步开始处理msg
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }
        
        didDispatchPortLastTime = false;
        /// 6.通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //设置RunLoop为休眠状态
        __CFRunLoopSetSleeping(rl);
        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        
        //这里有个内循环,用于接收等待端口的消息
        //进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            
            msg = (mach_msg_header_t *)msg_buffer;
            //7.接收waitSet端口的消息
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            //收到消息之后,livePort的值为msg->msgh_local_port,
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        /**
        7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
        • 一个基于 port 的Source 的事件。
        • 一个 Timer 到时间了
        • RunLoop 自身的超时时间到了
        • 被其他什么调用者手动唤醒
        **/
        // mach 事务 - 指令 
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.
        
        __CFPortSetRemove(dispatchPort, waitSet);
        
        __CFRunLoopSetIgnoreWakeUps(rl);
        
        // user callouts now OK again
        //取消runloop的休眠状态
        __CFRunLoopUnsetSleeping(rl);
        /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
        /// 收到消息,处理消息。
    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
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        /// 9.2 如果有dispatch到main_queue的block,执行block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // 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;
                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
        
        /// 执行加入到Loop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            /// 进入loop时参数说处理完事件就返回。
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            /// 超出传入参数标记的超时时间了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            /// 被外部调用者强制停止了
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            /// 自动停止了
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            /// source/timer/observer一个都没有了
            retVal = kCFRunLoopRunFinished;
        }
        /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}
Copy the code

The core process is shown below

4. Summary

Each thread has a corresponding RunLoop object. The RunLoop for the main thread is created automatically when the program starts, and the child threads need to manually obtain the RunLoop to create it. At the heart of RunLoop execution is a do.. while.. Loop through all the events that need to be handled, let the thread work if there is event processing, let the thread sleep if there is no event processing, while waiting for the event to arrive.

RunLoop application

1. Cocoa performs the Selector source

[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray<NSString *> *)#>]

[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray<NSString *> *)#>]
Copy the code

The waitUntilDone parameter is whether to wait until the end.

  1. If the value is set to YES, the thread will have to wait until the method completes before it can continue. It blocks the thread of submission.
  2. If NO, the method is invoked asynchronously and does not block the submission thread.

2. Timing sources

[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>
[NSTimer timerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]
Copy the code
  • The method at the beginning of schedule is added to NSDefaultRunLoopMode by default, All the other method needs to call the [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode] to give it specifies a mode.

  • When the user scrolls the Scrollview, the RunLoop will switch to the UITrackingRunLoopMode mode, and the timer will run under defaultMode. The system can process only one RunLoop mode at a time, so the timer in defaultMode will become invalid.

The solution: [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSRunLoopCommonModes]; Register the timer with NSRunLoopCommonModes, which include defaultMode and trackingMode.

3. The child thread processes time-consuming operations

dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer * timer = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) { static int count NSLog(@"%s - %d",__func__,count++);}]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; Runloop [[NSRunLoop currentRunLoop] run]; });Copy the code

Note that you must add [[NSRunLoop currentRunLoop] run]; Because when you enter a block, you create the timer, and you add the timer to the child thread’s runloop, but the important thing is that the runloop doesn’t run automatically, you have to run it manually, and if you don’t run the runloop, the timer is released, Nothing is executed.

4. RunLoop and AFNetworking(version 2.x)

+ (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } } + (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread; } - (void)start { [self.lock lock]; if ([self isCancelled]) { [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread]  withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } else if ([self isReady]) { self.state = AFOperationExecutingState; [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } [self.lock unlock]; }Copy the code

The AFURLConnectionOperation class is built on NSURLConnection and is expected to receive Delegate callbacks in the background thread. AFNetworking creates a separate thread for this purpose and starts a RunLoop in this thread. Before RunLoop starts, there must be at least one Timer/Observer/Source, so AFNetworking creates a new NSMachPort and adds it to it before [RunLoop run]. Normally, the caller needs to hold this NSMachPort (mach_port) and send messages inside the loop through this port from the external thread; But port is added here only to keep the RunLoop from exiting, not to actually send messages. When need this background threads execute tasks, AFNetworking by calling [NSObject performSelector: onThread:] will this task to the RunLoop background thread.

5. Performance optimization — read images in sandbox /bundle

UIImage *bundleImage = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]];  [self.myImageView performSelector:@selector(setImage:) withObject:bundleImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];Copy the code

PerformSelector is in multiple modes by default Under execution, add inModes parameters, only set an NSDefaultRunLoopMode, allows users to slide into NSEventTrackingRunLoopMode, does not perform setImage, thus improve the sliding view.

6. Optimize performance — TableView slide stuck

Let’s say the TableView loads a lot of images, and the thread will block.

Blocking causes: KCFRunLoopDefaultMode when multiple images (especially high definition large images) are loaded together (time-consuming) Loop Does not end Unable to BeforeWaiting Switching to UITrackingRunLoopMode to handle waiting UI refresh events blocks.

Workaround: The RunLoop only loads one image at a time so the loop will quickly go to the UI refresh after BeforeWaiting processing (UITrackingRunLoopMode takes precedence) or proceed to the next image without the UI refresh event.

The solution is the following code, where Timer is added to keep RunLoop active even after all tasks are processed.

Note: Always remove the Observer and timer when appropriate

@property (nonatomic, strong) NSTimer *timer; @property (nonatomic, strong) NSMutableArray *tasks; @property (nonatomic, assign) NSInteger maxTaskNumber; - (void)viewDidLoad { [super viewDidLoad]; [self addRunloopOvserver]; self.maxTaskNumber = 20; self.tasks = [NSMutableArray array]; The self. The timer = [NSTimer scheduledTimerWithTimeInterval: 0.05 target: self selector: @ the selector (run) the userInfo: nil repeats: YES];  } -(void)run{} -(void) addRunloopOvserver{// Get the current RunLoop CFRunLoopRef RunLoop = CFRunLoopGetCurrent(); // Pass (__bridge void *)self to Callback CFRunLoopObserverContext Context = {0, (__bridge void *)self, &CFRetain, &CFRelease}; CallBack CFRunLoopObserverRef Observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context); // Add observer to current runloop kCFRunLoopDefaultMode can be changed to kCFRunLoopCommonModes CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes); //CFRelease(observer); } void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, ViewController *vc = (__bridge ViewController *)(info); void *vc = (__bridge ViewController *)(info); if (vc.tasks.count == 0) { return; } void(^task)() = [vc.tasks firstObject]; task(); [vc.tasks removeObject:task]; NSLog(@"COUNT:%ld",vc.tasks.count); } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identity" forIndexPath:indexPath]; for (int i = 1; i < 4; i++) { UIImageView *imageView = [cell.contentView viewWithTag:i]; [imageView removeFromSuperview]; } for (int i = 1; i < 4; I++) {// float leading = 10, space = 20, width = 103, height = 87, top = 15; // float leading = 10, space = 20, width = 103, height = 87, top = 15; // UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)]; // [cell.contentView addSubview:imageView]; // imageView.tag = i; // imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]]; // Cause of block: Switch to UITrackingRunLoopMode to handle waiting UI refresh events causing a block // Each RunLoop only loads one image so that the loop will quickly go to the UI refresh immediately after BeforeWaiting processing (UITrackingRunLoopMode takes precedence) or proceed to the next image without the UI refresh event /* Smooth mode */ Void (^task)() = {CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15; void(^task)() = {CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15; UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)]; [cell.contentView addSubview:imageView]; imageView.tag = i; imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]]; }; [self.tasks addObject:task]; if (self.tasks.count >= self.maxTaskNumber) { [self.tasks removeObjectAtIndex:0]; } } return cell; }Copy the code

Finally, the Runloop source code and official documentation are attached

Part of the inspiration comes from RunLoop detailed explanation and analysis of RunLoop principle and its applications

To contact me

Code word/arrangement/summary is not easy, if you like, please click star

If you like hr, please contact QQ :1028717839. Thank you for your support