Runloop overview

1. What is Runloop?

A run loop is to do something in a loop while the program is running. If there is no runloop, the program will exit after execution. With runloop, the program can always run and wait for user input. Runloop can run itself when needed and sleep when there is no operation. It can save CPU resources and improve program performance.

2. Runloop function

1. Keep the program running continuously. A main thread will be opened as soon as the program is started. A corresponding runloop will be created as soon as the main thread is started. The runloop ensures that the main thread is not destroyed, thus ensuring the continuous running of the program.
2. Handle various APP events, such as touch events, timer events, selector events
3. Save CPU resources and improve program performance. When the thread that opens the runloop has no operations to do, the thread will sleep and release CPU resources. When there is something to be processed, the thread will be awakened immediately for processing.
Runloop internal principles
The figure shows that when Runloop is running, when it receives input sources (mach port, custom input source, performSelector:onThread:..., etc.) or Timer sources, it will be handed over to the corresponding processing method for processing. When no event messages are received, the runloop rests.

3. The relationship between RunLoop and threads

Runloop is managed based on pthread, which is a c-based cross-platform multi-threaded operation underlying API. It is the upper-layer encapsulation of mach thread (see Kernel Programming Guide), and corresponds one-to-one with NSThread (NSThread is a set of object-oriented APIs, so we almost do not need to use pthread directly in iOS development).

There is no interface for directly creating Runloop in the interface developed by Apple. If you need to use Runloop, you usually use CF's CFRunLoopGetMain() and CFRunLoopGetCurrent() methods to obtain it. Correspondingly, you can call [NSRunLoop currentRunLoop] in Foundation to obtain the RunLoop object of the current thread, and
call [NSRunLoop mainRunLoop] to obtain the RunLoop object of the main thread.

//获取主线程的RunLoop
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;
}

//获取当前Runloop,没有就调用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法内部
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
     //t为空默认是主线程
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    //__CFRunLoops字典为空
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    // 为主线程创建对应的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    // 将主线程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 将线程作为key从字典里获取一个loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    // 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
    if (!loop) {  
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    // 创建好之后,以线程为key,runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
    if (!loop) { 
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    //如果传入的线程和方法调用的线程是同一个,且尚未为该runloop注册销毁函数,则为runloop注册一个销毁函数__CFFinalizeRunLoop
    if (pthread_equal(t, pthread_self())) {
     //将RunLoop存在线程特定数据区,提高后续查找速度   _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

It can be seen from the above code:
1. There is a one-to-one correspondence between threads and RunLoop. The relationship is stored in a Dictionary, with thread as key and RunLoop as value.
2. When we want to create a RunLoop of a child thread, we only need to obtain the RunLoop object of the current thread in the child thread, using CFRunLoopGetCurrent() or [NSRunLoop currentRunLoop]. When the method is called, it will first check whether there is a RunLoop in the dictionary that is used by the sub-thread. If there is, it will directly return the RunLoop. If not, it will create one and store the corresponding sub-thread in the dictionary. If it is not obtained, the child thread will not create the RunLoop associated with it.
3. The Runloop of the main thread is special. Before any thread is created, it is guaranteed that the Runloop of the main thread already exists, and CFRunLoopGetMain() can obtain the RunLoop of the main thread regardless of whether it is called in the main thread or a child thread.
4.RunLoop is created when it is first acquired and destroyed when the thread ends.

In addition, when the APP starts, the Runloop is started in the UIApplicationMain function in the main thread, and the program will not exit immediately, but will remain running. Therefore every application must have a runloop.

4. Detailed explanation of RunLoop related classes and functions

Classes related to RunLoop in the Foundation framework include NSRunLoop. The five classes about RunLoop in Core Foundation:

  1. CFRunLoopRef - Get the current RunLoop and main RunLoop
  2. CFRunLoopModeRef - RunLoop operating mode, you can only choose one, perform different operations in different modes
  3. CFRunLoopSourceRef - event source, input source
  4. CFRunLoopTimerRef - timer event
  5. CFRunLoopObserverRef - observer

4.1 CFRunLoopRef

4.1.1 Types of RunLoop

We already know before that we can get the RunLoop of the current thread through the CFRunLoopGetCurrent function, and the RunLoop of the main thread through the CFRunLoopGetMain function, as well as some differences between them.

4.1.2 Structure of CFRunLoopRef

CFRunLoopRef is a pointer to the __CFRunLoop structure.
Through the source code we find the __CFRunLoop structure:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

In addition to some record attributes, let's mainly take a look at the following two member variables:
CFRunLoopModeRef _currentMode; which points to the current mode of RunLoop,
and CFMutableSetRef _modes; the mode included in RunLoop. CFMutableSetRef _commonModes contains the mode name marked as common mode in _modes, and CFMutableSetRef _commonModeItems contains the mode item that should be added to the common mode. We will also discuss it specifically in CFRunLoopModeRef.

CFRunLoopModeRef represents the running mode of RunLoop.
A RunLoop contains several Modes, and each Mode contains several Sources, Timers, and Observers called mode items.
When RunLoop starts and runs, only one of the Modes can and must be specified. This Mode is called Current Mode.
If you need to switch Mode, you can only exit the current Mode and re-specify a Mode to enter. This is mainly to separate different groups of Source, Timer, and Observer so that they do not affect each other. If there is no Source0/Source1/Timer in Mode, RunLoop will exit immediately.

CFRunLoopModeRef diagram

4.1.3 Operation of CFRunLoopRef

CFRunLoopRun
Runs the current thread's CFRunLoop object in its default mode indefinitely.
Let the current thread's RunLoop run in default mode.

CFRunLoopRunInMode
Runs the current thread's CFRunLoop object in a particular mode.
Let the current thread's RunLoop run in the specified mode.

CFRunLoopWakeUp
Wakes a waiting CFRunLoop object.
Wakes up a dormant RunLoop.

CFRunLoopStop
Forces a CFRunLoop object to stop running.
Stop the running of the RunLoop.

CFRunLoopIsWaiting
Returns a Boolean value that indicates whether the run loop is waiting for an event.
Determine whether a RunLoop is sleeping and waiting for the event to wake up.

4.2 CFRunLoopModeRef

4.2.1 mode type

There are 5 Modes registered by default on the system on iOS. There are two Modes publicly provided by Apple: kCFRunLoopDefaultMode (NSDefaultRunLoopMode) and UITrackingRunLoopMode. You can use these two Mode Names to operate their corresponding Modes.

The five operating modes are:

  1. kCFRunLoopDefaultMode: App’s default Mode. Usually the main thread runs in this Mode.
  2. UITrackingRunLoopMode: Interface tracking Mode, used for ScrollView to track touch sliding to ensure that the interface is not affected by other modes when sliding.
  3. UIInitializationRunLoopMode: The first Mode entered when the App is first started. It will no longer be used after the startup is completed.
  4. GSEventReceiveRunLoopMode: Internal Mode for receiving system events, usually not used
  5. kCFRunLoopCommonModes: This is a placeholder Mode, used as a mark for kCFRunLoopDefaultMode and UITrackingRunLoopMode, and is not a real Mode.

There is a concept called "CommonModes": a Mode can mark itself as a "Common" attribute (by adding its ModeName to the RunLoop's "commonModes"). Whenever the content of RunLoop changes, RunLoop will automatically synchronize the Source/Observer/Timer in _commonModeItems to all Modes with the "Common" tag.

Application scenario example: There are two preset Modes in the main thread's RunLoop: kCFRunLoopDefaultMode and UITrackingRunLoopMode. Both Modes have been marked as "Common" attributes. DefaultMode is the normal state of the App, and TrackingRunLoopMode is the state when tracking the ScrollView sliding. When you create a Timer and add it to DefaultMode, the Timer will get repeated callbacks, but when you slide a TableView, the RunLoop will switch the mode to TrackingRunLoopMode. At this time, the Timer will not be called back, and it will not affect the sliding operation. .

Sometimes you need a Timer that can get callbacks in both Modes. One way is to add this Timer to the two Modes respectively. Another way is to add the Timer to the "commonModeItems" of the top-level RunLoop. "commonModeItems" is automatically updated by RunLoop to all Modes with the "Common" attribute.

4.2.2 Structure of CFRunLoopModeRef

CFRunLoopModeRef is actually a pointer to the __CFRunLoopMode structure. The source code of the __CFRunLoopMode structure is as follows:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    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 */
};

Mainly view the following member variables

CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

In CFRunLoopModeRef, we have already discussed the relationship between CFRunLoopModeRef, CFRunLoopModeRef, and mode item, so we will not go into details here.

The mode item includes Source1/Source0/Timers/Observer. What do they represent?

  1. Source1: Port-based inter-thread communication
  2. Source0: Non-Port events, including touch events, Thread-related PerformSelectors without delay, etc.
  3. Timers: timers, NSTimer
  4. Observer: Listener, used to monitor the status of RunLoop

4.2.3 Operation of RunLoop mode

In Core Foundation, Apple only opens the following three APIs for Mode operations (Cocoa also has functions with the same function, which will not be listed again):

CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
Adds a mode to the set of run loop common modes.
向当前RunLoop的common modes中添加一个mode。

CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
CFArray
返回当前运行的mode的name

Ref CFRunLoopCopyAllModes(CFRunLoopRef rl)
返回当前RunLoop的所有mode

CFRunLoopAddCommonMode

We have no way to directly create a CFRunLoopMode object, but we can call CFRunLoopAddCommonMode to pass in a string to add Mode to RunLoop. The passed string is the name of Mode. When there is no corresponding mode inside RunLoop, RunLoop will automatically create a corresponding one for you. CFRunLoopModeRef. For a RunLoop, its internal mode can only be added but not deleted. Let’s take a look at the source code.

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    //看rl中是否已经有这个mode,如果有就什么都不做
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
        CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
        //把modeName添加到RunLoop的_commonModes中
        CFSetAddValue(rl->_commonModes, modeName);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, modeName};
            /* add all common-modes items to new mode */
            //这里调用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的时候会调用
            //__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode对象在这个时候被创建
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
            CFRelease(set);
        }
    } else {
    }
    __CFRunLoopUnlock(rl);
}

It can be seen that:

1.modeName cannot be repeated. modeName is the unique identifier of mode.
2.RunLoop's _commonModes array stores the names of all modes marked as common.
3. Adding commonMode will synchronize all sources in the commonModeItems array to the newly added mode
4. The CFRunLoopMode object is created when CFRunLoopAddItemsToCommonMode function calls CFRunLoopFindMode

CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes

The internal logic of CFRunLoopCopyCurrentMode and CFRunLoopCopyAllModes is relatively simple. Just take the _currentMode and _modes of RunLoop and return them. The source code will not be posted.

4.2.4 Switching between Modes

Schedule a Timer in the main thread and run normally, and then when sliding the TableView or UIScrollView on the interface, the previous Timer does not trigger. why?

[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer1) userInfo:nil repeats:YES];

This problem is also related to RunLoop's RunLoopMode. Because when scheduling a Timer, the Timer is added to the NSDefaultRunLoopMode of the RunLoop, and when sliding the TableView or UIScrollView, the RunLoop Mode is switched to the UITrackingRunLoopMode. The RunLoop will only track the sliding events on the UI and the Timer will not be suspended. will be triggered.

The Mode switching function is not implemented in the CF framework, so UIKit needs to handle it by itself when using it. The general process is that UIApplication pushes a mode that is about to be executed, and then wakeup RunLoop. When switching back, you only need to pop this mode and then wakeup.
[The external link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly (img-FlolCpqS-1652108715759)(https://upload-images.jianshu.io/upload_images/1049809-74da0b6560623ec5.png ?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000)]

4.3 CFRunLoopSourceRef event source

4.3.1 Types of CFRunLoopSource

CFRunLoopSource
You can see these two things CFMutableSetRef _source0 and CFMutableSetRef _source1 in my RunLoopMode data structure code. First of all, these two things are Set (set), and the set stores a bunch of data structures. So what is this source? , they are actually also a data structure CFRunLoopSourceRef.

CFRunLoopSource is an abstraction of input sources. CFRunLoopSource is divided into two versions: source0 and source1.

Source0: Not based on Port, used for events actively triggered by the user (clicking a button or clicking the screen, the PerformSelectors related to Thread without delay is source0, and the one with delay is timer).
Source1: Port-based, sending messages to each other through the kernel and other threads (related to the kernel).

4.3.2 Structure of CFRunLoopSourceRef

CFRunLoopSourceRef is a pointer to the __CFRunLoopSource structure. Its structure is as follows:

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;   //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    union {
      CFRunLoopSourceContext version0;    /* source0的数据结构 */
        CFRunLoopSourceContext1 version1;    /* source1的数据结构 */
    } _context;
};

The data structure __CFRunLoopSource contains a _context member. Its type is CFRunLoopSourceContext or CFRunLoopSourceContext1, which specifies the type of the source.

source0
source0 is an internal event of the App. UIEvent and CFSocket managed by the App itself are both source0. When a source0 event is ready to be executed, it must first be marked as signal. The following is the structure of source0:

//source0
typedef struct {
    CFIndex    version;  // 版本号,用来区分是source1还是source0
    void *    info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef    (*copyDescription)(const void *info);
    Boolean    (*equal)(const void *info1, const void *info2);
    CFHashCode    (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

source0 is not Port-based. It only contains a callback (function pointer), which cannot actively trigger events. When using it, you need to first call CFRunLoopSourceSignal(source) to mark the Source as pending, and then manually call CFRunLoopWakeUp(runloop) to wake up RunLoop and let it handle the event.

source1
source1 is managed by RunLoop and the kernel. Source1 has mach_port_t, which can receive kernel messages and trigger callbacks. The following is the structure of source1

//source1
typedef struct {
    CFIndex    version;  // 版本号,用来区分是source1还是source0
    void *    info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef    (*copyDescription)(const void *info);
    Boolean    (*equal)(const void *info1, const void *info2);
    CFHashCode    (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t    (*getPort)(void *info);  // 端口
    void *    (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *    (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

In addition to the callback pointer, Source1 contains a mach port. Source1 can listen to the system port, communicate with other threads through the kernel, receive and distribute system events. It can actively wake up RunLoop (managed by the operating system kernel, such as CFMessagePort messages). The official also pointed out that Source can be customized, so it is more like a protocol for CFRunLoopSourceRef. The framework has defined two implementations by default. Developers can also customize it if necessary. For details, you can check the official documentation .

4.3.3 Operation of CFRunLoopSource

CFRunLoopAddSource
The code structure of CFRunLoopAddSource is as follows:

//添加source事件
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);
    //如果是kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //如果runloop的_commonModes存在,则copy一个新的复制给set
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
       //如果runl _commonModeItems为空
        if (NULL == rl->_commonModeItems) {
            //先初始化
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //把传入的CFRunLoopSourceRef加入_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        //如果刚才set copy到的数组里有数据
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //则把set里的所有mode都执行一遍__CFRunLoopAddItemToCommonModes函数
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的逻辑就是,如果你往kCFRunLoopCommonModes里面添加一个source,那么所有_commonModes里的mode都会添加这个source
    } else {
        //根据modeName查找mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        //如果_sources0不存在,则初始化_sources0,_sources0和_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);
        }
        //如果_sources0和_sources1中都不包含传入的source
        if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
            //如果version是0,则加到_sources0
            if (0 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources0, rls);
                //如果version是1,则加到_sources1
            } 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) {
                    //此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来
                    //可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloop
                    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                    __CFPortSetInsert(src_port, rlm->_portSet);
                }
            }
            __CFRunLoopSourceLock(rls);
            //把runloop加入到source的_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 */
    }
}

By adding this source code, we can draw the following conclusions:

If modeName is passed into kCFRunLoopCommonModes, the source will be saved to RunLoop's _commonModeItems and added to all common Mode mode items.
If the modeName passed in is not kCFRunLoopCommonModes, the Mode will be searched first. If not, one will be created. The
same source can only be added once in a mode.

CFRunLoopRemoveSource

The logic of the remove operation and the add operation are basically the same and are easy to understand.

//移除source
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
    CHECK_FOR_FORK();
    Boolean doVer0Callout = false, doRLSRelease = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes,则从_commonModes的所有mode中移除该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 {
        //根据modeName查找mode,如果不存在,返回NULL
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false);
        if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) {
            CFRetain(rls);
            //根据source版本做对应的remove操作
            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);
}

CFRunLoopContainsSource
Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
Determine whether the specified source exists in the specified mode in RunLoop

Here is a brief summary:
1. CFRunLoopSourceRef is where the event occurs;
2. This CFRunLoopSourceRef has two versions, source0 and source1;
3. source0 only contains a callback (function pointer) and cannot actively initiate events. CFRunLoopSourceSignal(source) is required. Source is marked as pending, CFRunLoopWakeUp(runloop) wakes up RunLoop and lets it process event
4. source1 contains mach_port and a callback (function pointer), which is used to send messages to each other through the kernel and other threads, and can actively wake up RunLoop.
5. Input sources distribute asynchronous events to the corresponding handlers, causing runUntilDate: to exit; while timers distribute synchronous events to their handlers, which will not cause RunLoop to exit.
6.Perform selector is a custom input source. Perform selector requests are received serially on the target thread. After the selector is executed, it will remove itself from the runloop, but the port-based source will not. runloop will process all selectors each time it runs, instead of processing one at a time.

4.4 CFRunLoopTimerRef

CFRunLoopTimerRef is a time-based trigger. It and NSTimer are toll-free bridged and can be mixed. Therefore, NSTimer is an encapsulation of RunLoopTimer. CFRunLoopTimerRef contains a length of time and a callback (function pointer). When it is added to RunLoop, RunLoop will register the corresponding time point. When the time point arrives, RunLoop will be awakened to execute that callback.

4.4.1 Structure of CFRunLoopTimerRef

CFRunLoopTimerRef is a pointer to the __CFRunLoopTimer structure.

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;		/* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;			/* TSR units */
    CFIndex _order;			/* immutable */
    CFRunLoopTimerCallBack _callout;	/* immutable */
    CFRunLoopTimerContext _context;	/* immutable, except invalidation */
};

4.4.2 Operation of CFRunLoopTimerRef

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
adds the specified timer to the specified mode of the specified RunLoop

void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
removes the specified timer in the specified mode of the specified RunLoop

Boolean CFRunLoopContainsTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
determines whether the specified timer exists in the specified mode of the specified RunLoop

CFAbsoluteTime CFRunLoopGetNextTimerFireDate(CFRunLoopRef rl, CFRunLoopMode mode);
Get the latest trigger time of all timers registered in the specified mode of the specified RunLoop

####4.4.3 Timer implementation
Due to space reasons, it will be introduced in another article.

4.5CFRunLoopObserverRef

4.5.1RunLoop status

CFRunLoopObserverRef is a listener in the message loop that can monitor the status changes of RunLoop and notify the outside of the current running status of RunLoop at any time (it contains a function pointer _callout_ to tell the observer the current status in time).

The status of RunLoop (CFOptionFlags) includes the following:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1),        // runLoop即将处理 Timers
    kCFRunLoopBeforeSources = (1UL << 2),       // runLoop即将处理 Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),       // runLoop即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),        // runLoop刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),                // 即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU       
};

4.5.2Structure of CFRunLoopObserverRef

The structure of CFRunLoopObserverRef is as follows:

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;    //observer对应的runLoop
    CFIndex _rlCount;              //observer当前监测的runLoop数量
    CFOptionFlags _activities;   //observer观测runLoop的状态,枚举类型
    CFIndex _order;            //CFRunLoopMode中是数组形式存储的observer,_order就是他在数组中的位置
    CFRunLoopObserverCallBack _callout;    //observer观察者的函数回调
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

There is an _order field here, which determines the position of the observer. The smaller the value, the higher it is, and it can be called earlier. Similarly, __CFRunLoopSource, __CFRunLoopTimer and __CFRunLoopObserver all have such fields. The use of _order will also be introduced in detail in 7.1 AutoreleasePool.

Here we should pay attention to the _callout field. During the development process, almost all operations are called back through Call out (whether it is Observer status notification or Timer and Source processing), and the system usually uses the following functions to perform callbacks (in other words, your The code is actually ultimately called through the following functions. Even if you listen to the Observer yourself, it will first call the following functions and then notify you indirectly, so you often see these functions in the call stack):

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

For example, __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(rlo->_callout, rlo, activity, rlo->_context.info) will internally call the function pointed to by rlo->_callout.

We set a breakpoint in touchBegin of the controller to view the stack (since UIEvent is Source0, you can see a Call out function CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION of Source0):

4.5.3 Operation of CFRunLoopObserverRef

void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
adds the specified observer to RunLoop in the specified mode

void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
removes the specified observer in the RunLoop specified mode

Boolean CFRunLoopContainsObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
Determine whether there is a specified observer in the specified RunLoop mode

Discussion:
The internal logic of adding observer and timer is roughly similar to adding source.
The difference is that 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, the timer cannot be added to the RunLoop of the child thread. Source does not have this restriction. , no matter which RunLoop it is, as long as it is not in the mode, it can be added.
This difference can also be found in some of the previous structures. The CFRunLoopSource structure has an array that saves RunLoop objects, while CFRunLoopObserver and CFRunLoopTimer only have a single RunLoop object.

5. Start and exit of RunLoop

5.1Startup of RunLoop

Startup of RunLoop in Foundation
There are the following three ways to start a runloop in Foundation. No matter which of these three methods is used to start the runloop, if there is no input source or timer attached to the runloop, the runloop will exit immediately.

  • (void)run;
  • (void)runUntilDate:(NSDate *)limitDate;
  • (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

(1) In the first method, the runloop will continue to run, during which the data from the input source will be processed, and the runMode:beforeDate: method will be called repeatedly in the NSDefaultRunLoopMode mode; (2) In the second method, a timeout can be
set time, before the timeout time is reached, the runloop will keep running. During this period, the runloop will process the data from the input source, and will also repeatedly call the runMode:beforeDate: method in the NSDefaultRunLoopMode mode; (3) The third way, the
runloop Once executed, it is locked by the input source in the specified mode. When the timeout reaches or the first input source is processed, this execution will exit.

The first two startup methods will repeatedly call the runMode:beforeDate: method.

Startup of RunLoop in CF

Let's look at the source code for starting RunLoop in CF:

 // 用DefaultMode启动
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

//指定mode的一次运行
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

We found that RunLoop is indeed implemented by do while by judging the value of result.

5.2 Exit of RunLoop

1. The main thread is destroyed and RunLoop exits. (The thread is destroyed and exits)
2. There are some Timers and Sources in Mode, which ensure that when Mode is not empty, RunLoop is not idling and is running. When Mode is empty, RunLoop will exit immediately. (But Apple does not recommend that we do this, because the system may add some input sources to the runloop of the current thread, so manually removing the input source or timer does not guarantee that the runloop will definitely exit.) ( Exit without mode item)
3. We can set when to stop when starting RunLoop. (timeout exit)

How to exit NSRunLoop

6. RunLoop processing logic

6.1 RunLoop event processing

We have already introduced the startup of RunLoop in CF. How does RunLoop run? This is related to the function CFRunLoopRunSpecific.

The CFRunLoopRunSpecific function is called in the CFRunLoopRun function, the runloop parameter is passed into the current RunLoop object, and the modeName parameter is passed into kCFRunLoopDefaultMode.

The CFRunLoopRunSpecific function is also called in the CFRunLoopRunInMode function. The runloop parameter is passed in the current RunLoop object, and the modeName parameter continues to pass the modeName passed in by CFRunLoopRunInMode.

Let's take a look at the source code of CFRunLoopRunSpecific.

/*
 * 指定mode运行runloop
 * @param rl 当前运行的runloop
 * @param modeName 需要运行的mode的name
 * @param seconds  runloop的超时时间
 * @param returnAfterSourceHandled 是否处理完事件就返回
 */
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //根据modeName找到本次运行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    //保存上一次运行的mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    //更新本次mode
    rl->_currentMode = currentMode;
    //初始化一个result为kCFRunLoopRunFinished
    int32_t result = kCFRunLoopRunFinished;
    
    // 1.通知observer即将进入runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //10.通知observer已退出runloop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

Through the internal logic of CFRunLoopRunSpecific, we can conclude:

  1. If you specify a non-existent mode to run RunLoop, it will fail and the mode will not be created, so the mode passed here must exist.
  2. If a mode is specified, but the mode does not contain any modeItem, the RunLoop will not run, so a mode containing at least one modeItem must be passed in.
  3. Notify the observer before entering the run loop, the status is kCFRunLoopEntry
  4. Notify the observer after exiting the run loop, the status is kCFRunLoopExit

The core function of RunLoop operation is __CFRunLoopRun. Next, we analyze the source code of __CFRunLoopRun.
__CFRunLoopRun:

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

One thing to note here is that the macro USE_DISPATCH_SOURCE_FOR_TIMERS is 1 on Mac and 0 on iOS. Therefore, some code here does not exist on iOS.
On iOS, __CFRunLoopRun has only one do-while loop, which can be regarded as the entire life cycle of RunLoop. When some conditions are met, the loop ends, which means that RunLoop is also over. These conditions include that the source is processed and the RunLoop needs to be stopped after the source is processed, the RunLoop times out, the current mode of the RunLoop is stopped, and the mode item in the current mode is empty. __CFRunLoopServiceMachPort is used to handle sleep and wake-up.
According to the official website description, every time RunLoop is started, the thread's RunLoop will process some pending events and generate notifications for registered observers. The order of processing is specific:
1. Notify observers: Enter RunLoop.
2. Notify observers: the timer is about to be processed.
3. Notify observers: source0 is about to be processed.
4. Process source0 that is ready. If it is not processed, it will go to sleep. If it is processed
, it will not sleep. 5. If it is the runloop of the main thread and the main thread has events to process, jump to step 9. step, that is, it will not sleep; but if there are no events to be processed, it will also enter sleep.
6. Notify observers: the thread is about to sleep.
7. Make the thread sleep until any of the following events occurs.

  • source1 event occurs
  • timer trigger
  • RunLoop itself times out
  • RunLoop is woken up manually

8. Notify observers: the thread is awakened
9. Process pending events

  • If the user-defined timer fires, handle the timer and restart the loop, go to 2
  • If it is currently the runloop of the main thread, processing events of the main thread, the source is only 5
  • source1 occurs, handle the event. (If CFRunLoopRun is called, go to 2; if CFRunLoopRunInMode is called, depending on the last parameter, whether to end the loop or jump to 2 to continue the loop)
  • If the RunLoop is manually awakened and does not time out, restart the loop and go to 2

10. Notify observers: RunLoop has exited

The picture below may not be accurate, please refer to the summary above.
RunLoop processing logic
Note: There is no source0 in the green area of ​​the left picture.

more detailed version

[External link image transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the image and upload it directly (img-yysFnppD-1652108715762) (http://mrpeak.cn/images/rl00.png)] The above picture will
performTask and callout_to_observer are distinguished by different colors. From the figure, you can intuitively see how the 5 types of performTask and 6 types of callout_to_observer are distributed in a loop.

Some details are difficult to reflect in the picture, so I will explain them separately.

Poll?

Each time the loop processes the source0 task or the RunLoop times out, the poll value will be true. The direct impact is that there will be no DoObservers-BeforeWaiting and DoObservers-AfterWaiting, which means that the runloop will not go to sleep, so there will be no BeforeWaiting and AfterWaiting. These two activities.

mach_msg twice

In fact, there are two calls to mach_msg in a loop. One unmarked call occurs after DoSource0, which will actively read the msg queue related to mainQueue. However, this mach_msg call will not go to sleep because the timeout value is passed The input is 0. If the message is read, goto directly to the DoMainQueue code. This design should be to ensure that the code dispatched to the main queue always has a higher chance of running.

Port Type

Each time the runloop is awakened, it will decide which type of task to execute based on the port type. Only one of DoMainQueue, DoTimers, and DoSource1 will be run, and the rest will be left to the next loop for execution.

6.2 Sleep and wake-up of RunLoop

In fact, for Event Loop, the core thing of RunLoop is to ensure that the thread sleeps when there is no message to avoid occupying system resources, and can wake up in time when there is a message. This mechanism of RunLoop relies entirely on the system kernel, specifically Mach in Darwin, the core component of Apple's operating system (Darwin is open source). Mach can be found in the bottom Kernel in the figure below:

Mach is the core of Darwin, which can be said to be the core of the kernel. It provides basic services such as inter-process communication (IPC) and processor scheduling. In Mach, communication between processes and threads is completed in the form of messages, and messages are transmitted between two ports (this is why Source1 is called Port-based Source, because it relies on Triggered by the system sending a message to the specified port). The mach_msg() function in <mach/message.h> is used to send and receive messages (in fact, Apple provides very few Mach APIs and does not encourage us to call these APIs directly):

The essence of mach_msg() is a call to mach_msg_trap(), which is equivalent to a system call and triggers kernel state switching. When the program is at rest, RunLoop stays at __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy), and inside this function, mach_msg is called to put the program into sleep state.

__CFRunLoopServiceMachPort:

/**
 *  接收指定内核端口的消息
 *
 *  @param port        接收消息的端口
 *  @param buffer      消息缓冲区
 *  @param buffer_size 消息缓冲区大小
 *  @param livePort    暂且理解为活动的端口,接收消息成功时候值为msg->msgh_local_port,超时时为MACH_PORT_NULL
 *  @param timeout     超时时间,单位是ms,如果超时,则RunLoop进入休眠状态
 *
 *  @return 接收消息成功时返回true 其他情况返回false
 */
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {      /* In that sleep of death what nightmares may come ... */
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;  //消息头的标志位
        msg->msgh_local_port = port;  //源(发出的消息)或者目标(接收的消息)
        msg->msgh_remote_port = MACH_PORT_NULL; //目标(发出的消息)或者源(接收的消息)
        msg->msgh_size = buffer_size;  //消息缓冲区大小,单位是字节
        msg->msgh_id = 0;  //唯一id
       
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        
        //通过mach_msg发送或者接收的消息都是指针,
        //如果直接发送或者接收消息体,会频繁进行内存复制,损耗性能
        //所以XNU使用了单一内核的方式来解决该问题,所有内核组件都共享同一个地址空间,因此传递消息时候只需要传递消息的指针
        ret = mach_msg(msg,
                       MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
                       0,
                       msg->msgh_size,
                       port,
                       timeout,
                       MACH_PORT_NULL);
        CFRUNLOOP_WAKEUP(ret);
        
        //接收/发送消息成功,给livePort赋值为msgh_local_port
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        
        //MACH_RCV_TIMEOUT
        //超出timeout时间没有收到消息,返回MACH_RCV_TIMED_OUT
        //此时释放缓冲区,把livePort赋值为MACH_PORT_NULL
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        
        //MACH_RCV_LARGE
        //如果接收缓冲区太小,则将过大的消息放在队列中,并且出错返回MACH_RCV_TOO_LARGE,
        //这种情况下,只返回消息头,调用者可以分配更多的内存
        if (MACH_RCV_TOO_LARGE != ret) break;
        //此处给buffer分配更大内存
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}

mach_msg
Let’s first understand the input parameters of mach_msg:
the first parameter is to send the message content msg. The msg structure defines the ports and other contents of the message sender and receiver; the second
parameter belongs to the type of message sending or receiving. , the type has been defined through macro definition, sending is MACH_SEND_MSG, receiving is MACH_RCV_MSG; the
third parameter should be used by the receiver to apply for additional storage space to temporarily store the message, so that it can be processed by itself without coupling to the space of the source message; the
penultimate parameter Each parameter represents the waiting time. If it is 0, it means returning immediately after sending or receiving. If it is TIMEOUT_INFINITY, it will block and wait for a message. The current thread will always be in a dormant state until there is a message corresponding to the port corresponding to parameter 1. It will not return and continue to execute the following steps. code;

mach port
mentioned port many times in runloop. For example, port-based source1 is one of the wake-up sources during sleep. For example, the message __CFRunLoopServiceMachPort is also monitored during sleep through port.
So what is a port? Mach messages are passed between ports. A port can have only one receiver, but can have multiple senders at the same time. Sending a message to a port actually places the message in a message queue until the message can be processed by the receiver.
It can be seen from the source code that the port type is __CFPort /mach_port_name_t /mach_port_t, and mach_port_name_t is an unsigned integer, which is the index value of the port. There are several types of ports involved in the source code:

// 这个port就对应NSTimer;
    mach_port_t _timerPort;
    
// 这个port对应主线程
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    dispatchPort = _dispatch_get_main_queue_port_4CF();
    
// 这个port唤醒runloop
if (livePort == rl->_wakeUpPort)

Do you still remember the port collection monitored during sleep in the __CFRunLoopRun method?

// 第七步,进入循环开始不断的读取端口信息,如果端口有唤醒信息则唤醒当前runLoop
__CFPortSet waitSet = rlm->_portSet;
...
...
if (kCFUseCollectableAllocator) 
{
    memset(msg_buffer, 0, sizeof(msg_buffer));
}

// waitSet 为所有需要监听的port集合, TIMEOUT_INFINITY表示一直等待
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

The waitSet here is __CFPortSet, which is a collection of ports. So what type is __CFPortSet? What operations are involved in this collection?

typedef mach_port_t __CFPortSet;
...
...
CF_INLINE kern_return_t __CFPortSetInsert(__CFPort port, __CFPortSet portSet) {
    if (MACH_PORT_NULL == port) {
        return -1;
    }
    return mach_port_insert_member(mach_task_self(), port, portSet);
}

In other words, the type of __CFPortSet is also mach_port_t, which is an unsigned integer. Then the __CFPortSetInsert operation is guessed to be operated by bits, and different bits represent different port types. The __CFPort type parameter of __CFRunLoopServiceMachPort can also be passed into waitSet, and each bit will be traversed internally to monitor the messages of each port.
In addition, how is the set of polled ports determined during the runloop sleep phase? Through the source code, we found that it is the __CFRunLoopFindMode method that inserts each port into the waitSet:

static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create)
 {
    ...
    ...
    kern_return_t ret = KERN_SUCCESS;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    rlm->_timerFired = false;
    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
    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
    
    ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
    if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);
  
    CFSetAddValue(rl->_modes, rlm);
    CFRelease(rlm);
    __CFRunLoopModeLock(rlm);   /* return mode locked */
    return rlm;
}

From the three __CFPortSetInsert above, we can find that queuePort, _timerPort, and _wakeUpPort are inserted respectively; in addition, the port of source1 is inserted into the CFRunLoopAddSource method:

......
__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);
}
......

Coupled with the dispatchPort added in the __CFRunLoopRun method, waitSet already contains all ports that can wake up the runloop.

6.3 RunLoop timeout processing

In the core method __CFRunLoopRun of the runloop source code, dispatch is used to start a timer before entering the core do while loop.

The timeout is calculated based on seconds of the input parameter of __CFRunLoopRun. Where does the input parameter of __CFRunLoopRun come from? You can find CFRunLoopRunSpecific by following the source code. The default timeout set by the caller CFRunLoopRun is 1.0e10, and CFRunLoopRunInMode needs to be customized.

When the dispatch timer reaches the timeout, the callback function configured in dispatch_source_set_event_handler_f will be called (you can also use dispatch_source_set_event_handler to configure the block). The callback function here is __CFRunLoopTimeout. The source code is as follows:

static void __CFRunLoopTimeout(void *arg) {
    struct __timeout_context *context = (struct __timeout_context *)arg;
    context->termTSR = 0ULL;
    CFRUNLOOP_WAKEUP_FOR_TIMEOUT();// 没啥X用
    CFRunLoopWakeUp(context->rl);
    // The interval is DISPATCH_TIME_FOREVER, so this won't fire again
}
void CFRunLoopWakeUp(CFRunLoopRef rl) {
    ......
    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);
    ......
}
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;
    ......
    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;
}

From the source code above, we can see that when the timeout arrives, the main thing to do is to call mach_msg to send the message through __CFSendTrivialMachMessage. The mach_msg parameter has been configured with "send mode", "timeout time", and the wake-up port is "rl->_wakeUpPort", based on From the article "Do you understand runloop correctly?", we can know that when runloop is sleeping, when it receives a message from mach, it will judge the port and decide what judgment and processing to make:

if (MACH_PORT_NULL == livePort)
{
      CFRUNLOOP_WAKEUP_FOR_NOTHING();
}
else if (livePort == rl->_wakeUpPort)
{
      CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort)
{
      // 处理timer
}
else if (livePort == dispatchPort) 
{
      ......
      // 处理主线程队列中事件
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
      ......
}
else 
{
      ......
      // 处理Source1
      sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
      ......
}

That is to say, we can use the dispatch timer to pass the timeout message through mach, wake up the runloop, and then execute the else if (livePort == rl->_wakeUpPort) branch to handle the timeout (CFRUNLOOP_WAKEUP_FOR_WAKEUP in the current source code is just an empty macro definition and has not been done. any processing). So this timer is used to set context->termTSR = 0ULL and wake up the runloop after the specified time. In the following code, context->termTSR is used to determine whether it times out, and if it times out, exit RunLoop.

###6.4 Observer of RunLoop
The so-called Runloop, in short, is a running mechanism designed by Apple that continuously schedules various tasks in the current thread.

Each time the loop is executed, it mainly does three things:

  • callout_to_observer()
  • sleep()
  • performTask()

We have already discussed sleep before, and we will continue to discuss performTask in the later part. This part mainly discusses the observer.

We already know that RunLoop has six states. Runloop uses callout_to_observer to notify the external observer that a certain external task has been executed, or what state the runloop is currently in.

DoObservers-Timer

As the name suggests, DoObservers-Timer is called to inform interested observers before DoTimers is executed. runloop notifies the observer through the following function:

__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

DoObservers-Source0

In the same way, DoObservers-Sources is called to inform interested observers before executing source0. runloop notifies the observer through the following function:

__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

Among the above five ways of executing tasks, two of them can register the observer, but the others are not supported, including mainQueue, source1, and block. But for now, it is uncertain whether DoTimers will be executed after the kCFRunLoopBeforeTimers notification is sent.

DoObservers-Activity
is used by the runloop to notify the outside world of its current status. Which activity is the runloop currently executing? How many activities are there in total? See the source code for clarity:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Without further ado, let me explain one by one:

kCFRunLoopEntry

Every time the runloop re-enters the activity, every time the runloop enters a mode, it notifies the external kCFRunLoopEntry once, and then it will continue to run in that mode until the current mode is terminated, then switch to other modes, and notify kCFRunLoopEntry again.

kCFRunLoopBeforeTimers

This is the DoObservers-Timer mentioned above. Apple probably classified timer callout as an activity for the sake of code neatness. Its meaning has been introduced above and will not be repeated.

kCFRunLoopBeforeSources

Similarly, Apple classifies the source callout as a runloop activity.

kCFRunLoopBeforeWaiting

This activity indicates that the current thread may enter sleep. If the msg can be read from the kernel queue, the task will continue to run. If there are no extra messages on the current queue, it will enter sleep state. The function to read msg is:

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY);

The essence is that the mach_msg kernel function is called. Pay attention to the timeout value. TIMEOUT_INFINITY indicates that it is possible to enter the sleep state indefinitely.

kCFRunLoopAfterWaiting

This activity is when the current thread resumes from sleep state, which means that the above mach_msg finally reads msg from the queue and can continue to perform tasks. This is an activity that must be called every time the runloop resumes from the idle state. If you want to design a tool to detect the execution cycle of the runloop, then this activity can be used as the beginning of the cycle.

kCFRunLoopExit

exit Needless to say, this activity will be called when exiting the RunLoop of the current mode.

Activity callbacks are not only for developers. In fact, the system will also complete some tasks by registering callbacks for related activities. For example, I have seen the following callstack:

...
__CFRunLoopDoObservers
...
[UIView(Hierarchy) addSubview:] 
...

Obviously, after the system observes that the runloop enters an activity, it will perform some UIView layout work.

Look at this again:

...
__CFRunLoopDoObservers
...
[UIViewController __viewWillDisappear:] 
...

This is the system using DoObservers to pass the viewWillDisappear callback.

###6.5 RunLoop task processing

We have discussed callout_to_observer and sleep before, and we have also talked about performTask before. Here we continue to analyze these Tasks. From the source code we can see a type of Do function:

__CFRunLoopDoBlocks(内部调用callout_to_block)
__CFRunLoopDoSources0(内部循环调用callout_to_source0_perform_function)
__CFRunLoopDoSource1(内部调用callout_to_source1_perform_function)
__CFRunLoopDoTimers(内部调用__CFRunLoopDoTimer)

__CFRunLoopDoObservers(观察者,非任务,内部callout_to_observer)

下面这个不是Do系列的,但是因为和callout系列并列,是任务的一种,也列出来
_dispatch_main_queue_callback_4CF(被servicing_main_dispatch_queue所调用)

DoBlocks()
can be used by developers and is very simple to use. You can first insert a block into the target queue through CFRunLoopPerformBlock. The function signature is as follows:

voidCFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void(^block)( void));

For detailed usage, please refer to the document: https://developer.apple.com/documentation/corefoundation/1542985-cfrunloopperformblock?language=objc

It can be seen that when the block is inserted into the queue, it is bound to a certain runloop mode. After calling the above API, when runloop is executed, it will execute all blocks in the queue through the following API:

__CFRunLoopDoBlocks(rl, rlm);

Obviously, only all blocks related to a certain mode are executed during execution. As for the timing of execution, there are many points, which will also be marked later.

DoSources0()
We know that source is used to generate asynchronous events. There are two sources in Runloop, source0 and source1. Although the names are similar, the operating mechanisms of the two are different. Source0 needs to be manually managed by the application. When source0 is ready to trigger, CFRunLoopSourceSignal needs to be called to tell RunLoop that it is ready to trigger. CFSocket is currently implemented as source0.

For detailed documentation, please refer to: https://developer.apple.com/documentation/corefoundation/1542679-cfrunloopsourcecreate?language=objc

After binding, when runloop is executed, it will execute all source0 through the following API:

__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

In the same way, every time it is executed, only source0 related to the current mode will be run.

DoSource1()
source1 is managed by RunLoop and the kernel. The implementation principle of source1 is based on the mach_msg function. When a message arrives at the Mach port of source1, the kernel will actively send a signal and determine the task to be executed by reading the message on the kernel message queue on a certain port. CFMachPort and CFMessagePort are implemented with source1.

For detailed documentation, please refer to: https://developer.apple.com/documentation/corefoundation/1542679-cfrunloopsourcecreate?language=objc

After binding, when runloop is executed, it will execute a certain source1 through the following API:

__CFRunLoopDoSource1(rl, rlm, stopAfterHandle);

In the same way, every time it is executed, only source0 related to the current mode will be run.

DoTimers()

This is relatively simple. Developers can use NSTimer related APIs to register the tasks to be executed. Runloop performs related tasks through the following APIs:

__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());

In the same way, every time it is executed, only the timer related to the current mode will be run.

DoMainQueue()

Developers call GCD's API to put tasks into the main queue, and runloop executes scheduled tasks through the following API:

_dispatch_main_queue_callback_4CF(msg);

Note that there is no rlm parameter here, which means that DoMainQueue and runloop mode are irrelevant. msg is the msg read from a certain port through the mach_msg function.

Earlier we listed five ways to perform tasks in RunLoop. It can be seen that Apple will use them in different scenarios. We will not summarize here, but simply list Apple’s examples:

source0:
...
__CFRunLoopDoSources0     
...
[UIApplication sendEvent:] 
...
显然是系统用 source0 任务来接收硬件事件。
...
__CFRunLoopDoSources0
...
CA::Transaction::commit() 
...
这是系统在使用 doSource0 来提交 Core Animation 的绘制任务。

mainqueue:
...
_dispatch_main_queue_callback_4CF
...
[UIView(Hierarchy) _makeSubtreePerformSelector:withObject:withObject:copySublayers:]
...
系统在使用 doMainQueue 来执行 UIView 的布局任务。

timer:
...
__CFRunLoopDoTimer
...
[UICollectionView _updateWithItems:tentativelyForReordering:animator:]
...
这是系统在使用 doTimers 来 UI 绘制任务。

doBlocks:
...
__CFRunLoopDoBlocks
...
CA::Context::commit_transaction(CA::Transaction*)
...
这是系统在使用 doBlocks 来提交 Core Animation 的绘制任务。

7. Functions implemented by Apple using RunLoop

7.1 AutoreleasePool

After the App starts, Apple registers two Observers in the main thread RunLoop, and their callbacks are all _wrapRunLoopWithAutoreleasePoolHandler().

The first event monitored by the Observer is Entry (about to enter the Loop), and _objc_autoreleasePoolPush() will be called in its callback to create an automatic release pool. Its order is -2147483647, which has the highest priority and ensures that the creation of the release pool occurs before all other callbacks.

The second Observer monitors two events: _objc_autoreleasePoolPop() and _objc_autoreleasePoolPush() are called when BeforeWaiting (preparing to enter sleep) to release the old pool and create a new pool; _objc_autoreleasePoolPop() is called when Exit (about to exit the Loop) to release Autorelease pool. The order of this Observer is 2147483647, with the lowest priority, ensuring that its release pool occurs after all other callbacks.

Code executed on the main thread is usually written in event callbacks and Timer callbacks. These callbacks will be surrounded by the AutoreleasePool created by RunLoop, so there will be no memory leaks and developers do not have to explicitly create the Pool.

7.2 Incident response

Apple has registered a Source1 (based on mach port) to receive system events, and its callback function is __IOHIDEventSystemClientQueueCallback().

When a hardware event (touch/lock screen/shake, etc.) occurs, an IOHIDEvent event is first generated by IOKit.framework and received by SpringBoard. Details of this process can be found here. SpringBoard only receives several events such as button presses (lock screen/mute, etc.), touch, acceleration, proximity sensor, etc., and then forwards them to the required App process using mach port. Then the Source1 registered by Apple will trigger the callback and call _UIApplicationHandleEventQueue() for distribution within the application.

_UIApplicationHandleEventQueue() will process and package IOHIDEvent into UIEvent for processing or distribution, including identifying UIGesture/processing screen rotation/sending to UIWindow, etc. Usually events such as UIButton clicks and touchesBegin/Move/End/Cancel events are completed in this callback.

7.3 Gesture recognition

When the above _UIApplicationHandleEventQueue() recognizes a gesture, it will first call Cancel to interrupt the current touchesBegin/Move/End series callbacks. The system then marks the corresponding UIGestureRecognizer as pending.

Apple has registered an Observer to monitor the BeforeWaiting (Loop is about to go to sleep) event. The callback function of this Observer is _UIGestureRecognizerUpdateObserver(), which internally obtains all GestureRecognizers that have just been marked as pending and executes the GestureRecognizer callback.

When there are changes to UIGestureRecognizer (creation/destruction/status change), this callback will be processed accordingly.

7.4 Interface update

When operating the UI, such as changing the Frame, updating the hierarchy of UIView/CALayer, or manually calling the setNeedsLayout/setNeedsDisplay method of UIView/CALayer, the UIView/CALayer is marked as pending and submitted to a Go to the global container.

Apple has registered an Observer to listen for BeforeWaiting (about to enter sleep) and Exit (about to exit the Loop) events, and callback to execute a long function:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(). This function will traverse all pending UIView/CAlayers to perform actual drawing and adjustment, and update the UI interface.

The call stack inside this function probably looks like this:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

7.5 timer

NSTimer is actually CFRunLoopTimerRef, and they are toll-free bridged. After an NSTimer is registered to RunLoop, RunLoop will register events for its repeated time points. For example, these time points are 10:00, 10:10, and 10:20. In order to save resources, RunLoop will not call back this Timer at a very accurate time point. Timer has an attribute called Tolerance, which indicates the maximum error allowed when the time point is reached.

If a certain time point is missed, for example, a long task is executed, the callback at that time point will also be skipped and the execution will not be delayed. Just like waiting for the bus, if I am busy playing with my phone at 10:10 and miss the bus at that time, then I can only wait for the 10:20 bus.

CADisplayLink is a timer consistent with the screen refresh rate (but the actual implementation principle is more complex and different from NSTimer. It actually operates a Source internally). If a long task is executed between two screen refreshes, one frame will be skipped (similar to NSTimer), causing the interface to feel stuck. When sliding the TableView quickly, even one frame of lag will be noticed by the user. Facebook's open source AsyncDisplayLink is to solve the problem of interface lag, and RunLoop is also used internally.

7.6 PerformSelector

When NSObject's performSelector:afterDelay: is called, a Timer will actually be created internally and added to the RunLoop of the current thread. So if the current thread does not have a RunLoop, this method will fail.

When performSelector:onThread: is called, it will actually create a source0 and add it to the corresponding thread. Similarly, if the corresponding thread does not have a RunLoop, this method will also fail.

7.7 About GCD

In fact, GCD is also used at the bottom of RunLoop. For example, RunLoop can implement Timer through dispatch_source_t (despite the fact that NSTimer is implemented preferentially using mk_timer of the XNU kernel, the source code also includes ways to implement it using GCD). At the same time, some interfaces provided by GCD also use RunLoop, such as dispatch_async().

When dispatch_async(dispatch_get_main_queue(), block) is called, libDispatch will send a message to the RunLoop of the main thread. The RunLoop will wake up, obtain the block from the message, and execute the block in the callback CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE ( ) . But this logic is limited to dispatch to the main thread, and dispatch to other threads is still handled by libDispatch.

7.8 About network requests

In iOS, the interface for network requests has the following layers from bottom to top:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire

CFSocket is the lowest-level interface and is only responsible for socket communication.
• CFNetwork is an upper-layer encapsulation based on interfaces such as CFSocket, and ASIHttpRequest works on this layer.
• NSURLConnection is a higher-level encapsulation based on CFNetwork, providing an object-oriented interface. AFNetworking 1.0 works at this layer.
• NSURLSession is a new interface in iOS7. On the surface, it is parallel to NSURLConnection, but the bottom layer still uses some functions of NSURLConnection (such as com.apple.NSURLConnectionLoader thread). AFNetworking 2, 3 and Alamofire work at this layer.

The following mainly introduces the working process of NSURLConnection.

Usually when using NSURLConnection, you will pass in a Delegate. When [connection start] is called, this Delegate will continue to receive event callbacks. In fact, the start function internally obtains the CurrentRunLoop, and then adds 4 Source0 (that is, the Source that needs to be triggered manually) in the DefaultMode. CFMultiplexerSource is responsible for various Delegate callbacks, and CFHTTPCookieStorage is responsible for handling various Cookies.

When the network transfer starts, we can see that NSURLConnection creates two new threads: com.apple.NSURLConnectionLoader and com.apple.CFSocket.private. The CFSocket thread handles the underlying socket connection. NSURLConnectionLoader This thread will use RunLoop internally to receive events from the underlying socket and notify the upper Delegate through the previously added Source0.

The RunLoop in NSURLConnectionLoader receives notifications from the underlying CFSocket through some mach port-based Source. After receiving the notification, it will send notifications to Source0 such as CFMultiplexerSource at the appropriate time, and at the same time wake up the RunLoop of the Delegate thread to let it process these notifications. CFMultiplexerSource will perform the actual callback to the Delegate in the RunLoop of the Delegate thread.

8. Practical application examples of RunLoop

8.1 AFNetworking

AFURLConnectionOperation This class is built based on NSURLConnection, which hopes to receive Delegate callbacks in the background thread. AFNetworking created a separate thread for this purpose and started a RunLoop in this thread:

+ (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;
}

There must be at least one Timer/Observer/Source inside before RunLoop starts, so AFNetworking creates a new NSMachPort and adds it before [runLoop run]. Normally, the caller needs to hold this NSMachPort (mach_port) and send messages to the loop through this port in an external thread; but the port is added here just to prevent RunLoop from exiting, and is not used to actually send messages.

- (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];
}

When this background thread is required to perform a task, AFNetworking throws the task into the background thread's RunLoop by calling [NSObject performSelector:onThread:...].

8.2 AsyncDisplayKit

AsyncDisplayKit is a framework launched by Facebook to maintain interface fluency. Its principle is roughly as follows:

Once heavy tasks occur in the UI thread, the interface will become stuck. Such tasks are usually divided into three categories: typesetting, drawing, and UI object manipulation.

Typesetting usually includes operations such as calculating view size, calculating text height, and recalculating the typesetting of subformula diagrams.
Drawing generally includes text drawing (such as CoreText), image drawing (such as pre-decompression), element drawing (Quartz) and other operations.
UI object operations usually include the creation, setting properties and destruction of UI objects such as UIView/CALayer.

The first two types of operations can be thrown to the background thread for execution through various methods, while the last type of operation can only be completed on the main thread, and sometimes subsequent operations need to rely on the results of previous operations (for example, when a TextView is created, the text may need to be calculated in advance. the size of). What ASDK does is to try to put tasks that can be put into the background into the background, and try to postpone those that cannot (such as view creation and attribute adjustment).

To this end, ASDK creates an object named ASDisplayNode and encapsulates UIView/CALayer internally. It has properties similar to UIView/CALayer, such as frame, backgroundColor, etc. All these properties can be changed in the background thread. Developers can only operate the internal UIView/CALayer through Node, so that typesetting and drawing can be put into the background thread. But no matter how you operate, these properties always need to be synchronized to the UIView/CALayer of the main thread at some point.

ASDK follows the pattern of the QuartzCore/UIKit framework and implements a similar interface update mechanism: that is, adding an Observer in the RunLoop of the main thread, listening to the kCFRunLoopBeforeWaiting and kCFRunLoopExit events, and when receiving the callback, iterates through all the previously put into the queue of pending tasks and then execute them one by one.
The specific code can be seen here: _ASAsyncTransactionGroup.

I have seen a lot of RunLoop system applications and the use of some well-known third-party libraries. So besides these, can we properly use RunLoop to help us do something during the actual development process?

8.3 Others

To think about this problem, you only need to look at the inclusion relationship of RunLoopRef. RunLoop contains multiple Modes, and its Mode can be customized. It can be inferred that whether it is Source1, Timer or Observer developers can use it, but usually In this case, the Timer will not be customized, let alone a complete Mode. What is more utilized is actually the switching between Observer and Mode.
For example, many people are familiar with using performSelector to set images in default mode to prevent UITableView from scrolling ([[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode]). There is also sunnyxx's UITableView+FDTemplateLayoutCell that uses Observer to calculate the height of UITableViewCell and cache it when the interface is idle. Lao Tan’s PerformanceMonitor also uses Observer to monitor RunLoop in real-time for iOS lag monitoring.

The content of this article comes from the summary of the following blog post:

iOS in-depth investigation - in-depth understanding of RunLoop https://blog.ibireme.com/2015/05/18/runloop/
iOS development - a lengthy explanation of Runloop https://www.cnblogs.com/CrazyD0u/p/6481092.html
The underlying principles of iOS Summary - RunLoop https://www.jianshu.com/p/de752066d0ad
Detailed explanation of iOS RunLoop https://www.jianshu.com/p/23e3ff9619c3
https://www.jianshu.com/p/18e45cbd564f
There are many people about runloop They all got it wrong! https://www.jianshu.com/p/ae0118f968bf
Runloop source code interpretation notes https://www.jianshu.com/p/288e8abc80f1
Decryption of Runloop http://mrpeak.cn/blog/ios-runloop/

Recommended reading:

iOS runloop study notes (1) - Official document https://www.jianshu.com/p/8e40a7b16357
iOS runloop study notes (2) - sunnyxx master video https://www.jianshu.com/p/929d855c5a5a
iOS runloop study Notes (3) - Summary of Yanzu Ye Gucheng's video https://www.jianshu.com/p/924cb2b218f5
iOS runloop study notes (4) - Summary https://www.jianshu.com/p/6a41cd354dc6
Some things about performSelector Small discussion https://juejin.im/post/5c637a1ff265da2dbc596ec2
runloop nesting understanding https://www.jianshu.com/p/318328c0a2d1
multi-threading - abandoned thread http://sindrilin.com/2018/04/14 /abandoned_thread.html

Guess you like

Origin blog.csdn.net/Mamong/article/details/124677766