一.RunLoop结构模型

1.概念

RunLoop 是经过内部保护的事情循环(Event Loop)来对事情/音讯进行办理的一个方针。

  1. 没有音讯处理时,就会进行休眠防止资源占用,由用户态切换到内核态(CPU-内核态和用户态)
  2. 有音讯需求处理时,马上被唤醒,由内核态切换到用户态 Event Loop模型
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message)
    } while (message != quit);
}

OSX/iOS 体系中,供给了两个这样的方针:NSRunLoopCFRunLoopRef
CFRunLoopRef 是在 CoreFoundation 框架内的,它供给了纯 C 函数的 API,一切这些 API 都是线程安全的
NSRunLoop 是依据 CFRunLoopRef 的封装,供给了面向方针的 API,可是这些 API 不是线程安全的。
CFRunLoopRef 的代码是开源的,你可以在这儿下载到整个 CoreFoundation 的源码来查看。

2.RunLoop 数据结构

NSRunLoop(Foundation)CFRunLoop(CoreFoundation)的封装,供给了面向方针的API

结构如下

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

一个RunLoop方针,主要包含了一个线程若干个Mode若干个commonMode,还有一个当时运转的Mode

RunLoop 相关的主要涉及五个类:

  • CFRunLoop:RunLoop方针
  • CFRunLoopMode:运转形式
  • CFRunLoopSource:输入源/事情源
  • CFRunLoopTimer:守时源,依据时刻的触发器
  • CFRunLoopObserver:用于监听runLoop运转状况的调查者

2.1.CFRunLoop

CFRunLoop 由五部分组成:

  1. pthread:线程方针,说明 RunLoop线程一一对应
  2. currentMode:当时所处的运转形式
  3. modes:多个运转形式的调集
  4. commonModes:形式称号字符串调集
  5. commonModelItemsObserver,Timer,Source调集
typedef struct __CFRunLoop * CFRunLoopRef;
///CFRunLoop.c 结构体
struct __CFRunLoop {
    //..
    CFMutableSetRef _commonModes; // <Set> String UITrackingRunLoopMode/kCFRunLoopDefaultMode
    CFMutableSetRef _commonModeItems;// <Set> observer/source/timer
    CFRunLoopModeRef _currentMode; //当时运转的mode
    CFMutableSetRef _modes; //内置的modes;
    //...
};

2.2.CFRunLoopMode

name、source0、source1、observers、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称号
    Boolean _stopped;    //mode是否被停止
    char _padding[3];
    //几种事情
    CFMutableSetRef _sources0;  //sources0
    CFMutableSetRef _sources1;  //sources1
    CFMutableArrayRef _observers; //告诉
    CFMutableArrayRef _timers;    //守时器
    CFMutableDictionaryRef _portToV1SourceMap; //字典  key是mach_port_t,value是CFRunLoopSourceRef
    __CFPortSet _portSet; //保存一切需求监听的port,比方_wakeUpPort,_timerPort都保存在这个数组中
    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 */
};

苹果文档中说到的 Mode 有五个,别离是:

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

iOS中公开暴露出来的只要 NSDefaultRunLoopModeNSRunLoopCommonModesNSRunLoopCommonModes 实践上是一个 Mode 的调集,默许包含 NSDefaultRunLoopModeNSEventTrackingRunLoopMode(注意:并不是说Runloop运转kCFRunLoopCommonModes 这种形式下,而是相当于别离注册NSDefaultRunLoopModeUITrackingRunLoopMode。当然你也可以经过调用CFRunLoopAddCommonMode()办法将自界说Mode放到 kCFRunLoopCommonModes 组合)。

///`RunLoop`指定`Mode`运转
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

程序在运转的进程中,会处理依据时刻的、体系的、用户的事情,这些事情在程序运转期间有着不同的优先级,为了满意运用层依据优先级对这些事情的办理,体系选用RunLoopMode对这些事情分组,然后交由RunLoop去办理。除了体系界说的默许形式和常用形式,咱们也可以自界说形式,可是自界说的形式中有必要有相关的事情,否则自界说形式没有任何意义。

_commonModeItems_commonModeskCFRunLoopCommonModes (NSRunLoopCommonModes)背面的完结逻辑,可以了解为选用RunLoopMode对事情进行分组后,咱们又希望一些事情可以一起被多个Mode处理,所以咱们将这些事情(sources/timers/observers)放入_commonModeItems,将需求一起处理这些事情的多个Mode放入_commonModes调集进行符号;当事情指定kCFRunLoopCommonModes形式进行增加时,先会增加到_commonModeItems中,然后将_commonModeItems中的一切事情追加到_commonModes中现已符号的形式下。

///增加一个`Mode`到`RunLoop`的`commonMode`调集中,一旦增加无法移除。只加不减
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);

举个比方: 主线程默许运转在kCFRunLoopDefaultMode下,当咱们滑动ScrollView时会切换到UITrackingRunLoopMode,而主线程的RunLoop_commonModes默许包含这两种形式;
开发中会遇到在主线程发动一个守时器时,会受视图滑动的影响的问题,处理办法:

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

将守时器增加到NSRunLoop方针的NSRunLoopCommonModes形式下,终究守时器会被增加到kCFRunLoopDefaultModeUITrackingRunLoopMode下;当然也可以自行增加到这两个形式中。

查看CoreFoundationCFRunLoopAddTimer办法的完结,可以更深入的了解_commonModeItems_commonModes的作业原理:

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
    ///...
    if (modeName == kCFRunLoopCommonModes) { ///是否是`kCFRunLoopCommonModes`
        ///取`Runloop`的`_commonModes`
    CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
    if (NULL == rl->_commonModeItems) {
            ///创立`_commonModeItems`<Set>
        rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    }
        ///增加守时器到`_commonModeItems`
    CFSetAddValue(rl->_commonModeItems, rlt);
    if (NULL != set) { //`_commonModes`有值
        CFTypeRef context[2] = {rl, rlt};
        /* add new item to all common-modes */
            ///为Set调集中的每个元素都调用该办法
        CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
        CFRelease(set);
    }
    } else {
        ///非`kCFRunLoopCommonModes`,先找找`runloop`的modes是否有,找不到创立
    CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
    if (NULL != rlm) {
            if (NULL == rlm->_timers) { ///创立寄存`timer`的数组
                CFArrayCallBacks cb = kCFTypeArrayCallBacks;
                cb.equal = NULL;
                rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
            }
    }
        /// mode有了,守时器方针的modes又没有该mode
    if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
            ///...
            if (NULL == rlt->_runLoop) {
        rlt->_runLoop = rl;
        } else if (rl != rlt->_runLoop) {
                 //...
                //守时器已相关的runloop与当时runloop不共同,回来
        return;
        }
            ///守时器的modes 增加该mode的称号
        CFSetAddValue(rlt->_rlModes, rlm->_name);
            //选用mktimer(mach kernel timer)经过machport 和 machmsg 触发守时器事情
            __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
    }
}

CoreFoundation中向指定RunLoopMode中增加和移除事情的函数有:

//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);

2.3.CFRunLoopSource

结构如下

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order; ///同observer
    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 {
    ///...同`CFRunLoopSourceContext`前7个特点
#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;

分为 source0source1 两种

  1. source0:
    • 非依据port的,也便是用户触发的事情。需求手动唤醒线程,将当时线程从内核态切换到用户态
  2. source1:
    • 依据port的,包含一个 mach_port 和一个回调,可监听体系端口和经过内核和其他线程发送的音讯,能主动唤醒RunLoop,接纳分发体系事情。
    • 具备唤醒线程的才干

2.4.CFRunLoopTimer

依据时刻的触发器,基本上说的便是 NSTimerCFRunLoopTimerNSTimer 是toll-free bridged的,可以相互转化)。在预设的时刻点唤醒 RunLoop 履行 回调。

由于它是依据 RunLoop 的,因而它不是实时的(便是 NSTimer不精确的, 由于 RunLoop 只担任 分发源的音讯。假如线程当时正在处理繁重的使命,就有或许导致 Timer本次延时,或许少履行一次)。

结构如下

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;  //符号fire状况
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;        //增加该timer的runloop
    CFMutableSetRef _rlModes;     //寄存一切 包含该timer的 mode的 modeName,意味着一个timer或许会在多个mode中存在
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;     //抱负时刻距离  /* immutable */
    CFTimeInterval _tolerance;    //时刻误差      /* mutable */
    uint64_t _fireTSR;          /* TSR units */
    CFIndex _order;         /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

2.5.CFRunLoopObserver

CFRunLoopObserver是调查者,可以调查RunLoop的各种状况,并抛出回调。

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 */
};

CFRunLoopObserver可以调查的状况有如下6种:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //行将进入run loop
    kCFRunLoopBeforeTimers = (1UL << 1), //行将处理timer
    kCFRunLoopBeforeSources = (1UL << 2),//行将处理source
    kCFRunLoopBeforeWaiting = (1UL << 5),//行将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒可是还没开端处理事情
    kCFRunLoopExit = (1UL << 7),//run loop现已退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

3.RunLoop 内部逻辑

Runloop 经过监控 Source 来决议有没有使命要做,除此之外,咱们还可以用 Runloop Observer 来监控 Runloop 本身的状况。 Runloop Observer 可以监控上面的 Runloop 事情,具体流程如下图。

从底层了解面试题-RunLoop篇

次序如下:

  1. 告诉调查者RunLoop现已发动
  2. 告诉调查者行行将开端的守时器
  3. 告诉调查者任何行将发动的非依据端口的源
  4. 发动任何准备好的非依据端口的源
  5. 假如依据端口的源准备好并处于等候状况,当即发动,并进入进程9
  6. 告诉调查者线程进入休眠状况
  7. 将线程置于休眠直到任一下面的事情产生:
    • 某一事情到达依据端口的源
    • 守时器发动
    • RunLoop设置的时刻现已超时
    • RunLoop被显现唤醒
  8. 告诉调查者线程将被唤醒
  9. 处理未处理的事情
    • 假如用户界说的守时器发动,处理守时器事情并重启RunLoop,进入进程2
    • 假如输入源发动,传递相应的音讯
    • 假如RunLoop被显现唤醒而且时刻还没超时,重启RunLoop。进入进程2
  10. 告诉调查者RunLoop完毕。

3.1.RunLoop 发动

/// 用DefaultMode发动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode发动,允许设置RunLoop超时时刻
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

Runloop 总是运转在某种特定的CFRunLoopModeRef下(每次运转__CFRunLoopRun()函数时有必要指定 Mode),该Mode会被设置为_currentMode,只要与该Mode相关的输入源source0source1才干被处理,相同的,监听RunLoop的运转,只要与该Mode相关的observers才干收到告诉。

3.2.CFRunLoopRunSpecific

/*
* RunLoop的完结,指定mode运转runloop 
* @param rl 当时运转的runloop 
* @param modeName 需求运转的mode的name 
* @param seconds runloop的超时时刻 
* @param returnAfterSourceHandled 是否处理完事情就回来 
*/
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    /// 首先依据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, 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和上次的mode共同 
    rl->_currentMode = currentMode; 
    //初始化一个result为kCFRunLoopRunFinished 
    int32_t result = kCFRunLoopRunFinished;
    /// 1. 告诉 Observers: RunLoop 行将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
            /// 2. 告诉 Observers: RunLoop 行将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 告诉 Observers: RunLoop 行将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 履行被参加的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            /// 4. RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 履行被参加的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            /// 5. 假如有 Source1 (依据port) 处于 ready 状况,直接处理这个 Source1 然后跳转去处理音讯。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            /// 6.告诉 Observers: RunLoop 的线程行将进入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            /// 7. 调用 mach_msg 等候接受 mach_port 的音讯。线程将进入休眠, 直到被下面某一个事情唤醒。
            /// • 一个依据 port 的Source 的事情。
            /// • 一个 Timer 到时刻了
            /// • RunLoop 本身的超时时刻到了
            /// • 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
            /// 8. 告诉 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            /// 9.收到音讯,处理音讯。
            handle_msg:
            /// 9.1 假如一个 Timer 到时刻了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
            /// 9.2 假如有dispatch到main_queue的block,履行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
            /// 9.3 假如一个 Source1 (依据port) 宣布事情了,处理这个事情
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            /// 履行参加到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode); 
            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事情就回来。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数符号的超时时刻了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部调用者强制中止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }
            /// 假如没超时,mode里没空,loop也没被中止,那持续loop。
        } while (retVal == 0);
    }
    /// 10. 告诉 Observers: RunLoop 行将退出。
    __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;
}

经过 CFRunLoopRunSpecific 的内部逻辑,咱们可以得出:

  • 假如指定了一个不存在的mode运转RunLoop,那么会失利,mode 不会创立,所以这儿传入的 mode 有必要是存在
  • 假如指定了一个 mode,可是这个 mode 中不包含任何 modeItem,那么 RunLoop 也不会运转,所以有必要要* 传入至少包含一个 modeItem的mode
  • 在进入 runloop 之前告诉 observer,状况为 kCFRunLoopEntry
  • 在退出 runloop 之后告诉 observer,状况为 kCFRunLoopExit

RunLoop 的运转的最中心函数是 __CFRunLoopRun,接下来咱们剖析 __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;
}

源代码虽然不算太长,可是假如不太熟悉的话面对这么一堆不知道做什么的函数调用仍是会给人一种神秘感。可是现在可以不必逐行阅读,后边慢慢解开这层神秘面纱。现在只要了解上面的伪代码知道中心的办法 __CFRunLoopRun 内部其实是一个_do while_循环,这也正是Runloop运转本质。履行了这个函数以后就一向处于“等候-处理”的循环之中,直到循环完毕。仅仅不同于咱们自己写的循环它在休眠时简直不会占用体系资源,当然这是由于体系内核担任完结的,也是Runloop精华所在

3.4.__CFRunLoopServiceMachPort

这个函数在runloop中起到了至关重要的效果

/**
 *  接纳指定内核端口的音讯
 *
 *  @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;
}

3.5.逻辑导图

3.13.4 的代码逻辑画成一张图,逻辑如下:

从底层了解面试题-RunLoop篇

二.RunLoop 相关的功能

RunLoop 进行回调时,一般都是经过一个很长的函数调用出去 (call out), 当你在你的代码中下断点调试时,一般能在调用栈上看到这些函数。下面是这几个函数的收拾版本,假如你在调用栈中看到这些长函数名,在这儿查找一下就能定位到具体的调用地点了:

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

实践代码块

{
    /// 1. 告诉Observers,行将进入RunLoop
    /// 此处有Observer会创立AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
        /// 2. 告诉 Observers: 行将触发 Timer 回调。        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 告诉 Observers: 行将触发 Source (非依据port的,Source0) 回调。        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); 
        /// 4. 触发 Source0 (非依据port的) 回调。        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
        /// 6. 告诉Observers,行将进入休眠
        /// 此处有Observer开释并新建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();         
        /// 8. 告诉Observers,线程被唤醒        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); 
        /// 9. 假如是被Timer唤醒的,回调Timer        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer); 
        /// 9. 假如是被dispatch唤醒的,履行一切调用 dispatch_async 等办法放入main queue 的 block        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); 
        /// 9. 假如假如Runloop是被 Source1 (依据port的) 的事情唤醒了,处理这个事情        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); 
    } while (...); 
    /// 10. 告诉Observers,行将退出RunLoop
    /// 此处有Observer开释AutoreleasePool: _objc_autoreleasePoolPop(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

1.AutoreleasePool

AutoreleasePool 是一个与 RunLoop 相关评论较多的论题。其实从 RunLoop 源代码剖析,AutoreleasePoolRunLoop 并没有直接的联系,之所以将两个论题放到一起评论最主要的原因是由于在iOS运用发动后会注册两个 Observer 办理保护 AutoreleasePool。不妨在运用程序刚刚发动时打印 currentRunLoop 可以看到体系默许注册了很多个 Observer,其间有两个 Observercallout 都是 _ wrapRunLoopWithAutoreleasePoolHandler,这两个是和主动开释池相关的两个监听。

<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 = ()}}

第一个 Observer 会监听 RunLoop进入,它会回调objc_autoreleasePoolPush()向当时的 AutoreleasePoolPage 增加一个岗兵方针标志创立主动开释池。这个 Observerorder 是-2147483647优先级最高,确保产生在一切回调操作之前。

第二个 Observer 会监听 RunLoop 的进入休眠和行将退出 RunLoop 两种状况,在行将进入休眠时会调用 objc_autoreleasePoolPop()objc_autoreleasePoolPush() 依据状况从最新参加的方针一向往前整理直到遇到岗兵方针。
而在行将退出 RunLoop 时会调用 objc_autoreleasePoolPop() 开释主动主动开释池内方针。这个 Observerorder 是2147483647,优先级最低,确保产生在一切回调操作之后。 主线程的其他操作一般均在这个 AutoreleasePool 之内(main函数中),以尽或许削减内存保护操作(当然你假如需求显式开释【例如循环】时可以自己创立 AutoreleasePool,否则一般不需求自己创立)。 其实在运用程序发动后体系还注册了其他 Observer(例如行将进入休眠时履行注册回调 _UIGestureRecognizerUpdateObserver 用于手势处理、回调为 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 的Observer 用于界面实时制作更新)和多个 Source1(例如 contextCFMachPortSource1 用于接纳硬件事情呼应从而分发到运用程序一向到 UIEvent)。

在主线程履行的代码,一般是写在比方 事情回调Timer 回调内的。这些回调会被 RunLoop 创立好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显现创立 Pool 了。

主动开释池的创立和开释,毁掉的机遇如下所示

  • kCFRunLoopEntry; // 进入runloop之前,创立一个主动开释池
  • kCFRunLoopBeforeWaiting; // 休眠之前,毁掉主动开释池,创立一个新的主动开释池
  • kCFRunLoopExit; // 退出runloop之前,毁掉主动开释池

2.CADisplayLink

CADisplayLink 是一个履行频率(fps)和屏幕改写相同(可以修正 preferredFramesPerSecond 改变改写频率)的守时器,它也需求参加到 RunLoop 才干履行。与 NSTimer 类似,CADisplayLink 相同是依据 CFRunloopTimerRef 完结,底层运用 mk_timer(可以比较参加到 RunLoop 前后 RunLooptimer 的改变)。和 NSTimer 相比它精度更高(虽然 NSTimer 也可以修正精度),不过和 NStimer 类似的是假如遇到大使命它仍然存在丢帧现象。一般状况下 CADisaplayLink 用于构建帧动画,看起来相对更加流畅,而 NSTimer 则有更广泛的用处。

3.GCD Timer

GCD 则不同,GCD 的线程办理是经过体系来直接办理的。GCD Timer 是经过 dispatch portRunLoop 发送音讯,来使 RunLoop 履行相应的 block,假如所在线程没有 RunLoop,那么 GCD 会临时创立一个线程去履行 block,履行完之后再毁掉掉,因而 GCDTimer 是不依赖 RunLoop 的。

至于这两个 Timer 的精确性问题,假如不在 RunLoop 的线程里边履行,那么只能运用 GCD Timer,由于 GCD Timer 是依据 MKTimer(mach kernel timer),现已很底层了,因而是很精确的。

假如在 RunLoop 的线程里边履行,由于 GCD TimerNSTimer 都是经过 port 发送音讯的机制来触发 RunLoop 的,因而精确性差别应该不是很大。假如线程 RunLoop 堵塞了,不管是 GCD Timer 仍是 NSTimer 都会存在推迟问题。

4.NSTimer

前面一向说到 Timer Source 作为事情源,事实上它的上层对应便是 NSTimer(其实便是 CFRunloopTimerRef)这个开发者常常用到的守时器(底层依据运用 mk_timer 完结)

NSTimer 其实便是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时刻点注册好事情。例如 10:00, 10:10, 10:20 这几个时刻点。RunLoop 为了节约资源,并不会在非常精确的时刻点回调这个 TimerTimer 有个特点叫做 Tolerance (宽容度),标明了当时刻点到后,容许有多少最大误差。由于 NSTimer 的这种机制,因而 NSTimer 的履行有必要依赖于 RunLoop,假如没有 RunLoopNSTimer 是不会履行的。

假如某个时刻点被错过了,例如履行了一个很长的使命,则那个时刻点的回调也会跳过去,不会延后履行。就比方等公交,假如 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

5.事情呼应

苹果注册了一个 Source1 (依据 mach port 的) 用来接纳体系事情,其回调函数为 __IOHIDEventSystemClientQueueCallback()

当一个硬件事情(触摸/锁屏/摇晃等)产生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事情并由 SpringBoard 接纳。这个进程的具体状况可以参阅这儿。SpringBoard 只接纳按键(锁屏/静音等),触摸,加速,挨近传感器等几种 Event,随后用 mach port 转发给需求的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行运用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其间包含识别UIGesture/处理屏幕旋转/发送给UIWindow 等。一般事情比方 UIButton 点击、touchesBegin/Move/End/Cancel 事情都是在这个回调中完结的。

6.手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当时的 touchesBegin/Move/End 系列回调打断。随后体系将对应的 UIGestureRecognizer 符号为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop行将进入休眠) 事情,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取一切刚被符号为待处理的 GestureRecognizer,并履行 GestureRecognizer 的回调。

当有 UIGestureRecognizer 的改变(创立/毁掉/状况改变)时,这个回调都会进行相应处理。

7.UI更新

假如打印App发动之后的主线程 RunLoop 可以发现另外一个 callout_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 的Observer,这个监听专门担任UI改变后的更新,比方修正了 frame、调整了UI层级(UIView/CALayer)或许手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到大局容器。而这个 Observer 监听了主线程 RunLoop 的行将进入休眠和退出状况,一旦进入这两种状况则会遍历一切的UI更新并提交进行实践制作更新。

这个函数内部的调用栈大概是这样的:

_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];

一般状况下这种办法是完美的,由于除了体系的更新,还可以利用 setNeedsDisplay 等办法手动触发下一次 RunLoop 运转的更新。可是假如当时正在履行很多的逻辑运算或许UI的更新就会比较卡,因而 facebook 推出了 Texture 来处理这个问题。Texture 其实是将UI排版和制作运算尽或许放到后台,将UI的终究更新操作放到主线程(这一步也有必要在主线程完结),一起供给一套类 UIViewCALayer 的相关特点,尽或许确保开发者的开发习惯。这个进程中 Texture 在主线程 RunLoop 中增加了一个 Observer 监听行将进入休眠和退出 RunLoop 两种状况,收到回调时遍历行列中的待处理使命一一履行。

8.Core Animation

iOS 图形服务接纳到 VSync 信号后,会经过 IPC 告诉到 App 内。AppRunloop 在发动后会注册对应的 CFRunLoopSource 经过 mach_port 接纳传过来的时钟信号告诉,随后 Source 的回调会驱动整个 App 的动画与显现。Core AnimationRunLoop 中注册了一个 Observer,监听了 BeforeWaitingExit 事情。这个 Observer 的优先级是 2000000,低于常见的其他 Observer

当一个触摸事情到来时,RunLoop 被唤醒,App 中的代码会履行一些操作,比方创立和调整视图层级、设置 UIViewframe、修正 CALayer 的透明度、为视图增加一个动画;这些操作终究都会被 CALayer 捕获,并经过 CATransaction 提交到一个中间状况去(CATransaction 的文档略有说到这些内容,但并不完整)。

当上面一切操作完毕后,RunLoop 行将进入休眠(或许退出)时,重视该事情的 Observer 都会得到告诉。这时 CA 注册的那个 Observer 就会在回调中,把一切的中间状况兼并提交到 GPU 去显现;假如此处有动画,CA 会经过 DisplayLink 等机制屡次触发相关流程。

三. RunLoop 的运用

1.UIImageView 推迟加载图片

给 UIImageView 设置图片或许耗时不少,假如此时要滑动 tableView 等则或许影响到界面的流畅。处理是:运用performSelector:withObject:afterDelay:inModes:办法,将设置图片的办法放到 DefaultMode 中履行。

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

2.常驻线程

子线程默许是完结使命后完毕。当要常常运用子线程,每次敞开子线程比较耗功能。此时可以敞开子线程的 RunLoop,坚持 RunLoop 运转,则使子线程坚持不死。

具体完结:

/* 回来一个线程 */
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        // 创立一个线程,并在该线程上履行下一个办法
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        // 敞开线程
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
/* 在新开的线程中履行的第一个办法 */
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        // 获取当时线程对应的 RunLoop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        // 为 RunLoop 增加 source,形式为 DefaultMode
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 开端运转 RunLoop
        [runLoop run];
    }
}

由于 RunLoop 发动前有必要设置一个 mode,而 mode 要存在则至少需求一个 source / timer。所以上面的做法是为 RunLoopDefaultMode 增加一个 NSMachPort 方针,虽然音讯是可以经过NSMachPort方针发送到 loop 内,但这儿增加的 port 仅仅为了 RunLoop 一向不退出,而没有发送什么音讯。当然咱们也可以增加一个超长发动时刻的 timer 来既坚持 RunLoop 不退出也不占用资源。

3.滚动 Scrollview 导致守时器失效

在界面上有一个 UIScrollview 控件,假如此时还有一个守时器在履行一个事情,你会发现当你滚动 Scrollview 的时分,守时器会失效。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self timer1];
    [self timer2];
}
//下面两种增加守时器的办法效果相同,都是在主线程中增加守时器
- (void)timer1 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];
}
- (void)timer2 {
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
仿制代码

由于当你滚动 Scrollview 的时分,RunLoop 会切换到 UITrackingRunLoopMode 形式,而守时器运转在 defaultMode 下面,体系一次只能处理一种形式的 RunLoop,所以导致 defaultMode 下的守时器失效。

处理办法:

  • timer 注册到 NSRunLoopCommonModes,它包含了 defaultModetrackingMode 两种形式。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
仿制代码
  • 运用 GCD 创立守时器,GCD 创立的守时器不会受 RunLoop 的影响
    // 获得行列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 创立一个守时器(dispatch_source_t本质仍是个OC方针)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 设置守时器的各种特点(何时开端使命,每隔多长时刻履行一次)
    // GCD的时刻参数,一般是纳秒(1秒 == 10的9次方纳秒)
    // 比当时时刻晚1秒开端履行
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    //每隔一秒履行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);
    // 设置回调
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);
    });
    // 发动守时器
    dispatch_resume(self.timer);

4.调查事情状况,优化功能

假定咱们想完结 cell 的高度缓存计算,由于“计算cell的预缓存高度”的使命需求在最无感知的时刻进行,所以应该一起满意:

  • RunLoop 处于“闲暇”状况 Mode
  • 当这一次 RunLoop 迭代处理完结了一切事情,马上要休眠时
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
    // TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);
在其间的 TODO 方位,就可以开端使命的收集和分发了,当然,不能忘掉适时的移除这个 observer

5.监控界面卡顿

@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];
}
/**
* 敞开卡顿监控
* @Param timeOut 卡顿超时时刻(毫秒)
*/
- (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), ^{
        // 敞开持续的loop来监控
        while ([XCatonMonitoring instance].isMonitoring) {
            // 假定连续5次超时50ms以为卡顿(当然也包含了单次超时250ms)
            // 由于下面 runloop 状况改变回调办法runLoopObserverCallBack中会将信号量递加 1,所以每次 runloop 状况改变后,下面的语句都会履行一次
            // 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 != 0) {  // 信号量超时了 - 即 runloop 的状况长时刻没有产生变更,长期处于某一个状况下
                if (![XCatonMonitoring instance].observer) {
                    [XCatonMonitoring instance].timeoutCount = 0;
                    [XCatonMonitoring instance].semaphore = 0;
                    [XCatonMonitoring instance].currentActivity = 0;
                    return;
                }
                // kCFRunLoopBeforeSources - 行将处理source kCFRunLoopAfterWaiting - 刚从休眠中唤醒
                // 获取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状况就可以知道是否有卡顿的状况。
                // kCFRunLoopBeforeSources:停留在这个状况,表示在做很多事情
                if ([XCatonMonitoring instance].currentActivity == kCFRunLoopBeforeSources ||
                    [XCatonMonitoring instance].currentActivity == kCFRunLoopAfterWaiting) {    // 产生卡顿,记载卡顿次数
                    // 不足 5 次,直接 continue 当次循环,不将timeoutCount置为0
                    [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);
}
@end

四.面试题

1.app怎么接纳到触摸事情的

具体介绍

2.为什么只要主线程的runloop是敞开的

mian()函数中调用 UIApplicationMain,这儿会创立一个主线程,用于UI处理,为了让程序可以一向运转并接纳事情,所以在主线程中敞开一个 runloop,让主线程常驻.

3.为什么只在主线程改写UI

咱们一切用到的UI都是来自于UIKit这个基础库.由于objc不是一门线程安全的言语所以存在多线程读写不同步的问题,假如运用加锁的办法操作体系开销很大,会耗费很多的体系资源(内存、时刻片轮转、cpu处理速度…),加上体系事情的接纳处理都在主线程,假如UI异步线程的话 还会存在 同步处理事情的问题,所以多点触摸手势等一些事情要坚持和UI在同一个线程相对是最优解.

另一方面是 屏幕的渲染是 60帧(60Hz/秒), 也便是1秒钟回调60次的频率,(iPad Pro 是120Hz/秒),咱们的runloop 抱负状况下也会按照时钟周期 回调60次(iPad Pro 120次), 这么高频率的调用是为了 屏幕图画显现可以垂直同步 不卡顿.在异步线程的话是很难确保这个处理进程的同步更新. 即便能确保的话 相对主线程而言 体系资源开销 线程调度等等将会占有大部分资源和在同一个线程只专门干一件事有点得不偿失.

4.PerformSelectorrunloop的联系

当调用 NSObectperformSelector: 相关的时分,内部会创立一个 timer 守时器增加到当时线程的 runloop 中,假如当时线程没有发动 runloop,则该办法不会被调用.

开发中遇到最多的问题便是这个 performSelector: 导致方针的推迟开释,这儿开发进程中注意一下,可以用单次的 NSTimer 替代.

5.怎么使线程保活

完结一个常驻线程,参阅【RunLoop的运用 - 常驻线程