iOS RunLoop详解

6,880 阅读21分钟

1、RunLoop初探

1.1、RunLoop是什么?

RunLoop从字面上来说是跑圈的意思,如果这样理解不免有些肤浅。下面是苹果官方文档的关于RunLoop的一段说明。

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

这段话翻译成中文如下:

RunLoop是与线程息息相关的基本基础结构的一部分。RunLoop是一个调度任务和处理任务的事件循环。RunLoop的目的是为了在有工作的时让线程忙起来,而在没工作时让线程进入睡眠状态。

简单的说RunLoop是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

1.2、RunLoop与线程

从上面关于RunLoop的定义我们可以知道,RunLoop和线程有着密不可分的关系。通常情况下线程的作用是用来执行一个或多个特定的任务,在线程执行完成之后就会退出不再执行任务,RunLoop这样的循环机制会让线程能够不断地执行任务并不退出。

Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events. Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop. Your application does not need to create these objects explicitly; each thread, including the application’s main thread, has an associated run loop object. Only secondary threads need to run their run loop explicitly, however. The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process.
【译】RunLoop管理并不是完全自动的。需要设计一个线程在合适的时机启动并响应传入的事件,您仍然必须设计线程的代码以在适当的时候启动运行循环并响应传入的事件。 CocoaCore Foundation都提供RunLoop对象,以帮助配置和管理线程的RunLoop。应用程序并不需要显式创建这些对象。每个线程(包括应用程序的主线程)都有一个关联的RunLoop对象。但是,在子线程中需要显式地运行RunLoop。在应用程序启动过程中,应用程序框架会自动在主线程上设置并运行RunLoop。

从上面这一段话中我们获取到如下几点信息:

  • RunLoop和线程是绑定在一起的,每条线程都有唯一一个与之对应的RunLoop对象
  • 不能自己创建RunLoop对象,但是可以获取系统提供的RunLoop对象
  • 主线程的RunLoop对象是由系统自动创建好的,在应用程序启动的时候会自动完成启动,而子线程中的RunLoop对象需要我们手动获取并启动。

RunLoop与线程的关系如下图所示:

从上图中可以看出,RunLoop在线程中不断检测,通过input sourcetimer source接受事件,然后通知线程进行处理事件。

1.3、RunLoop的结构

如下是RunLoop的结构定义源码:

struct __CFRunLoop {
    CFRuntimeBase _base;
    //获取mode列表的锁
    pthread_mutex_t _lock;            /* locked for accessing mode list */
    //唤醒端口
    __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp
    Boolean _unused;
    //重置RunLoop数据
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    //RunLoop所对应的线程
    pthread_t _pthread;
    uint32_t _winthread;
    //标记为common的mode的集合
    CFMutableSetRef _commonModes;
    //commonMode的item集合
    CFMutableSetRef _commonModeItems;
    //当前的mode
    CFRunLoopModeRef _currentMode;
    //存储的是CFRunLoopModeRef
    CFMutableSetRef _modes;
     // _block_item链表表头指针
    struct _block_item *_blocks_head;
    // _block_item链表表尾指针
    struct _block_item *_blocks_tail;
    //运行时间点
    CFAbsoluteTime _runTime;
    //睡眠时间点
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

从上面RunLoop的源码不难看出,一个RunLoop对象包含一个线程(_pthread),若干个mode(_modes),若干个commonMode(_commonModes)。 不管是mode还是commonMode其类型都是CFRunLoopMode,只是在处理上有所不同。

1.4、CFRunLoopModeRef

如下所示是CFRunLoopMode的源码。

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    //锁, 必须runloop加锁后才能加锁
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    //mode的名称
    CFStringRef _name;
    //mode是否停止
    Boolean _stopped;
    char _padding[3];
    //sources0事件
    CFMutableSetRef _sources0;
    //sources1事件
    CFMutableSetRef _sources1;
    //observers事件
    CFMutableArrayRef _observers;
    //timers事件
    CFMutableArrayRef _timers;
    //字典  key是mach_port_t,value是CFRunLoopSourceRef
    CFMutableDictionaryRef _portToV1SourceMap;
    //保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    //GCD定时器
    dispatch_source_t _timerSource;
    //GCD队列
    dispatch_queue_t _queue;
    // 当定时器触发时设置为true
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    //MK_TIMER的port
    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 */
};

CFRunLoopMode的源码不难看出,一个CFRunLoopMode对象有唯一一个name,若干个sources0事件,若干个sources1事件,若干个timer事件,若干个observer事件和若干portRunLoop总是在某种特定的CFRunLoopMode下运行的,这个特定的mode便是_currentMode。而CFRunloopRef对应结构体的定义知道一个RunLoop对象包含有若干个mode,那么就形成了如下如所示的结构。

关于CFRunLoopMode,苹果提到了5个Model,分别是NSDefaultRunLoopModeNSConnectionReplyModeNSModalPanelRunLoopModeNSEventTrackingRunLoopModeNSRunLoopCommonModes。在iOS中公开暴露只有NSDefaultRunLoopModeNSRunLoopCommonModes

  • NSDefaultRunLoopMode:默认模式是用于大多数操作的模式。大多数时候使用此模式来启动RunLoop并配置输入源。
  • NSConnectionReplyMode:Cocoa将此模式与NSConnection对象结合使用以监测回应。几乎不需要自己使用此模式。
  • NSModalPanelRunLoopMode:Cocoa使用此模式来识别用于模式面板的事件。
  • NSEventTrackingRunLoopMode:Cocoa使用此模式来限制鼠标拖动loop和其他类型的用户界面跟踪loop期间的传入事件。通常用不到。
  • NSRunLoopCommonModes:是NSDefaultRunLoopModeNSEventTrackingRunLoopMode集合,在这种模式下RunLoop分别注册了NSDefaultRunLoopModeUITrackingRunLoopMode。当然也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到kCFRunLoopCommonModes组合。

1.5、CFRunLoopSourceRef-事件源

如下是CFRunLoopSource的源码。

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    //联合体 
    union {
        CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

根据苹果官方的定义CFRunLoopSource是输入源的抽象,分为source0和source1两个版本。

  • source0:是App内部事件,只包含一个函数指针回调,并不能主动触发事件,使用时,你需要先调用CFRunLoopSourceSignal(source),将这个source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件。
  • source1source1包含一个mach_port和一个函数回调指针。source1是基于port的,通过读取某个port上内核消息队列上的消息来决定执行的任务,然后再分发到sources0中处理的。source1只供系统使用,并不对开发者开放。

1.6、CFRunLoopTimerRef--Timer事件

CFRunLoopTimer是定时器。下面是CFRunLoopTimer的源码:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    //timer对应的runloop
    CFRunLoopRef _runLoop;
    //timer对应的mode
    CFMutableSetRef _rlModes;
    //下一次触发的时间
    CFAbsoluteTime _nextFireDate;
    //定时的间隔
    CFTimeInterval _interval;        /* immutable */
    //定时器允许的误差
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    //优先级
    CFIndex _order;            /* immutable */
    //任务回调
    CFRunLoopTimerCallBack _callout;    /* immutable */
    //上下文
    CFRunLoopTimerContext _context;    /* immutable, except invalidation */
};

从上面的代码可以看出,timer是依赖于runloop的,而且有函数指针回调,那么便可以在设定的时间点抛出回调执行任务。同时苹果官方文档也有提到CFRunLoopTimerNSTimertoll-free bridged的,这就一位着两者之间可以相互转换。 关于NSTimer和RunLoop之间的关系,可以参照之前的文章iOS定时器-- NSTimer&GCD定时器

1.7、CFRunLoopObserverRef--观察者

CFRunLoopObserver是观察者,监测RunLoop的各种状态的变化。如下是CFRunLoopObserver的源码。

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    //对应的runLoop对象
    CFRunLoopRef _runLoop;
    // 当前的观察的runloop个数
    CFIndex _rlCount;
    //runloop的状态
    CFOptionFlags _activities;        /* immutable */
    CFIndex _order;            /* immutable */
    //回调
    CFRunLoopObserverCallBack _callout;    /* immutable */
    //上下文
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

RunLoop的source事件源来监测是否有需要执行的任务,而observer则是监测RunLoop本身的各种状态的变化,在合适的时机抛出回调,执行不同类型的任务。RunLoop用于观察的状态如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),//即将进入runloop
    kCFRunLoopBeforeTimers = (1UL << 1),//即将处理timer事件
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理source事件
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//即将唤醒
    kCFRunLoopExit = (1UL << 7),//runloop退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

1.8、小结

  1. RunLoop是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠
  2. RunLoop和线程是绑定在一起的,每条线程都有唯一一个与之对应的RunLoop对象
  3. 每个RunLoop对象都会包含有若干个mode,每个mode包含有唯一一个name,若干个sources0事件,若干个sources1事件,若干个timer事件,若干个observer事件和若干portRunLoop总是在某种特定的mode下运行的,这个特定的mode便是_currentMode

2、RunLoop底层实现原理

2.1、RunLoop启动

RunLoop启动有两个方法可供调用,分别是CFRunLoopRunCFRunLoopRunInMode。先来看一下这两个方法的源码。

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

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

从上面这两个方法的实现可以看出,CFRunLoopRun方法启动的RunLoop是运行在kCFRunLoopDefaultMode模式下的,即以这种方式启动的runloop是在默认模式下运行的。而CFRunLoopRunInMode方法则是需要指定运行的mode。从这里也可以看出来RunLoop虽然有很多的mode,但是RunLoop的运行只能是在一种mode下进行。同时这两个方法都调用了CFRunLoopRunSpecific方法,该方法就是具体启动RunLoop的方法,这个方法的第一个参数就是当前的RunLoop,所以在分析CFRunLoopRunSpecific方法之前,先来看下是怎么获取到RunLoop的。

2.2、获取RunLoop

苹果开放给开发者连个方法获取RunLoop对象,分别是CFRunLoopGetCurrentCFRunLoopGetMain,它们分别代表着获取当前线程的Runloop对象和获取主线程的RunLoop对象。

2.2.1、CFRunLoopGetCurrent

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

上面的代码是CFRunLoopGetCurrent的实现源码,从这段代码可以看出该方法内部调用了_CFRunLoopGet0方法,传入的参数是当前线程pthread_self(),由此可见CFRunLoopGetCurrent函数必须要在线程内部调用才能获取到RunLoop对象。

2.2.2、CFRunLoopGetMain

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

上面的代码是CFRunLoopGetMain的实现源码,从这段代码可以看出该方法内部调用了_CFRunLoopGet0方法,传入的参数是主线程pthread_main_thread_np(),由此可见CFRunLoopGetCurrent函数不管是在主线程中还是在子线程中都可以获取到主线程的RunLoop。

2.2.3、_CFRunLoopGet0

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)
{
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        //创建一个字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //创建主线程的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //把主线程的RunLoop保存到dict中,key是线程,value是RunLoop
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        //此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void *volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        //释放主线程RunLoop
        CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    // 根据线程从__CFRunLoops获取RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    //如果在__CFRunLoops中没有找到
    if (!loop) {
        //创建一个新的RunLoop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //把新创建的RunLoop存放到__CFRunLoops中
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
        CFRelease(newLoop);
    }
    //如果传入的线程就是当前的线程
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            //注册一个回调,当当前线程销毁时销毁对应的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS - 1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

上面这段关于CFRunLoopGet0函数方法的代码并不复杂,我们可以从中得到如下几个信息:

  1. RunLoop和线程是一一对应的,是以线程为key,RunLoop对象为value存放在一个全局字典中的。
  2. 主线程的RunLoop会在初始化全局化字典时创建。
  3. 子线程的RunLoop会在第一次获取时创建。
  4. 当线程销毁时,对应的RunLoop也会随之销毁。

2.3、CFRunLoopRunSpecific

让我们回到CFRunLoopRunSpecific方法。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)       /* DOES CALLOUT */
{
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //根据modeName找到本次运行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果没有找到mode或者找到的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;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;
    //通知observer即将进入RunLoop
    if (currentMode->_observerMask & kCFRunLoopEntry)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知observer已经退出RunLoop
    if (currentMode->_observerMask & kCFRunLoopExit)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

如上是CFRunLoopRunSpecific方法的实现代码,这段代码看上去复杂,其实很简单。这个方法需要传入四个参数:

  • rl:当前运行的RunLoop对象。
  • modeName:指定RunLoop对象的mode的名称。
  • seconds:RunLoop的超时时间
  • returnAfterSourceHandled:是否在处理完事件之后返回。

从上面的代码我们可以获取到如下几点信息:

  1. RunLoop运行必须要指定一个mode,否则不会运行RunLoop。
  2. 如果指定的mode没有注册时间任务,RunLoop不会运行。
  3. 通知observer进入runloop,调用__CFRunLoopRun方法处理任务,通知observer退出runloop。

2.4、__CFRunLoopRun

如下是__CFRunLoopRun方法的源码(摘自iOS RunLoop详解,小编懒得为代码添加注释☺️☺️)。

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处理各类事件的流程有着详细的描述。

  1. 通知观察者RunLoop已经启动。
  2. 通知观察者定时器即将触发。
  3. 通知观察者任何不基于端口的输入源都将触发。
  4. 触发所有准备触发的非基于端口的输入源。
  5. 如果基于端口的输入源已准备好并等待启动,立即处理事件;并进入步骤9。
  6. 通知观察者线程进入休眠状态。
  7. 使线程进入睡眠状态,直到发生以下事件之一:
    • 某一事件到达基于端口的源。
    • 定时器触发。
    • RunLoop设置的时间已经超时。
    • RunLoop被唤醒。
  8. 通知观察者线程即将被唤醒。
  9. 处理未处理的事件。
    • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
    • 如果输入源启动,传递相应的消息。
    • 如果RunLoop被唤醒而且时间还没超时,重启RunLoop。进入步骤2。
  10. 通知观察者RunLoop结束。

整个流程如下图所示:

2.5、__CFRunLoopServiceMachPort

如果你仔细看过__CFRunLoopRun方法的代码实现,就会发现在其方法内部有一个内置的循环,这个循环会让线程进入休眠状态,直到收到新消息才跳出该循环,继续执行RunLoop。这些消息是基于mach port来进行进程之间的通讯的。

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

如上是__CFRunLoopServiceMachPort的源码,该方法接收指定内核端口的消息,并将消息缓存在缓存区,供外界获取。该方法的核心是mach_msg方法,该方法实现消息的发送个接收。RunLoop调用这个函数去接收消息,如果没有接收到port的消息,内核会将线程置于等待状态。

2.6、RunLoop事件处理

在上一个小节中我们探索了RunLoop运行的核心方法__CFRunLoopRun的代码,根据官方文档的描述总结了事件处理的流程。源码中显示处理事件主要涉及到如下几个方法:

  • __CFRunLoopDoObservers:处理通知事件。
  • __CFRunLoopDoBlocks:处理block事件。
  • __CFRunLoopDoSources0:处理source0事件。
  • __CFRunLoopDoSource1:处理source1事件。
  • __CFRunLoopDoTimers:处理定时器。
  • CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE:GCD主队列

这些方法的实现我们不必关系,但是这些方法在处理事件后如何回调给上层,才是我们需要关心的。比如说__CFRunLoopDoSources0处理的是系统的事件,那么触发一个UIButton的点击事件后,查看函数调用栈应该可以知道回到给上层是如何进行的。

如上图所示UIButton的点击事件的函数调用栈,我们可以清楚的看到__CFRunLoopDoSources0方法的调用,然后调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 回到UIKit层。

关于上述方法回调上层的方法对应如下图所示:

2.7、小结

  1. RunLoop的运行必定要指定一种mode,并且该mode必须注册任务事件。
  2. RunLoop是在默认mode下运行的,当然也可以指定一种mode运行,但是只能在一种mode下运行。
  3. RunLoop内部实际上是维护了一个do-while循环,线程就会一直留在这个循环里面,直到超时或者手动被停止。
  4. RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态,否则线程处理事件。

3、RunLoop应用

3.1、NSTimer

用过NSTimer来做定时器开发的人都知道,NSTimer对象需要添加到RunLoop中才能正确执行。在前面的内容也讲到了CFRunLoopTimerNSTimertoll-free bridged的。一个NSTimer注册到 RunLoop 后,RunLoop会为其重复的时间点注册好事件。尤其是在一个滚动视图中使用NSTimer时,由于其mode的变化导致NSTimer停止工作,解决这个问题的关键就是讲NSTimer注册到RunLoopNSRunLoopCommonModes下。NSTimer更多的内容请移步iOS定时器-- NSTimer&GCD定时器

GCD则不同,GCD的线程管理是通过系统来直接管理的。GCD Timer是通过dispatch portRunLoop发送消息,来使RunLoop执行相应的block,如果所在线程没有RunLoop,那么GCD 会临时创建一个线程去执行block,执行完之后再销毁掉,因此GCDTimer是不依赖RunLoop的。

3.2、AutoReleasepool

一般很少会将自动释放池和RunLoop联系起来,但是如果打印[NSRunLoop currentRunLoop]结果中会发现和自动释放池相关的回调。

<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}

即App启动后,苹果会给RunLoop注册很多个observers,其中有两个是跟自动释放池相关的,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。\

  • 第一个observer监听的是activities=0x1(kCFRunLoopEntry),也就是在即将进入loop时,其回调会调用_objc_autoreleasePoolPush() 创建自动释放池;
  • 第二个observer监听的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit), 即监听的是准备进入睡眠和即将退出loop两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用_objc_autoreleasePoolPop()释放旧的释放池,并调用_objc_autoreleasePoolPush() 创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出loop时则会 _objc_autoreleasePoolPop()释放池子。

关于自动释放池更多的内容请移步iOS内存管理二:自动释放池autoreleasepool

3.3、卡顿检测

我们可以通过RunLoop的不同状态来做页面刷新的卡顿检测。

- (void)start{
    [self registerObserver];
    [self startMonitor];
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    DSBlockMonitor *monitor = (__bridge DSBlockMonitor *)info;
    monitor->activity = activity;
    // 发送信号
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    //NSIntegerMax : 优先级最小
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            NSIntegerMax,
                                                            &CallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

- (void)startMonitor{
    // 创建信号
    _semaphore = dispatch_semaphore_create(0);
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            //超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
            long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
            if (st != 0)
            {
                if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
                {
                    if (++self->_timeoutCount < 2){
                        NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                        continue;
                    }
                    NSLog(@"检测到卡顿");
                }
            }
            self->_timeoutCount = 0;
        }
    });
}

如上面的代码所示就是一段简单的监测页面卡顿的代码,其原理就是根据RunLoop进入kCFRunLoopBeforeSources状态处理source事件到kCFRunLoopAfterWaiting状态变化之间的时间间隔来做判断依据。当然这只是一个简单的demo,但是RunLoop各种状态的变化为很多优秀的卡顿检测的三方库提供了理论基础。

3.4、常驻线程

有的时候我们需要创建一个线程在后台一直做一些任务,但是常规的线程在任务完成后就会立即销毁,因此我们需要一个常驻线程来让线程一直都存在。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run {
    NSRunLoop *currentRl = [NSRunLoop currentRunLoop];
    [currentRl addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [currentRl run];
}

- (void)run2
{
    NSLog(@"常驻线程");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

上面的代码就是一个常驻线程。把线程thread添加在到RunLoop中,通常RunLoop启动前必须要设置一个mode,并且为mode至少设置一个Source/Timer/Observer,在这里是添加了一个port,虽然消息可以通过port发送到RunLoop内,但是这里并没有发送任何的消息,所以这样便可以保持RunLoop不退出,s实现线程常驻。

参考资料