RunLoop 实现原理

🔗 原文:RunLoop 实现原理 - 李峰峰博客

1、RunLoop 的概念

我们都知道,APP 运行过程中有一个很重要的线程,就是主线程。但是,一般线程执行完任务后就会退出,而 APP 需要持续运行,所以就需要一个机制使主线程持续运行并随时处理用户事件,在 iOS 里,程序的持续运行就是通过 RunLoop 实现的。

RunLoop 的作用:

  • 保持程序持续运行; 程序一启动就会开启一个主线程,主线程开启之后会自动运行一个主线程对应的 RunLoop,RunLoop 保证主线程不会被销毁,也就保证了程序的持续运行;

  • 处理 App 中的各种事件 比如:触摸事件,定时器事件等;

  • 节省 CPU 资源,提高程序性能 程序运行起来时,当什么操作都没有做的时候,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情,当有事情做的时候 RunLoop 就会立马起来去做事情;

在 iOS 中,NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,CFRunLoopRef 这些 API 都是线程安全的,Apple 在其文档上对 NSRunLoop 非线程安全的提示:

通常不将 RunLoop 类视为线程安全的,并且只能在当前线程的上下文中调用其方法。永远不要尝试调用在不同线程中运行的 RunLoop 对象的方法,因为这样做可能会导致意外的结果。

CFRunLoopRef 是开源的,源码下载地址:opensource.apple.com/tarballs/CF...

为了源码阅读更容易,便于理解 RunLoop 代码关键逻辑,本文所贴出的源码部分有删减,只留下了关键部分。

2、RunLoop 的数据结构

在 CoreFoundation 中,RunLoop 主要有 5 个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

(1) CFRunLoopRef

NSRunLoop 是基于 CFRunLoopRef 封装的,提供了面向对象的 API,接下来看下 NSRunLoop(即 CFRunLoopRef)的数据结构:

objc 复制代码
typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop
{
    // ......

    // runloop 所对应线程
    pthread_t _pthread;

    //  存放所有标记为 common 的 mode
    CFMutableSetRef _commonModes;

    // 存放 common mode item 的集合(source、timer、observer)
    CFMutableSetRef _commonModeItems;

    // 当前所在 mode
    CFRunLoopModeRef _currentMode;

    // 存放 mode 的集合
    CFMutableSetRef _modes;

    // ......
};

根据以上源码可知,RunLoop 也是一个结构体,即 __CFRunLoop,并且可以看到其中几个关键的成员变量:

  • _commonModes RunLoop 的内容发生变化时,RunLoop 会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到 _commonModes 中所有 Mode 里。主线程的 RunLoop 中 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 都已经被标记为"Common"。 我们可以通过下面方法把一个 Mode 标记为 "Common": CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);

  • _commonModeItems 添加 mode item 的时候,如果 modeName 传入 NSRunLoopCommonModes,则该 mode item 会被保存到 RunLoop 的 _commonModeItems 中,例如: [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

  • _currentMode runloop 当前所在 mode

  • _modes 存放 mode 的集合

也就是说,RunLoop 可以有多个 mode(CFRunLoopModeRef) 对象,但是同一时间只能运行某一种特定的 Mode。

CFRunLoop 对外暴露的管理 Mode 接口只有下面 2 个:

objc 复制代码
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

Mode 暴露的管理 mode item 的接口有下面几个:

objc 复制代码
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop 会自动创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

(2) CFRunLoopModeRef

CFRunLoopModeRef 其实是指向 __CFRunLoopMode 结构体的指针,其源码如下:

objc 复制代码
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode
{
    // ...

    // mode 的名称
    CFStringRef _name;

    // mode 是否停止
    Boolean _stopped;

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

    // ...

};

从以上源码可知,每个 mode 对象中,可以存储多个 source、observer、timer(source/observer/timer 被统称为 mode item)。

系统默认注册了 5 个 Mode:

  • kCFRunLoopDefaultMode App的默认 Mode,通常主线程是在这个 Mode 下运行的,是大多数操作使用的模式。一般情况下,使用此模式来启动运行循环并配置输入源。

  • UIInitializationRunLoopMode 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

  • UITrackingRunLoopMode 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

  • GSEventReceiveRunLoopMode 接受系统事件的内部 Mode。

  • kCFRunLoopCommonModes 这是一个占位用的 Mode,作为标记 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 用,并不是一种真正的 Mode

(3) CFRunLoopSourceRef

CFRunLoopSourceRef 是事件源(输入源)。其分为 source0 和 source1:

  • source0 非基于 port 的,接收点击事件,触摸事件等 APP 内部事件,也就是用户触发的事件。这种 source 是不能主动唤醒 RunLoop 的。 使用时,需要先调用 : CFRunLoopSourceSignal(source) 将这个 Source 标记为待处理,然后再调用: CFRunLoopWakeUp(runloop) 来主动唤醒 RunLoop,让其处理这个事件。

  • source1 基于 Port 的,能主动唤醒 RunLoop,通过内核和其他线程通信,接收分发系统事件;触摸硬件,通过 Source1 接收和分发系统事件到 Source0 处理。

关于 Port 内容后文会进行总结。

CFRunLoopSourceRef 源码如下:

objc 复制代码
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

struct __CFRunLoopSource
{
    // ...

    // source 的优先级,值为小,优先级越高
    CFIndex _order; /* immutable */

    // runloop 集合
    CFMutableBagRef _runLoops;

    // 联合体,表示 source 是 source0 还是 source1
    union {
        CFRunLoopSourceContext version0;  /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;

    // ...
};

(4) CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

objc 复制代码
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};

CFRunLoopObserverRef 源码如下:
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

struct __CFRunLoopObserver
{
    // ...

    // observer 对应的 runloop
    CFRunLoopRef _runLoop;

    // observer 观察了多少个 runloop
    CFIndex _rlCount;

    CFOptionFlags _activities;          /* immutable */

    // observer 优先级
    CFIndex _order;                     /* immutable */

    // observer 回调函数
    CFRunLoopObserverCallBack _callout; /* immutable */

    // ...
};

typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);

例如,监听 RunLoop 的状态:

objc 复制代码
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
        case kCFRunLoopEntry:
        NSLog(@"kCFRunLoopEntry");
        break;
        case kCFRunLoopBeforeTimers:
        NSLog(@"kCFRunLoopBeforeTimers");
        break;
        case kCFRunLoopBeforeSources:
        NSLog(@"kCFRunLoopBeforeSources");
        break;
        case kCFRunLoopBeforeWaiting:
        NSLog(@"kCFRunLoopBeforeWaiting");
        break;
        case kCFRunLoopAfterWaiting:
        NSLog(@"kCFRunLoopAfterWaiting");
        break;
        case kCFRunLoopExit:
        NSLog(@"kCFRunLoopExit");
        break;
        default:
        break;
    }
});
// 把 observer 添加到 RunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

(5) CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,我们常用的 NSTimer 其实就是 CFRunLoopTimerRef ,他们之间是 toll-free bridged 的,可以相互转换。其包含一个时间长度和一个回调(函数指针)。 当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行那个回调。

总结: 一个 RunLoop 中,只能对应一个线程,但是可以包含多个 Mode,每个 mode,可以包含多个 source、observer、timer,其关系如下:

  • RunLoop 启动时只能选择其中一个 Mode,作为 currentMode。
  • 如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入。
  • 不同 Mode 的 Source0/Source1/Timer/Observer 能分隔开来,互不影响。
  • 如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出。

3、RunLoop 的执行流程

当 APP 没有任何任务的时候,RunLoop 会进入休眠,RunLoop 就告诉 CPU,现在没有事情做,我要去休息,这时 CPU 就会将其资源释放出来去做其他的事情。当下次有任务的时候,例如用户点击了屏幕,RunLoop 就会结束休眠开始处理用户的点击事件。所以,为了看到 RunLoop 执行流程,可以在点击事件里加个断点,查看 RunLoop 相关的方法调用栈:

根据上图发现,分析 RunLoop 执行流程,可以从 CFRunLoopRunSpecific、__CFRunLoopRun 函数入手,而对 CFRunLoopRunSpecific 函数的调用,可以在源码中找到,是在 CFRunLoopRun 函数中,源码如下:

objc 复制代码
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 的,并且超时时间设置的非常大:1.0e10,可以理解为不超时。
  • 我们也可以通过 CFRunLoopRunInMode 函数设置自定义启动方式,可以自定义超时时间、mode。

然后进入 CFRunLoopRunSpecific 函数,这是 RunLoop 的核心逻辑:

objc 复制代码
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { {
    
    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    /// __CFRunLoopRun中 具体要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    /// 11. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {

        // 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        // 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        // 4. 处理block
        __CFRunLoopDoBlocks(rl, rlm);

        // 5. 处理Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        // 如果处理Source0的结果是true
        if (sourceHandledThisLoop) {
            // 再次处理block
            __CFRunLoopDoBlocks(rl, rlm);
        }

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        // 6. 如果有Source1 (基于port) 处于ready状态,直接处理这个Source1然后跳转去处理消息。
        if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
            // 如果有Source1, 就跳转到handle_msg
            goto handle_msg;
        }

        // 7. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
    
        // 调用mach_msg等待接受mach_port的消息。线程将进入休眠, 等待别的消息来唤醒当前线程:
        // 一个基于 port 的Source 的事件。
        // 一个 Timer 到时间了
        // RunLoop 自身的超时时间到了
        // 被其他什么调用者手动唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
        
        __CFRunLoopUnsetSleeping(rl);
        // 8. 通知Observers: 结束休眠, RunLoop的线程刚刚被唤醒了
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        
    // 收到消息,处理消息。
    handle_msg:;
        if (/* 被timer唤醒 */) {
            // 01. 处理Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        } else if (/* 被gcd唤醒 */) {
            // 02. 处理gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {  // 被Source1唤醒
            // 处理Source1
            sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
        }
        
        // 9. 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 10. 设置返回值, 根据不同的结果, 处理不同操作
        if (sourceHandledThisLoop && stopAfterHandle) {
            // 进入loop时参数说处理完事件就返回。
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            // 超出传入参数标记的超时时间了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
             // 被外部调用者强制停止了
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            // source/timer/observer一个都没有了
            retVal = kCFRunLoopRunFinished;
        }

        // 如果没超时,mode里没空,loop也没被停止,那继续loop。
    } while (0 == retVal);

    return retVal;
}

由以上源码可知,RunLoop 内部是一个 do-while 循环;当调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

RunLoop 执行流程可用下面这张图概括: 通过上面的执行流程可以发现,RunLoop 处理了很多次 Block,即调用了很多次 __CFRunLoopDoBlocks,那这里处理的 Block 到底是什么 Block 呢? 前面提到了 __CFRunLoop 结构体中的一些常见成员,其实还有两个和 Block 相关的成员:

objc 复制代码
struct __CFRunLoop {
    // ...

    struct _block_item *_blocks_head; // 存放 CFRunLoopPerformBlock 函数添加的 Block 的双向链表的头指针
    struct _block_item *_blocks_tail; // 存放 CFRunLoopPerformBlock 函数添加的 Block 的双向链表的尾指针

    // ...
};

_blocks_head 和 _blocks_tail 就是用于存放 CFRunLoopPerformBlock 函数添加的 Block 的,可见 RunLoop 是将添加的 Block 任务保存在双向链表中的。

我们可以通过 CFRunLoopPerformBlock 将一个 Block 任务加入到 RunLoop:

objc 复制代码
void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void(block)( void));

可以看出添加 Block 任务的时候,是绑定到某个 runloop mode 的。调用上面的 api 之后,runloop 在执行的时候,会通过如下 API 执行对应 mode 中所有的 block:

objc 复制代码
__CFRunLoopDoBlocks(rl, rlm);

需要注意的是,CFRunLoopPerformBlock 不会主动唤醒 RunLoop,添加完 Block 之后可以使用 CFRunLoopWakeUp 来主动唤醒 RunLoop。

4、RunLoop 与线程的关系

CFRunLoop 是基于 pthread 来管理线程的,苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大致如下:

objc 复制代码
// 获得当前线程的 RunLoop 对象,内部调用 _CFRunLoopGet0 函数
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    // 如果为空则t设置为主线程
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    // 如果不存在 RunLoop,则创建
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    // 根据传入的主线程获取主线程对应的 RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    // 保存主线程 将主线程-key 和 RunLoop-Value 保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 从字典里面取 RunLoop:将线程作为 key 从字典里获取 RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    // 如果 RunLoop 为空,则创建一个新的 RunLoop,所以 RunLoop 会在第一次获取的时候创建
    if (!loop) {  
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    // 创建好之后,以线程为 key,RunLoop 为 value,一对一存储在字典中,下次获取的时候,则直接返回字典内的 RunLoop
    if (!loop) { 
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // do not release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    // 线程结束是销毁 loop
    CFRelease(newLoop);
    }
    // 如果传入线程和当前线程相同
    if (pthread_equal(t, pthread_self())) {
        // 注册一个回调,当线程销毁时,顺便也销毁对应的 RunLoop
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

通过源码分析可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 字典里。 所以我们创建子线程 RunLoop 时,只需在子线程中获取当前线程的 RunLoop 对象即可 [NSRunLoop currentRunLoop]。如果不获取,那子线程就不会创建与之相关联的 RunLoop,并且只能在一个线程的内部获取其 RunLoop。 当通过调用 [NSRunLoop currentRunLoop] 方法获取 RunLoop 时,会先看一下字典里有没有子线程对应的 RunLoop,如果有则直接返回 RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop 会被销毁。

Runloop 与线程的关系总结:

  • 每条线程都有唯一的一个与之对应的 RunLoop 对象;
  • RunLoop 保存在一个全局的 Dictionary 里,线程作为 key,RunLoop 作为 value
  • 调用 [NSRunLoop currentRunLoop] 方法获取 RunLoop 时,会先看一下字典里有没有子线程对应的 RunLoop,如果有则直接返回 RunLoop,如果没有则会创建一个,并将对应关系保存到字典里。
  • 主线程的 RunLoop 已经自动创建好了,子线程的 RunLoop 需要主动创建;
  • RunLoop 在第一次获取时创建,在线程结束时销毁;

5、Runloop 的启动与退出

(1) 创建 Runloop

无法直接创建 RunLoop,但是 RunLoop 在第一次获取时自动创建,获取 RunLoop:

objc 复制代码
Foundation
// 获得当前线程的 RunLoop 对象
[NSRunLoop currentRunLoop]; 
// 获得主线程的 RunLoop 对象
[NSRunLoop mainRunLoop]; 

Core Foundation
// 获得当前线程的 RunLoop 对象
CFRunLoopGetCurrent(); 
// 获得主线程的 RunLoop 对象
CFRunLoopGetMain(); 

(2) 启动 Runloop

Apple 把 Runloop 启动方式分成了三种:

  • 无条件地(Unconditionally)
  • 有时间限制(With a set time limit)
  • 指定 Mode(In a particular mode)

这三种方式分别对应下面三个方法:

objc 复制代码
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
  • 第 1 种方式,本质就是在 NSDefaultRunLoopMode 模式下无限循环调用 runMode:beforeDate: 方法,在此期间会处理来自输入源的数据;
  • 第 2 种方式,本质也是在 NSDefaultRunLoopMode 模式下无限循环调用 runMode:beforeDate: 方法,区别在于它达到指定的超时时间后就不会再调用,在此期间会处理来自输入源的数据。
  • 第 3 种方式,Runloop 只会运行一次,达到指定超时时间或者第一个 input source 被处理,则 Runloop 就会退出,这个方法会阻塞当前线程,直到返回结果(YES:输入源被处理或者达到指定的超时值,NO:没有启动成功)。

(3) 退出 Runloop

相较于 Runloop 的启动,它的退出就比较简单了,只有两种方法:

  • 设置超时时间
  • 手动结束

针对前面提到的第 2、3 中启动方式,可以直接设置超时时间控制退出。如果想要手动退出,可以使用下面函数,其参数就是 Runloop 对象:

objc 复制代码
void CFRunLoopStop(CFRunLoopRef rl)

但是 Apple 文档中在介绍利用 CFRunLoopStop() 手动退出时提到:

The difference is that you can use this technique on run loops you started unconditionally.

这里的解释非常容易产生误会,如果在阅读时没有注意到 exit 和 terminate 的微小差异就很容易掉进坑里,因为在 run 方法的文档中还有这句话:

If you want the run loop to terminate, you shouldn't use this method

也就是说,前面三种 Runloop 启动方式,对应退出方式如下:

  • run 无法退出

  • runUntilDate: 只能通过设置超时时间进行退出

  • runMode:beforeDate: 可以通过设置超时时间或者使用 CFRunLoopStop 方法来退出

CFRunLoopStop() 函数只会结束当前的 runMode:beforeDate: 调用,而不会结束后续的调用,这也就是为什么 Runloop 的文档中说 CFRunLoopStop() 可以 exit(退出) 一个 Runloop,而在 run 等方法的文档中又说这样会导致 Runloop 无法 terminate(终结)。

如果既让 Runloop 长时间运行,又要在必要时刻手动退出 Runloop,Apple 官方文档提供了推荐方式:

objc 复制代码
BOOL shouldKeepRunning = YES;        // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

在对应线程中通过如下逻辑退出 Runloop:

objc 复制代码
shouldKeepRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());

6、RunLoop 的底层实现

(1) RunLoop 与 mach port

Apple 将 iOS 系统大致划分为下面 4 个层次: Darwin 的架构如下:

Darwin 是 macOS 和 iOS 操作环境的操作系统部分,Darwin 是一种类 Unix 操作系统(即 Unix 衍生出的系统,在一定程度上继承了原始 Unix 特性),Darwin 的内核是 XNU,XNU 是 Apple 开发的用于 macOS、iOS、tvOS、watchOS 操作系统的内核,XNU 是 X is Not Unix 的缩写。它是一个宏内核 BSD 与微内核 Mach 混合内核,以期将两者的特性兼收并蓄,同时拥有两种内核的优点。

关于 iOS 系统架构相关更多内容,可以看下我的这篇博客:《深入解析 iOS 系统架构》

Mach: Mach 是一个由卡内基梅隆大学开发的计算机操作系统微内核,Mach 核心之上可平行运行多个操作系统,XNU 内核以一个被深度定制的 Mach 内核作为基础。Mach 提供了诸如处理器调度、IPC (进程间通信)等少量且不可或缺的基础 API。在 Mach 中,所有东西都是"对象",进程(在 Mach 中称为任务)、线程和虚拟内存都是对象。但是,在 Mach 架构中,对象间不能相互调用,对象间通信只能通过消息传递。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

BSD: XNU 中的 BSD 代码来自 FreeBSD 内核,FreeBSD 是一种开放源代码的类 Unix 的操作系统,基于 BSD Unix 的源代码派生发展而来。BSD 层确保了 Darwin 系统的 UNIX 特性,真正的内核是 Mach,但是对外部隐藏。BSD 提供了更高层次的抽象 API,例如:基于 Mach 的任务之上的 Unix 进程模型、文件系统、网络协议栈等相关 API。

I/O Kit: I/O Kit 为设备驱动提供了一个面向对象(C++)的一个框架,框架提供每种设备驱动需要的常见特性,以使驱动程序可以用更少的时间和代码完成。

用户态与内核态: 内核控制着操作系统最核心的部分,为了防止应用程序崩溃而导致的内核崩溃,内核与应用程序之间需要进行严格的分离。基于软件的分离会产生巨大的开销,因此现代的操作系统都是依靠硬件来分离。分离的结果就是用户态与内核态。

用户态和内核态的切换有两种类型:

  • 自愿转换:比如系统调用;
  • 非自愿转换:当发生异常、中断或处理器陷阱的时候,代码的执行会被挂起,并且保留发生错误时候的完整状态。控制权被转交给预定义的内核态错误处理程序或中断服务程序。

在 XNU 中,系统调用有四种类别:

  • BSD 系统调用
  • Mach 陷阱
  • 机器相关调用
  • 诊断调用

Mach 消息的发送和接收都是通过同一个 API 函数 mach_msg() 进行的,这个函数在用户态和内核态都有实现。mach_msg() 函数调用了一个 Mach 陷阱(trap),在用户态调用 mach_msg_trap() 会引发陷阱机制,切换到内核态,在内核态中,内核实现的 mach_msg() 会完成实际的工作,如下图: RunLoop 的核心就是基于 mach port 的,其进入休眠时调用的函数是 mach_msg(),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。

前面提到的 source1 就是基于 mach port 的,它用来接收系统事件。当对应系统事件发生后(例如用户点击了屏幕),最终会通过 mach port 将事件转发给需要的 App 进程。随后苹果注册的那个 source1 就会触发回调,RunLoop 被唤醒,APP 开始处理对应事件。

(2) RunLoop 输入源

Runloop 作为线程的入口用来响应传入事件,Runloop 从两种不同类型的源接收事件:

  • 输入源(Input Source) 用于传输异步事件,通常是来自另一个线程或者其他程序的消息。输入源将异步事件传递给相应的处理程序,并调用 runUntilDate: 方法(在线程的关联 NSRunLoop 对象上调用)退出。

  • 定时器源(Timer Source) 提供同步事件,预定的时间或者固定的时间间隔重复执行,计时器源将事件传递给其处理程序,但不会导致 Runloop 退出。

输入源(Input Source) 创建输入源时,可以将其分配给 Runloop 的一种或多种 mode。一般情况下应该在默认模式下运行 Runloop,但也可以指定自定义 mode。如果输入源不在当前监视的 mode 下,则它生成的任何事件都将保留,直到 Runloop 以正确的 mode 运行,输入源主要有:基于的端口的输入源、自定义输入源、Perform Selector 源。

基于的端口的输入源(Port-based Source) 监听应用程序的 Mach 端口,由内核自动发出信号,对应源码中的 source1。

Cocoa 和 Core Foundation 都提供了创建基于的端口输入源相关的对象和函数,如果使用 Cocoa 提供的相关方法,不需要直接创建输入源,可以使用 NSPort 相关的方法来创建一个 Port 对象,并将该对象添加到 Runloop 中,该 Port 对象会负责创建和配置输入源。使用 Core Foundation 函数实现稍微复杂些,我们需要手动的创建 Port 和它的 Runloop 源。使用 CFMachPortRef, CFMessagePortRef, 或者 CFSocketRef 函数来创建适当地对象。

例如:

objc 复制代码
- (void)testsource1 {
    // 声明两个端口
    NSPort *mainPort = [NSMachPort port];
    NSPort *threadPort = [NSMachPort port];
    // 设置线程的端口的代理回调为自己
    threadPort.delegate = self;
    
    // 给主线程 Runloop 加一个端口
    [[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 子线程
        // 给子线程添加一个 Port,并运行子线程中的 Runloop
        [[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
    });
    
    // 2 秒后,从主线程向子线程发送一条消息
    NSString *s1 = @"hello";
    NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
        // 发送一条消息
        // 参数:date(发送时间)、msgid(消息标识)、components(发送消息附带参数)、reserved(预留参数,暂时用不到)
        [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
        
    });
}

// 这个 NSMachPort 收到消息的回调,注意这个参数,可以先给一个id
- (void)handlePortMessage:(id)message
{
    NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
    NSArray *array = [message valueForKeyPath:@"components"];
    NSData *data =  array[1];
    NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@",s1);
}

打印结果:

ini 复制代码
RunLoopTest[10368:5612468] 收到消息了,线程为:<NSThread: 0x6000015acf80>{number = 6, name = (null)}
RunLoopTest[10368:5612468] hello

自定义输入源(Custom input Source) 监听自定义事件源,必须从另一个线程手动发信号通知自定义源,对应源码中的 source0。 可以使用 CoreFoundation 中 CFRunLoopSourceRef 相关的函数来创建自定义输入源,可以使用多个回调函数配置自定义输入源,CoreFoundation 在必要时候调用这些函数来配置 source,处理传入的事件,并在从 Runloop 中移除 source 时将其移除。 除此之外,还需要定义事件的传递机制,这部分是运行在单独的线程上,负责向输入源提供数据并在适当的时候发出信号,事件的传递机制可自行定义。

例如:

objc 复制代码
@implementation ViewController{
    CFRunLoopRef _runLoopRef;
    CFRunLoopSourceRef _source;
    CFRunLoopSourceContext _source_context;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self testsource0];
}

- (void)testsource0 {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"starting thread.......");
        
        _runLoopRef = CFRunLoopGetCurrent();
        // 初始化_source_context。
        bzero(&_source_context, sizeof(_source_context));
        // 这里创建了一个基于事件的源,绑定了一个函数
        _source_context.perform = fire;
        //参数
        _source_context.info = "hello";
        
        // 创建一个source
        _source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
        
        // 将 source 添加到当前 RunLoop 中去
        CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);
        
        // 开启 RunLoop 第三个参数设置为 YES,执行完一次事件后返回
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);
        
        NSLog(@"end thread.......");
    });
    
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        if (CFRunLoopIsWaiting(_runLoopRef)) {
            NSLog(@"RunLoop 正在等待事件输入");
            // 添加输入事件
            CFRunLoopSourceSignal(_source);
            // 唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
            CFRunLoopWakeUp(_runLoopRef);
        }else {
            NSLog(@"RunLoop 正在处理事件");
            // 添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
            CFRunLoopSourceSignal(_source);
        }
    });
    
}

static void fire(void* info) {
    NSLog(@"我现在正在处理后台任务");
    printf("%s",info);
}

@end

打印结果:

objc 复制代码
RunLoopTest[10630:5649707] starting thread.......
RunLoopTest[10630:5649457] RunLoop 正在等待事件输入
RunLoopTest[10630:5649707] 我现在正在处理后台任务
RunLoopTest[10630:5649707] end thread.......

Perform Selector 源(Cocoa Perform Selector Source) 除了基于端口的源外,Cocoa 还定义了一个自定义输入源,允许在任何线程上 Perfrom Selector。与基于端口的源一样,Perfrom Selector 请求在目标线程上序列化,缓解了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同的是,Perform Selector 源在 Perfrom Selector 后会从 Runloop 中删除自己。

在另一个线程上 Perfrom Selector 时,目标线程必须具有活动的 Runloop。这意味对于我们创建的子线程,需要显式创建 Runloop。由于主线程的 Runloop 是自动创建的,所以可以在 applicationDidFinishLaunching:方法后随时 Perfrom Selector。Runloop 每次循环时,Runloop 都会处理所有的 Perfrom Selector 调用,而不是在每次循环时都只处理一个。

在其他线程上 Perfrom Selector 的相关方法:

objc 复制代码
// 在主线程的下一个 Runloop 周期中,执行指定的选择器。这些方法允许您选择阻塞当前线程,直到执行选择器结束。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

// 对拥有 NSThread 对象的任何线程执行指定的选择器。这些方法允许您选择阻塞当前线程,直到执行选择器结束。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

// 在下一个 Runloop 周期和指定时长延迟(可选)后,在当前线程上执行指定的选择器。由于它要等到下一个 Runloop 周期来执行选择器,所以这些方法提供了一个来自当前执行代码的自动最小延迟,多个选择器时会按照顺序依次执行。
// performSelector:afterDelay: 方法内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

// 取消使用 performSelector:withObject:afterDelay: 或 performSelector:withObject:afterDelay:inModes: 发送的到当前线程的消息
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

计时器源(Timer Source) 定时器源在预定时间内同步地将事件传递给线程,定时器可以让线程在对应时刻通知自己执行一些事务,尽管定时器是基于时间的通知方式,但是并不是真的时间机制。就像输入源,定时器在 Runloop 中也是和特定 Mode 相关联的。如果定时器没有处在 Runloop 正在监视的 Mode 中的话,该定时器是不会触发的。必须要等到 Runloop 在定时器支持的 Mode 中运行时,该定时器才会正常运行。如果定时器被触发时机正好是在 Runloop 执行任务中,那么这个定时器源的相关事件只有在 Runloop 下一次运行循环时才能被执行。如果 Runloop 停止运行,那么该定时器源的事件将永远没办法执行。

反复执行的定时器会根据它的触发时间自动配置,并不是真实的触发时间。例如,一个定时器设置的是每 5 秒触发一次,在真实时间上可能是有点延迟的,如果真实时间的延迟大于定时器触发时间的话,那么这次触发时机将被错过。

创建定时器源,可以使用下面两个方法:

objc 复制代码
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:

上面这两个方法创建了定时器并添加到当前线程的默认 mode (NSDefaultRunLoopMode)中,但是也可以通过 NSRunLoop 的下面的实例方法来将 NSTimer 对象添加到其他 mode 中: addTimer:forMode:

例如,下面两种实现方式效果是一样的:

objc 复制代码
/// 分开处理,我们可以通过更多的自定义方式来处理timer,比如添加到不同的NSDefaultRunLoopMode。
NSDate *futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer *myTimer = [[NSTimer alloc] initWithFireDate:futureDate interval:0.1 target:self selector:@selector(timedothing:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];

/// 将创建和调度同时进行
[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(timedothing:) userInfo:nil repeats:YES];

7、RunLoop 目前的应用

(1) AutoreleasePool

  • RunLoop 的进入的时候会调用 objc_autoreleasePoolPush() 创建新的自动释放池。
  • RunLoop 的进入休眠的时候会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 销毁自动释放池、创建一个新的自动释放池。
  • RunLoop 即将退出时会调用 objc_autoreleasePoolPop() 释放自动自动释放池内对象。

(2) 事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

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

(3) 手势识别

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

苹果注册了一个 Observer 监测 BeforeWaiting (Loop 即将进入休眠) 事件,这个 Observe r的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

(4) 界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

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

objc 复制代码
_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];

(5) 定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

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

(6) PerformSelector

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

performSelector:withObject: 只是发消息,不会有 Timer ,所以不会有上面的问题,在子线程调用,不需要开启 Runloop

(7) 关于 GCD

根据前面 RunLoop 的执行流程可以知道,GCD 也是可以唤醒 RunLoop 的,GCD 由子线程返回到 主线程,只有在这种情况下才会触发 RunLoop,会触发 RunLoop 的 Source 1 事件:

objc 复制代码
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"main queue");
});

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop 会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

(8) 关于网络请求

iOS 中,关于网络请求的接口有如下几层: CFSocket 是最底层的接口,只负责 socket 通信。

  • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。

  • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。

+ NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking 2 和 Alamofire 工作于这一层。

下面主要介绍下 NSURLConnection 的工作过程。

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了 4 个 Source0 (即需要手动触发的 Source)。其中 CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.CFSocket.private 和 com.apple.NSURLConnectionLoader。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。 NSURLConnectionLoader 中的 RunLoop 接收来自底层 CFSocket 的 Source1 通知。当收到通知后,在合适的时机向 Delegate 线程 RunLoop 发送 CFMultiplexerSource 等 Source0 通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。接收到 CFMultiplexerSource 通知后,Delegate 线程的 RunLoop 执行对应 Delegate 回调。

8、Runloop 开发中的使用场景

唯一需要显式运行 Runloop 是在创建子线程时。主线程的 Runloop 已自动创建并运行。对于子线程,需要自行判断是否需要 Runloop,如果需要,则开发者自行创建。例如,下列操作需要启动 Runloop:

  • 使用端口或自定义输入源与其他线程通信。
  • 在子线程上使用计时器。
  • 调用 performSelector... 相关方法。
  • 线程保活,以执行周期性任务。

以下是在实际开发是,Runloop 的一些使用场景:

(1) 线程保活

当子线程中的任务执行完毕后,线程就被会被立刻销毁。如果 APP 中需要经常在子线程中执行任务,频繁的创建和销毁线程,会造成资源的浪费,这时候我们就可以使用 Runloop 来让该线程长时间存活而不被销毁,实现如下:

objc 复制代码
KeepAliveThread.h
typedef void (^KeepAliveThreadTask)(void);

@interface KeepAliveThread : NSObject

// 在子线程执行任务
- (void)executeTask:(KeepAliveThreadTask)task;

// 结束线程
- (void)stop;

@end

KeepAliveThread.m
@interface KeepAliveThread()

@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, assign) BOOL shouldKeepRunning;

@end

@implementation KeepAliveThread

#pragma mark - Public methods
- (instancetype)init {
    if (self = [super init]) {
        self.shouldKeepRunning = YES;
        
        __weak typeof(self) weakSelf = self;
        
        self.thread = [[NSThread alloc] initWithBlock:^{
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            while (weakSelf && weakSelf.shouldKeepRunning) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];
        
        [self.thread start];
    }
    return self;
}

- (void)executeTask:(KeepAliveThreadTask)task {
    if (!self.thread || !task) return;
    [self performSelector:@selector(p_executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}

- (void)stop {
    if (!self.thread) return;
    
    [self performSelector:@selector(p_stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}

- (void)dealloc {
    [self stop];
}

#pragma mark - Private methods
- (void)p_stop {
    self.shouldKeepRunning = NO;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
}

- (void)p_executeTask:(KeepAliveThreadTask)task {
    task();
}

@end

(2) 保证 Timer 正常运行

创建 Timer 有下面两种方式,两种实现方式是等价的:

objc 复制代码
// 方式 1
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];

// 方式 2
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];

当滑动 UIScrollView 时,主线程的 RunLoop 会切换到 UITrackingRunLoopMode 这个 Mode,执行的也是 UITrackingRunLoopMode 下的任务(Mode 中的 item),而 Timer 是添加在 NSDefaultRunLoopMode 下的,所以 Timer 任务并不会执行,只有当 UITrackingRunLoopMode 的任务执行完毕,Runloop 切换到 NSDefaultRunLoopMode 后,才会继续执行 Timer。解决方法很简单,我们只需要在添加 Timer 时,将 mode 设置为 NSRunLoopCommonModes 即可:

objc 复制代码
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];

如果是在子线程中使用 Timer,由于子线程的 Runloop 并不会自动创建,所以必须在子线程中创建并启动 Runloop,否则 Timer 无法正常运行,创建并启动 Runloop 方法:

objc 复制代码
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

由于子线程中不会涉及到 UI 更新,所以无需再主动将 Timer 添加到 NSRunLoopCommonModes 下。

(3) 利用 Runloop 优化 UITableView 加载图片时滑动卡顿问题

UITableView 滚动时,主线程的 Runloop 会切换到 UITrackingRunLoopMode 这个 Mode,我们可以在 NSDefaultRunLoopMode 中设置图片,避免一边滑动一边设置 image 引起的卡顿问题:

objc 复制代码
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = ...
    // ......
    // 在 NSDefaultRunLoopMode 下设置图片
    [self performSelector:@selector(p_loadImgeWithIndexPath:)
                       withObject:indexPath
                       afterDelay:0.0
                          inModes:@[NSDefaultRunLoopMode]];
    // ......
    return cell;
}

- (void)p_loadImgeWithIndexPath:(NSIndexPath *)indexPath{
    
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
    
    // 子线程下载图片
    [ImageDownload loadImageWithUrl:@"xxxx" success:^(UIImage *image) {
        // 回到主线程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            cell.imageView.image = image;
        });
    }];
}

(4) 利用 Runloop 监控卡顿

根据 Runloop 的执行流程可以发现,Runloop 对我们业务逻辑的处理时间在两个阶段:

  • kCFRunLoopBeforeSources 和 kCFRunLoopBeforeWaiting 之间
  • kCFRunLoopAfterWaiting 之后

所以,如果主线程 Runloop 处在 kCFRunLoopBeforeSources 时间过长,也就是迟迟无法将任务处理完成,顺利到达 kCFRunLoopBeforeWaiting 阶段,说明发生了卡顿。

同样的,如果 Runloop 处在 kCFRunLoopAfterWaiting 时间过长,也是发生了卡顿。

所以,如果我们要利用 Runloop 来监控卡顿的话,就要关注 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个阶段,一般卡顿时间超过 250ms 会被明显感知,所以,可以以连续 5 次卡顿时长超过 50ms 可以认为发生卡顿,或者根据需要调整统计阀值。以下是通过 Runloop 监听卡顿的一个例子:

objc 复制代码
@interface LagMonitor() {
    int timeoutCount;
    CFRunLoopObserverRef runLoopObserver;
    @public
    dispatch_semaphore_t dispatchSemaphore;
    CFRunLoopActivity runLoopActivity;
}

@end

@implementation LagMonitor

#pragma mark - Interface
+ (instancetype)shareInstance {
    static id instance = nil;
    static dispatch_once_t dispatchOnce;
    dispatch_once(&dispatchOnce, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

// 开始监控
- (void)beginMonitor {
    NSLog(@"开始监控");
    if (runLoopObserver) {
        return;
    }
    
    // 创建信号量,注意这里信号量为 0
    dispatchSemaphore = dispatch_semaphore_create(0);
    // 创建 Observer
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              runLoopObserverCallBack,
                                              &context);
    // 将 Observer 添加到主线程的 RunLoop
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    
    // 在子线程持续监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES) {
            /**
             信号量 P 操作,成功的话信号量会 - 1,这里超时时间为 50ms,即等待 50ms 后还没成功就返回失败
             操作成功,信号量 - 1,返回值为 0;操作失败,返回值非 0
             由于初始信号量为 0,这里会阻塞,直到 runLoopObserverCallBack 函数中对信号量做了 V 操作,即 RunLoop 状态发生改变的时候。
             */
            long semaphoreWait = dispatch_semaphore_wait(self->dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
            if (semaphoreWait != 0) {
                // 发生超时,说明 RunLoop 保持在一个状态的时间超过了 50ms
                if (!self->runLoopObserver) {
                    self->timeoutCount = 0;
                    self->dispatchSemaphore = 0;
                    self->runLoopActivity = 0;
                    return;
                }
                // 如果是在 BeforeSources 或 AfterWaiting 这两个状态持续时间达到超时时间,就代表发生了卡顿
                if (self->runLoopActivity == kCFRunLoopBeforeSources || self->runLoopActivity == kCFRunLoopAfterWaiting) {
                    // 出现五次出结果
                    if (++self->timeoutCount < 5) {
                        continue;
                    }
                    // 发生了卡顿,可以使用 PLCrashReporter 等收集堆栈并上报
                    NSLog(@"发生了卡顿,");
                }
            }
            self->timeoutCount = 0;
        }
    });
    
}


// 结束监控
- (void)endMonitor {
    NSLog(@"结束监控");
    if (!runLoopObserver) {
        return;
    }
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    // 释放 Observer
    CFRelease(runLoopObserver);
    runLoopObserver = NULL;
}

#pragma mark - Private
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    LagMonitor *lagMonitor = (__bridge LagMonitor*)info;
    lagMonitor->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    // 对信号量进行 V 操作,信号量 + 1
    dispatch_semaphore_signal(semaphore);
}

@end

上面只是统计卡顿的基础版本,如果真的使用到项目中上面逻辑还有不少需要优化的地方,例如:

  • 避免多次重复上报同一个卡顿堆栈
    • 可以先将堆栈保存到内存中,以堆栈栈顶函数为特征,如果相同认为整个堆栈是同一个,不重复上报。
  • 准确定位真正卡顿的堆栈
    • 假如主线程有三个任务,只有第一个是引起卡顿的任务,当开始上报卡顿时获取到的堆栈可能是后两个不耗时的任务的堆栈。这种情况可以每 50ms 甚至更短时间获取一次堆栈,只保留最近一定数量(例如最近 20 个)堆栈信息,当发生卡顿时相同堆栈数量最多的堆栈就是真正引起卡顿的堆栈。

目前也有一些比较成熟的卡顿监控方案,例如:matrix。

🔗 原文链接:
RunLoop 实现原理 - 李峰峰博客
📢 作者声明:

转载请注明来源

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端