1.RunLoop structure model

1. The concept

A RunLoop is an object that manages events/messages through an internally maintained Event Loop.

  1. When there is no message processing, sleep is performed to avoid resource usageUser modeSwitch to theKernel mode(CPU-kernel mode and user mode)
  2. When a message needs to be processed, it is immediately woken up byKernel modeSwitch to theUser mode

The Event Loop model

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

OSX/iOS provides two such objects: NSRunLoop and CFRunLoopRef. CFRunLoopRef is within the CoreFoundation framework and provides apis for pure C functions, all of which are thread-safe. NSRunLoop is a wrapper based on CFRunLoopRef that provides object-oriented apis, but these apis are not thread-safe. The CFRunLoopRef code is open source, and you can download the entire CoreFoundation source code here.

2.RunLoopThe data structure

NSRunLoop(Foundation) is an encapsulation of CFRunLoop(CoreFoundation) and provides an object-oriented API

Structure is as follows

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

Copy the code

A RunLoop object consists of one thread, several modes, several commonModes, and one currently running Mode.

RunLoopThere are five main categories involved:

  • CFRunLoopObject: the RunLoop
  • CFRunLoopMode: Operating mode
  • CFRunLoopSource: Input source/event source
  • CFRunLoopTimer: timing source, time – based trigger
  • CFRunLoopObserver: An observer that listens for the runLoop status

2.1.CFRunLoop

CFRunLoopIt consists of five parts:

  1. pthread: thread object, descriptionRunLoopthreadOne to one correspondence
  2. currentMode: Indicates the current running mode
  3. modes: A collection of multiple operating modes
  4. commonModes: a collection of schema name strings
  5. commonModelItems:Observer,Timer,SourceA collection of
typedef struct __CFRunLoop * CFRunLoopRef; Struct __CFRunLoop {//... CFMutableSetRef _commonModes; // <Set> String UITrackingRunLoopMode/kCFRunLoopDefaultMode CFMutableSetRef _commonModeItems; // <Set> observer/source/timer CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; // Built-in modes; / /... };Copy the code

2.2.CFRunLoopMode

Consisting of name, source0, source1, observers, and timers

///CFRunLoop.c typedef struct __CFRunLoopMode *CFRunLoopModeRef; struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; /* must have the run loop locked before locking this */ CFStringRef _name; //mode name Boolean _stopped; // Whether mode is terminated char _padding[3]; CFMutableSetRef _sources0; //sources0 CFMutableSetRef _sources1; //sources1 CFMutableArrayRef _observers; // Notify CFMutableArrayRef _timers; // CFMutableDictionaryRef _portToV1SourceMap; // the dictionary key is mach_port_t and the value is CFRunLoopSourceRef __CFPortSet _portSet; _wakeUpPort and _timerPort are stored in the array CFIndex _observerMask; #if USE_DISPATCH_SOURCE_FOR_TIMERS dispatch_source_t _timerSource; dispatch_queue_t _queue; Boolean _timerFired; // set to true by the source when a timer has fired Boolean _dispatchTimerArmed; #endif #if USE_MK_TIMER_TOO mach_port_t _timerPort; Boolean _mkTimerArmed; #endif #if DEPLOYMENT_TARGET_WINDOWS DWORD _msgQMask; void (*_msgPump)(void); #endif uint64_t _timerSoftDeadline; /* TSR */ uint64_t _timerHardDeadline; /* TSR */ };Copy the code

There are five modes mentioned in apple documentation, which are:

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

The only publicly exposed modes in iOS are NSDefaultRunLoopMode and NSRunLoopCommonModes. NSRunLoopCommonModes is actually a collection of the Mode, including the default NSDefaultRunLoopMode and NSEventTrackingRunLoopMode (note: This does not mean that Runloop will run in kCFRunLoopCommonModes, but rather that it will register NSDefaultRunLoopMode and UITrackingRunLoopMode respectively. You can also add custom modes to the kCFRunLoopCommonModes group by calling CFRunLoopAddCommonMode().

/// 'RunLoop' specifies' Mode 'to run CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode Mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);Copy the code

During the running of the program, events based on time, system and user are processed. These events have different priorities during the running of the program. In order to meet the application layer’s management of these events according to their priorities, the system uses RunLoopMode to group these events and then sends them to RunLoop for management. In addition to the default and common schemas defined by the system, we can also customize the schema, but the custom schema must have associated events, otherwise the custom schema is meaningless.

_commonModeItems and _commonModes are the implementation logic behind kCFRunLoopCommonModes (NSRunLoopCommonModes). Once again, we expect some events to be handled by multiple modes simultaneously, so we place these events (sources/timers/observers) in _commonModeItems, and mark multiple modes that need to be handled simultaneously in the _commonModes set. When events specify kCFRunLoopCommonModes to be added, they are first added to _commonModeItems, and then all events in _commonModeItems are appended to the modes already marked in _commonModes.

// add a 'Mode' to RunLoop's 'commonMode' set. Once added, it cannot be removed. Void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);Copy the code

For example, the main thread runs in kCFRunLoopDefaultMode by default. When we swipe ScrollView, we switch to UITrackingRunLoopMode. The _commonModes of the main thread RunLoop include both modes by default. In the development of the main thread will encounter a timer, will be affected by the view sliding problem, solution:

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
Copy the code

Adding timers to the NSRunLoopCommonModes of the NSRunLoop object will eventually add timers to kCFRunLoopDefaultMode and UITrackingRunLoopMode. Of course, you can also add them to both modes.

To understand how _commonModeItems and _commonModes work, see CoreFoundation CFRunLoopAddTimer.

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) { ///... If (modeName == kCFRunLoopCommonModes) {// If 'kCFRunLoopCommonModes' // Take' _commonModes' of 'Runloop' CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; If (NULL == rl->_commonModeItems) {create '_commonModeItems' <Set> rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); } // add timer to '_commonModeItems' CFSetAddValue(rl->_commonModeItems, RLT); if (NULL ! = set) {// '_commonModes' have values CFTypeRef Context [2] = {rl, RLT}; / * the add new item to all common modes - * / / / / call the method for each element of the Set Set CFSetApplyFunction (Set, (__CFRunLoopAddItemToCommonModes), (void *)context); CFRelease(set); }} else {/// not 'kCFRunLoopCommonModes', check whether' runloop 'modes have any, CFRunLoopModeRef RLM = __CFRunLoopFindMode(rl, modeName, true); if (NULL ! If (NULL == timers ->_timers) {CFArrayCallBacks cb = kCFTypeArrayCallBacks; cb.equal = NULL; rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb); }} // The timer object has modes of this mode if (NULL! = rlm && ! CFSetContainsValue(rlt->_rlModes, rlm->_name)) { ///... if (NULL == rlt->_runLoop) { rlt->_runLoop = rl; } else if (rl ! = rlt->_runLoop) { //... // The runloop associated with the timer is inconsistent with the current runloop. } // add the mode name CFSetAddValue(RLT ->_rlModes, RLM ->_name); __CFRepositionTimerInMode(RLM, RLT, false); / / /... } / / /... } / / /.. } static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) { CFStringRef modeName = (CFStringRef)value;  CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]); CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]); if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) { CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName); //add source } else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) { CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName); // add observer } else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) { CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName); // add timer } }Copy the code

The CoreFoundation functions that add and remove events to and from the specified RunLoopMode are:

//Source
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
//Timer
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
//Observer
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
Copy the code

2.3.CFRunLoopSource

Structure is as follows

struct __CFRunLoopSource { CFRuntimeBase _base; uint32_t _bits; pthread_mutex_t _lock; CFIndex _order; CFMutableBagRef _runLoops; union { CFRunLoopSourceContext version0; //source0 CFRunLoopSourceContext1 version1; //source1 } _context; }; typedef struct { CFIndex version; 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; typedef struct { ///... 7 properties before with ` CFRunLoopSourceContext ` # if TARGET_OS_OSX | | TARGET_OS_IPHONE mach_port_t getPort (*) (void * info); void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info); #else ///... #endif } CFRunLoopSourceContext1; typedef struct __CFRunLoopSource * CFRunLoopSourceRef;Copy the code

Divided intosource0source1Two kinds of

  1. source0:
    • namelyNot based on the portOf, that isEvents triggered by the user. Need to beWake up the thread manuallyTo switch the current thread from kernel mode to user mode
  2. source1:
    • Based on the portOf, including onemach_portAnd aThe callback, can listen to system ports and messages sent through the kernel and other threadsActively wake up RunLoopTo receive distribution system events.
    • haveWake up the threadThe ability to

2.4.CFRunLoopTimer

A time-based trigger is basically an N-stimer (CFRunLoopTimer and N-stimer are toll-free bridged and can be exchanged). Wake up RunLoop at a preset point in time to perform a callback.

Since it is based on RunLoop, it is not real-time (except that NSTimer is inaccurate because RunLoop is only responsible for distributing messages from the source. If the thread is currently working on a heavy task, the Timer may be delayed or executed less than once.

Structure is as follows

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

2.5.CFRunLoopObserver

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

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

Copy the code

The CFRunLoopObserver can observe the following six states:

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

3.RunLoopInternal logic

RunloopBy monitoring theSourceTo determine if there’s any work to be done, and besides that, we can useRunloop ObserverTo monitorRunloopIts own state.Runloop ObserverWe can monitor the ones up thereRunloopEvent, the specific process is shown below.

The order is as follows:

  1. Notifies the observer that RunLoop has started
  2. A timer that notifies the observer that it is about to start
  3. Notifies the observer of any non-port-based sources that are about to start
  4. Start any prepared non-port-based sources
  5. If the port-based source is ready and in a waiting state, start immediately and go to Step 9
  6. Notifies the observer that the thread is asleep
  7. The thread is put to sleep until one of the following events occurs:
    • An event reaches a port-based source
    • Timer start
    • The time set by RunLoop has timed out
    • RunLoop is shown waking up
  8. Notify the observer that the thread will be awakened
  9. Handle unprocessed events
    • If the user-defined timer is started, process the timer event and restart RunLoop to go to Step 2
    • If the input source is started, the corresponding message is passed
    • If RunLoop is awakened and the time has not expired, restart the RunLoop. Enter Step 2.
  10. Notifies the observer that RunLoop ends.

3.1.RunLoopStart the

Void CFRunLoopRun(void) {CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode, 1.0e10, false); } // start with the specified Mode, RunLoop timeout allowed int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); }Copy the code

Runloop always runs in some particular CFRunLoopModeRef (Mode must be specified every time __CFRunLoopRun() is run), which is set to _currentMode, Only the input sources, source0 and source1, associated with this Mode, can be processed. Similarly, observers of RunLoop are astounded, and only observers associated with this Mode are notified.

3.2.CFRunLoopRunSpecific

/* * Implementation of RunLoop, Runloop * @param rl Specifies the current runloop * @param modeName Specifies the name of the mode to be run * @param seconds Specifies the runloop timeout * @param Return */ int CFRunLoopRunSpecific(runloop, modeName, seconds, CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); / / if you don't find | | mode not registered in any event, the end, Not to circulate the if (NULL = = currentMode | | __CFRunLoopModeIsEmpty (rl, currentMode, rl - > _currentMode)) {Boolean did = false; if (currentMode) __CFRunLoopModeUnlock(currentMode); __CFRunLoopUnlock(rl); return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); CFRunLoopModeRef previousMode = rl->_currentMode; Rl ->_currentMode = currentMode; Int32_t result = kCFRunLoopRunFinished; /// 1. Notify Observers: RunLoop is about to enter loop. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); // the internal function, Loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {Boolean sourceHandledThisLoop = NO; int retVal = 0; Do {/// 2. Notify Observers: RunLoop is about to trigger a Timer callback. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); /// 3. notify Observers: RunLoop is about to trigger the Source0 (non-port) callback. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); // Execute the added block __CFRunLoopDoBlocks(runloop, currentMode); /// 4. RunLoop triggers the Source0 (non-port) callback. sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); // Execute the added block __CFRunLoopDoBlocks(runloop, currentMode); /// 5. If there is a port based Source1 in ready state, process the Source1 directly and jump to the message. if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; } /// 6. Notify Observers: RunLoop threads that are about to enter sleep. if (! sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); } /// 7. Call mach_msg and wait for the message to accept mach_port. The thread will sleep until it is awakened by one of the following events. /// • A port-based Source event. __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) { mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg } /// 8. Notify Observers: The threads of RunLoop have just been awakened. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting); /// 9. Process the message after receiving it. Handle_msg: /// 9.1 If a Timer runs out, trigger the Timer's callback. if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, Mach_absolute_time ())} // 9.2 If there are blocks dispatched to main_queue, execute the block. else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } /// 9.3 If a Source1 (port based) emits an event, To deal with this event else {CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort (runloop currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); } // execute the block __CFRunLoopDoBlocks(runloop, currentMode) added to the Loop; If (sourceHandledThisLoop && stopAfterHandle) {/// return when the loop is finished. retVal = kCFRunLoopRunHandledSource; RetVal = kCFRunLoopRunTimedOut;} else if (timeout) {retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(runloop)) {retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(runloop, RetVal = kCFRunLoopRunFinished; } // If there is no timeout, mode is not available and loop is not stopped, continue loop. } while (retVal == 0); } /// 10. Notify Observers: RunLoop is about to exit. __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); } __CFRunLoopModeIsEmpty() { if (NULL ! = rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; if (NULL ! = rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; if (NULL ! = rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; return true; }Copy the code

Through the internal logic of CFRunLoopRunSpecific, we can conclude:

  • If you specify oneMode does not existtoRun the RunLoop, then will fail,mode Don'tbecreateSo here’s the incomingmodeIt must beThere arethe
  • If you specify onemodeBut thismodeThe RunLoop will not run if the mode does not contain any modeItem, so you must * pass in a mode that contains at least one modeItem
  • Notify the Observer before entering runloop that the status is kCFRunLoopEntry
  • Notify the Observer of the status kCFRunLoopExit after exiting runloop

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

3.3.__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;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
        //取所有需要监听的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //设置RunLoop为可以被唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        //2.通知observer,即将触发timer回调,处理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3.通知observer,即将触发Source0回调
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //执行加入当前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //4.处理source0事件
        //有事件处理返回true,没有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //执行加入当前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //如果没有Sources0事件处理 并且 没有超时,poll为false
        //如果有Sources0事件处理 或者 超时,poll都为true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        //第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //从缓冲区读取消息
            msg = (mach_msg_header_t *)msg_buffer;
            //5.接收dispatchPort端口的消息,(接收source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                //如果接收到了消息的话,前往第9步开始处理msg
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }
        
        didDispatchPortLastTime = false;
        
        //6.通知观察者RunLoop即将进入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //设置RunLoop为休眠状态
        __CFRunLoopSetSleeping(rl);
        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //这里有个内循环,用于接收等待端口的消息
        //进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            //7.接收waitSet端口的消息
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            //收到消息之后,livePort的值为msg->msgh_local_port,
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.
        
        __CFPortSetRemove(dispatchPort, waitSet);
        
 
        __CFRunLoopSetIgnoreWakeUps(rl);
        
        // user callouts now OK again
        //取消runloop的休眠状态
        __CFRunLoopUnsetSleeping(rl);
        //8.通知观察者runloop被唤醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
      
        //9.处理收到的消息
    handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);
        
#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            
            // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
            // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
            // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
            __CFRunLoopSetSleeping(rl);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            __CFRunLoopUnsetSleeping(rl);
            // If we have a new live port then it will be handled below as normal
        }
        
        
#endif
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
            //通过CFRunloopWake唤醒
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            //什么都不干,跳回2重新循环
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //如果是定时器事件
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            //9.1 处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        //如果是定时器事件
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
           //9.1处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        //如果是dispatch到main queue的block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            //9.2执行block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            // 有source1事件待处理
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
                mach_msg_header_t *reply = NULL;
                //9.2 处理source1事件
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                if (NULL != reply) {
                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
                }
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
            }
        }
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
        __CFRunLoopDoBlocks(rl, rlm);
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            //进入run loop时传入的参数,处理完事件就返回
            retVal = kCFRunLoopRunHandledSource;
        }else if (timeout_context->termTSR < mach_absolute_time()) {
            //run loop超时
            retVal = kCFRunLoopRunTimedOut;
        }else if (__CFRunLoopIsStopped(rl)) {
            //run loop被手动终止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        }else if (rlm->_stopped) {
            //mode被终止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        }else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //mode中没有要处理的事件
            retVal = kCFRunLoopRunFinished;
        }
        //除了上面这几种情况,都继续循环
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    return retVal;
}
Copy the code

The source code isn’t too long, but it can be a bit of a mystery if you’re not familiar with a bunch of function calls you don’t know what to do. But instead of reading line by line, you can slowly unravel the mystery. The core __CFRunLoopRun method is actually an _do while_ loop, which is exactly how Runloop is run. After executing this function, we are in a wait-and-process loop until the end of the loop. Unlike the loop we wrote ourselves, it uses almost no system resources when it sleeps, because of course the kernel is responsible for implementing it, which is the essence of Runloop

3.4.__CFRunLoopServiceMachPort

This function plays a crucial role in runloop

/** * receives messages from the specified kernel port ** @param port Port that receives messages * @param buffer message buffer * @param buffer_size Message buffer size * @param livePort MSG -> msGH_LOCAL_port if the message is received successfully; MACH_PORT_NULL * @param timeout If the message is timed out, the unit is ms. * * @return Returns true if the message is received successfully 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; // unique id if (TIMEOUT_INFINITY == timeout) {CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); } // All messages sent and received through mach_msg are Pointers. // If you send or receive messages directly, memory replication will occur frequently, resulting in performance loss. // So XNU uses a single kernel approach to solve this problem. So when the message is only need to pass the message pointer 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); Msgh_local_port if (MACH_MSG_SUCCESS == ret) {*livePort = MSG? msg->msgh_local_port : MACH_PORT_NULL; return true; } mach_rcv_out MACH_RCV_TIMED_OUT mach_rcv_out Set livePort to MACH_PORT_NULL if (MACH_RCV_TIMED_OUT == ret) {if (! originalBuffer) free(msg); *buffer = NULL; *livePort = MACH_PORT_NULL; return false; } // If the receive buffer is too small, the MACH_RCV_LARGE message is placed in the queue and MACH_RCV_TOO_LARGE is returned in error, // In this case, only the header is returned, Callers can allocate more memory if (MACH_RCV_TOO_LARGE! = ret) break; Buffer_size = round_msg(MSG ->msgh_size + MAX_TRAILER_SIZE); if (originalBuffer) *buffer = NULL; originalBuffer = false; *buffer = realloc(*buffer, buffer_size); } HALT; return false; }Copy the code

3.5. Logical mapping

will3.13.4The code logic is drawn as follows:

twoRunLoopRelated functions

When a RunLoop does a callback, it usually does a long function call out, which is usually visible on the call stack when you debug your code with breakpoints. The following is a condensed version of each function. If you see the long function name in the call stack, look it up here to locate the specific call location:

    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__();
Copy the code

Actual code block

Observers create AutoreleasePool: _objc_autoreleasePoolPush(); /// Observers create AutoreleasePool: _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry); Do {/// 2. Notify Observers that a Timer callback is about to occur. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers); Notifying Observers that Source (non-port-based,Source0) callback is about to occur. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// 4. Trigger the Source0 (non-port-based) callback. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// Observers are in this state torelease and create AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting); /// 7. sleep to wait msg. mach_msg() -> mach_msg_trap(); Observers, __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); /// 8. /// 9. If the Timer wakes up, call Timer __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(Timer); /// 9. Block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); If Runloop is woken up by Source1 (port based) event, Handle this event __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); } while (...) ; Observers are about torelease AutoreleasePool: _objc_autoreleasePoolPop(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit); }Copy the code

1.AutoreleasePool

AutoreleasePool is one of the most discussed topics related to RunLoop. AutoreleasePool is not directly related to RunLoop. The main reason to bring the two topics together is that two Observers are registered to manage and maintain AutoreleasePool after the iOS application is launched. If you print currentRunLoop just after the application starts, you can see that many observers are registered by default. There are two of the Observer is _ the callout wrapRunLoopWithAutoreleasePoolHandler, these two are two listening and automatic release pool related.

<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}

Copy the code

The first Observer listens for the RunLoop to enter by calling objc_autoreleasePoolPush() to add a sentinel object flag to the current AutoreleasePoolPage to create the automatic release pool. The order of this Observer is -2147483647, the highest priority, ensuring that it occurs before any callback operation.

The second Observer listens for the RunLoop when it is asleep and when it is about to exit. When going to sleep, objc_autoreleasePoolPop() and objc_autoreleasePoolPush() are called to clean up from the newly added object until the sentry object is encountered. When exiting RunLoop, objc_autoreleasePoolPop() is called torelease objects in the pool automatically. The order of this Observer is 2147483647, which has the lowest priority and is guaranteed to occur after all callbacks. Other operations on the main thread are usually kept within this AutoreleasePool (in the main function) to minimize memory maintenance operations (although you can create AutoreleasePool yourself if you want to explicitly free (e.g. loop), but otherwise you don’t need to). Actually after application startup system also registered the other Observer (such as entering hibernation and execute register callback _UIGestureRecognizerUpdateObserver used to deal with the gesture, the callback _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv Observer is used to interface real-time rendering update) and multiple Source1 (such as the context for CFMachPort’s Source1 is used to receive hardware event responses and distribute them to the application all the way to the UIEvent.

The code that executes on the main thread is usually written inside such callbacks as event callbacks and Timer callbacks. These callbacks are surrounded by AutoreleasePool created by RunLoop, so there is no memory leak and the developer does not have to show that the Pool was created.

Automatic release pool creation, release, and destruction times are shown below

  • kCFRunLoopEntry; // Create an automatic release pool before entering runloop
  • kCFRunLoopBeforeWaiting; // Before sleep, destroy the auto release pool and create a new auto release pool
  • kCFRunLoopExit; // Destroy the auto-release pool before exiting runloop

2.CADisplayLink

CADisplayLink is a timer that executes at the same FPS rate as a screen refresh (preferredFrame persecond can be modified to change the refresh rate), and it also needs to be added to the RunLoop to execute. Like NSTimer, CADisplayLink is implemented based on CFRunloopTimerRef and uses mk_timer at the bottom. It is more accurate than NSTimer (although NSTimer can also modify the accuracy), but like NSTimer it still loses frames for large tasks. In general, CADisaplayLink is used to build frame animations and looks relatively smooth, while NSTimer is more widely used.

3.GCD Timer

GCD thread management is managed directly through the system. The GCD Timer sends a message to the RunLoop through the dispatch port to cause the RunLoop to execute the corresponding block. If the thread does not have a RunLoop, the GCD creates a temporary thread to execute the block and then destroys it after execution. So GCD timers are independent of RunLoop.

As for the accuracy of these two timers, if they are not executed in the thread of RunLoop, GCD Timer can only be used. Since GCD Timer is based on MKTimer(Mach kernel Timer), it is very low-level, so it is very accurate.

If executed in a RunLoop thread, since both GCD Timer and NSTimer trigger the RunLoop through the port sending mechanism, there should not be much difference in accuracy. If the thread RunLoop blocks, both GCD Timer and NSTimer will have latency issues.

4.NSTimer

The Timer Source has been mentioned as the event Source. In fact, its upper counterpart is NSTimer (actually CFRunloopTimerRef), a Timer that developers often use (the underlying implementation is based on mk_timer).

NSTimer is essentially a CFRunLoopTimerRef. It’s toll-free bridged between them. Once an NSTimer is registered with a RunLoop, the RunLoop registers events for its repeated time points. Such as 10:00, 10:10, 10:20. To save resources, RunLoop does not call back this Timer at a very precise point in time. The Timer has a property called Tolerance that indicates the maximum error allowed when the time point is up. Because of this mechanism of NSTimer, execution of NSTimer must depend on RunLoop, and without RunLoop, NSTimer will not execute.

If a point in time is missed, such as when a long task is executed, the callback at that point will also be skipped without delay. Like waiting for a bus, if I’m busy with my phone at 10:10 and I miss the bus at that time, I’ll have to wait for the 10:20 bus.

5. Event response

Apple registered a Source1 (based on the Mach port) to the receiving system, the callback function as __IOHIDEventSystemClientQueueCallback ().

When a hardware event (touch/lock/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 events such as buttons (lock screen/mute etc.), touch, acceleration and proximity sensor, and then forwards them to the App process using Mach port. Then apple registered the Source1 will trigger the callback, and call the _UIApplicationHandleEventQueue () distribution within the application.

_UIApplicationHandleEventQueue () will wrap IOHIDEvent process, and as a UIEvent processing or distribution, including identifying UIGesture/processing screen rotation/send UIWindow, etc. Usually click event such as a UIButton, touchesBegin/Move/End/Cancel events are completed in the callback.

Gesture recognition

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

Apple registered a Observer monitoring BeforeWaiting (Loop entering hibernation) events, the callback function Observer is _UIGestureRecognizerUpdateObserver (), Internally it gets all GestureRecognizer that has just been marked for processing and executes the GestureRecognizer callback.

This callback is handled when there are changes to the UIGestureRecognizer (create/destroy/state change).

7. The UI update

If the print start App after the main thread RunLoop can find another callout for _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv Observer, The monitor for the UI changes after the update, such as changed the frame, adjusted the UI level (UIView/CALayer) or manually after the setNeedsDisplay/setNeedsLayout these operations will be submitted to the global container. This Observer listens for the main thread RunLoop to go to sleep and exit, and once in both states it iterates through all UI updates and commits them for the actual drawing update.

The internal call stack looks something 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];
Copy the code

This is usually perfect because, in addition to system updates, you can also manually trigger the update for the next RunLoop run using methods like setNeedsDisplay. However, if you’re doing a lot of logic, the UI update can get a little rough, so Facebook’s Texture is a solution to that. Texture is basically a Texture that puts the UI layout and drawing in the background as far as possible, the final UI update in the main thread (which must also be done in the main thread), and a set of properties like UIView or CALayer to keep the developer as comfortable as possible. The Texture adds an Observer to the main thread RunLoop to listen for both impending sleep and exit RunLoop states, and to iterate through the queue of pending tasks when a callback is received.

8.Core Animation

After iOS graphics service receives VSync signal, it will notify the App through IPC. App Runloop will register the corresponding CFRunLoopSource to receive the clock signal notification through mach_port after startup, and then the Source callback will drive the animation and display of the entire App. Core Animation registers an Observer in the RunLoop that listens for BeforeWaiting and Exit events. This Observer has a priority of 2,000,000, which is lower than other common observers.

When a touch event arrives, the RunLoop is awakened, and the code in the App performs operations such as creating and adjusting the view hierarchy, setting the UIView’s frame, modifying the CALayer’s transparency, and adding an animation to the view. These operations are eventually captured by CALayer and submitted to an intermediate state via CATransaction (the CATransaction documentation mentions these slightly, but not completely).

Observers of the event are notified when the RunLoop is about to go to sleep (or exit) after all the above operations have been completed. The Observer registered with the CA then merges all intermediate states and submits them to the GPU for display in a callback; If there is an animation, the CA will trigger the relevant process multiple times through mechanisms such as DisplayLink.

3.RunLoopThe application of

1.UIImageViewLazy loading image

Setting up images for UIImageView can take a lot of time, and if you have to slide the tableView or something like that, it might affect the smoothness of the interface. Address is: use the performSelector: withObject: afterDelay: inModes: method, will set the image method in the DefaultMode.

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

Copy the code

2. Resident threads

By default, a child thread terminates after completing its task. When subthreads are used frequently, starting them each time is inefficient. At this point, you can enable the RunLoop of the child thread and keep the RunLoop running to keep the child thread immortal.

Concrete implementation:

/* Return a thread */ + (NSThread *)networkRequestThread {static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; Dispatch_once (&oncepredicate, ^{ And execute the next method on that thread _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread; } / * in a new thread of execution of the first method * / + (void) networkRequestThreadEntryPoint: (id) __unused object {@ autoreleasepool {[[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop * RunLoop = [NSRunLoop currentRunLoop]; / / add the source for the RunLoop, mode to DefaultMode [RunLoop addPort: [NSMachPort port] forMode: NSDefaultRunLoopMode]; // start RunLoop [RunLoop run]; }}Copy the code

This is because a mode must be set before a RunLoop can start, and at least one source/timer is required for the mode to exist. So we add an NSMachPort object to the DefaultMode of the RunLoop. Although messages can be sent to the loop through the NSMachPort object, we add a port to keep the RunLoop going. No message was sent. Of course, we can also add an extra long start time timer to keep the RunLoop from exiting and not take up resources.

3. ScrollScrollviewThe timer fails.

There is a UIScrollview control on the interface, and if there is a timer executing an event, you will notice that the timer will expire when you scroll the Scrollview.

- (void)viewDidLoad { [super viewDidLoad]; [self timer1]; [self timer2]; } // The following two methods of adding a timer have the same effect, Are added in the main thread timer - (void) timer1 {NSTimer * timer = [NSTimer timerWithTimeInterval: 2.0 target: self selector: @ the selector (run) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes]; } - (void) timer2 {[NSTimer scheduledTimerWithTimeInterval: 2.0 target: self selector: @ the selector (run) the userInfo: nil repeats:YES]; } Duplicate codeCopy the code

Because when you scroll Scrollview, RunLoop switches to UITrackingRunLoopMode and the timer runs under defaultMode, the system can only handle one RunLoop at a time. Therefore, the timer in defaultMode fails.

Solutions:

  • thetimerRegistered toNSRunLoopCommonModesIt containsdefaultModetrackingModeTwo modes.
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; Copy the codeCopy the code
  • useGCDCreate a timer,GCDThe created timer is not affectedRunLoopThe influence of
Dispatch_queue_t queue = dispatch_get_main_queue(); Self. timer = dispatch_source_create(dispatch_source_timer, 0, 0, queue); // Set various attributes of timer (when to start the task, how often to execute it) // GCD time parameter, Dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, DISPATCH_TIME_NOW) (int64_t) (1.0 * NSEC_PER_SEC)); // Execute the uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC); dispatch_source_set_timer(self.timer, start, interval, 0); / / set the callback dispatch_source_set_event_handler (self timer, ^ {NSLog (@ "-- -- -- -- -- -- -- -- -- -- - % @", [NSThread currentThread]); }); // Start timer dispatch_resume(self.timer);Copy the code

4. Observe the event status to optimize performance

Suppose we want to realize the height cache calculation of the cell, since the task of “calculating the pre-cache height of the cell” needs to be carried out at the least perceptive moment, it should satisfy both:

  • RunLoopIn a"Idle"stateMode
  • When this timeRunLoopWhen the iterative processing has completed all the events and is about to go to sleep
CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFStringRef runLoopMode = kCFRunLoopDefaultMode; CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler (kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) { // TODO here }); CFRunLoopAddObserver(runLoop, observer, runLoopMode); In the TODO location, task collection and distribution can begin, without forgetting to remove the Observer in due courseCopy the code

5. The monitoring interface is slow

@interface XCatonMonitoring () @property (nonatomic,assign) BOOL isMonitoring; @property (nonatomic,assign) NSInteger timeoutCount; @property (nonatomic,assign) CFRunLoopObserverRef observer; @property (nonatomic,assign) CFRunLoopActivity currentActivity; @property (nonatomic,strong) dispatch_semaphore_t semaphore; + (instancetype) instance; @end @implementation XCatonMonitoring + (instancetype) instance { static id instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[super allocWithZone:NSDefaultMallocZone()] init]; }); return instance; } + (instancetype) allocWithZone:(struct _NSZone *)zone { return [self instance]; } - (void) dealloc { [self endMonitor]; } /** * enable the timeOut monitoring * @param timeOut timeOut period of the timeOut (ms) */ - (void) beginMonitorWithTimeOut:(NSInteger) timeOut {if (_isMonitoring && _observer) return; _isMonitoring = YES; CFRunLoopObserverContext Context = {0, (__bridge void*)self, &CFRetain, &CFRelease, NULL}; // static CFRunLoopObserverRef observer; _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runloopObserverCallback, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); _semaphore = dispatch_semaphoRE_create (0); dispatch_async(dispatch_get_global_queue(0, 0), ^{// enable continuous loop to monitor while ([XCatonMonitoring instance].ismonitoring) {// Assume 5 consecutive timeout times 50ms is considered to be stuck (of course also including single timeout times 250ms) // because of the following The runLoopObserverCallBack method increments the semaphore by 1, so each time the runloop status changes, the following statement will execute // dispatch_semaphore_wait:Returns zero  on success, or non-zero if the timeout occurred. long st = dispatch_semaphore_wait([XCatonMonitoring instance].semaphore, dispatch_time(DISPATCH_TIME_NOW, timeOut*NSEC_PER_MSEC)); if (st ! If (! = 0) {// The semaphore has timed out. [XCatonMonitoring instance].observer) { [XCatonMonitoring instance].timeoutCount = 0; [XCatonMonitoring instance].semaphore = 0; [XCatonMonitoring instance].currentActivity = 0; return; } // kCFRunLoopBeforeSources - about to process source kCFRunLoopAfterWaiting - just woke up from sleep // Get the status of kCFRunLoopBeforeSources to kCFRunLoopBeforeWaiting and then to kCFRunLoopAfterWaiting to know if there is a lag. If ([XCatonMonitoring Instance]. CurrentActivity == kCFRunLoopBeforeSources. // kCFRunLoopBeforeSources: If ([XCatonMonitoring Instance] | | [XCatonMonitoring instance] currentActivity = = kCFRunLoopAfterWaiting) {/ / caton, card number / / less than 5 times, directly to continue [XCatonMonitoring instance]. TimeoutCount ++; if ([XCatonMonitoring instance].timeoutCount < 5) continue; [LXDBacktraceLogger lxd_logMain]; } } [XCatonMonitoring instance].timeoutCount = 0; }}); } - (void) endMonitor { if (! _observer) return; if (! _isMonitoring) return; _isMonitoring = NO; CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); CFRelease(_observer); _observer = nil; } static void runloopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { [XCatonMonitoring instance].currentActivity = activity; dispatch_semaphore_t semaphore = [XCatonMonitoring instance].semaphore; dispatch_semaphore_signal(semaphore); } @endCopy the code

4. Interview questions

1. How does app receive touch events

Detailed introduction

2. Why only the main threadrunloopIs open

UIApplicationMain is called in the mian() function, which creates a main thread for UI processing. To keep the program running and receiving events, open a runloop in the main thread to keep the main thread permanent.

3. Why only refresh the UI on the main thread

All of the UI that we’re using is coming from this base library CALLED UIKit. Because objC is not a thread-safe language, there is the problem of multi-threaded read/write synchronization. Using locking is very expensive for the operating system, consuming a lot of system resources (memory, time slice rotation, CPU processing speed…). In addition, the receiving and processing of system events are all in the main thread. If the UI is asynchronous, there will be the problem of synchronous processing of events. Therefore, it is the optimal solution to keep some events such as multi-touch gestures in the same thread with the UI.

On the other hand, the screen renders at 60 frames (60Hz/ SEC), which is called back 60 times per second (iPad Pro is 120Hz/ SEC), and our Runloop will ideally call back 60 times per clock (iPad Pro 120 times). The high frequency of the call is so that the screen image display can be vertically synchronized without lag. In asynchronous threads it is difficult to ensure that this process is updated synchronously. Even if you can guarantee it, the overhead of the system relative to the main thread, thread scheduling and so on is going to take up most of the resources and it’s not worth it to just do one thing on the same thread.

4.PerformSelectorandrunloopThe relationship between

When NSObect’s performSelector: correlation is called, a timer timer is created internally and added to the current thread’s runloop. If the current thread has not started the runloop, the method will not be called.

One of the most common problems in development is this performSelector, which leads to delayed release of the object, and one of the things to notice in development is that you can use one-pass NSTimer instead.

5. How to keep threads alive

Implement a resident thread, see RunLoop application – Resident Thread