一、RunLoop简介

1.RunLoop介绍

一个线程一次只能执行一个任务,执行完毕后就会退出。如果需要一个机制,让线程能随时处理事件,处理完毕后并不退出,代码逻辑是这样的:

do {
   //获取消息
   //处理消息
} while (消息 != 退出)

这种模型通常被称为Event Loop,Event loop 在很多系统和框架都有实现。例如:Node.js 的事件处理,Windows 程序的消息循环,而在macOS、iOS中就是Run Loop。
简单的说RunLoop其实就是一种循环,通过这种循环得以让应用程序持续运行,让线程能随时处理事件,并在线程需要进行事件处理时忙起来,在不需要进行事件处理时闲下来。

2.RunLoop功能

  • 保持程序的持续运行
  • 处理app中各种事件
  • 节省CPU资源,提高程序性能:该做事时做事,该休眠时休眠。并且休眠时不占用CPU

3.RunLoop使用场景

只有在创建辅助线程时,才需要显式启动RunLoop。app 启动过程中主线程会自动启动RunLoop。即使用 Xcode 提供的模版创建 app,无需为主线程显式启动RunLoop。

RunLoop用于需要与线程交互的情况。对于辅助线程,根据需要配置、启动 RunLoop,但并非所有线程都需要使用 RunLoop。例如,执行一些长时间运行的固定任务,则无需启动RunLoop。如有以下需求,则需要启动RunLoop:

  1. 使用端口或自定义 input source 与其他线程通讯。
  2. 在线程上使用计时器。
  3. 使用任一 performSelector 方法。
  4. 线程需要定期执行任务。

RunLoop 的配置和启动也很简单,但需要在某些条件下退出 RunLoop。最好清理所有工作后退出RunLoop,而非强制终止线程。

4.Run Loop 与线程

下图为RunLoop与线程的关系:
在这里插入图片描述
Runloop 在线程中的作用主要是从 input source 和 timer source 接受事件,然后在线程中处理事件。

输入源(Input source):异步传递事件,通常是从其他线程、app 发送的消息。
计时器源(Timer source):同步传递事件,这些事件在计划的时间、间隔触发。
Input source 传递异步事件到对应处理程序,并退出runUntilDate:方法。Timer source 传递事件到对应处理程序,但不会退出 run loop。

runUntilDate:方法在指定时间到达前会保持RunLoop 处于运行状态,RunLoop运行时会处理 input source 的各种事件。

Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。
主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。

除处理 input source 传递的任务,RunLoop还会发送其当前状态的通知。注册观察RunLoop状态变化,可以在状态变化时进行额外工作。使用 Core Foundation 的CFRunLoopObserverCreate()CFRunLoopObserverCreateWithHandler()方法创建观察者,CFRunLoopAddObserver()方法添加观察者,最后使用CFRelease()释放observer。

5.RunLoop源代码和模型图

在这里插入图片描述

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其实也是个对象,一个RunLoop对象,主要包含了一个线程,若干个Mode,若干个commonMode,若干个commonModeItems还有一个当前运行的Mode(currentMode)。

二、RunLoop Mode

1.CFRunLoopModeRef

RunLoop的Mode属于CFRunLoopModeRef类,其代码结构如下:

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


  • 通过源代码可以看出一个CFRunLoopModeRef对象有一个name,若干source0,source1,timer,observer和port,可以看出来事件都是由mode在管理,而RunLoop管理着Mode。
  • Runloop Mode 实际上是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的
    Source,Timer 和 Observer 隔绝开来。Runloop 在某个时刻只能跑在一个 Mode 下(这个 mode 被称作 currentMode),处理这一个 Mode当中的 Source,Timer 和 Observer。如果需要切换 mode,需退出 loop,重新进入要切换的 mode。这样做可以隔离不同组 source、timer、observer,让其互不影响。

正如下图所示:
在这里插入图片描述
Mode 可用于过滤不需要的 source。大部分情况下,使用系统定义的 default mode 即可。UIScrollView滑动时主线程会进入UITrackingRunLoopMode。此时,只有与UITrackingRunLoopMode关联的 mode 可以向主线程传递事件。对于辅助线程,可以使用自定义 mode 来防止低优先级 source 在时间紧迫的操作期间传递事件。

2.RunLoop Mode的五种模式

  • NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行
  • NSConnectionReplyMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • NSModalPanelRunLoopMode:当应用程序显示一个模态对话框时使用此模式。在此模式下,RunLoop只处理与模态面板相关的事件,忽略其他非模态事件,确保用户只能与模态面板交互。
  • NSEventTrackingRunLoopMode:接受系统事件的内部 Mode,通常用不到
  • NSRunLoopCommonModes:这不是一个单一的模式,而是一个特殊的集合,包含了多个模式,其中最典型的就是默认模式(NSDefaultRunLoopMode)和事件跟踪模式(NSEventTrackingRunLoopMode)。将输入源(如定时器)添加到NSRunLoopCommonModes,意味着它会在所有包含的模式下有效,即使RunLoop切换模式也不会停止工作。

CommonModes:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的
“commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的
Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

3.RunLoop Mode使用

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

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

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

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 只能增加不能删除。

官方公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

同时官方还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。


三、RunLoop Source

Run Loop Source分为Source、Observer、Timer三种,他们统称为ModeItem。

1.CFRunLoopSourceRef

根据官方描述,CFRunLoopSourceRef是input sources的抽象。
CFRunLoopSource分为Source0Source1两个版本。
它的结构如下:

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

sourc0:

是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态,以下是source0的结构体:

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;

source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。适用于那些不需要底层硬件或系统事件驱动的场景,比如基于条件的任务,当满足某个条件时手动触发。

source1:

source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体:

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);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *  (*getPort)(void *info);
    void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Source1包含了mach_port和一个回调(函数指针),Source1可以监听系统端口,通过内核和其他线程通信,接收、分发系统事件,他能主动唤醒RunLoop(由操作系统内核进行管理)。并且Source1在处理的时候会分发一些操作给Source0去处理。广泛应用于处理系统级事件,如触摸事件、摇晃、锁屏等,以及进程间通信(IPC)。由于它直接与系统内核和其他进程或线程的事件交互,因此可以实现高效的事件传递和响应。

2.CFRunLoopTimerRef

CFRunLoopTimer是基于时间的触发器,其包含一个时间长度、一个回调(函数指针)。当其加入runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行那个回调。
并且CFRunLoopTimer和NSTimer是toll-free bridged(对象桥接),可以相互转换。其结构如下:

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

Timer source 在预设时间将事件同步传递到线程。Timer 是线程通知自己执行任务的一种方式。例如,用户在搜索框键入字符时,使用计时器间隔指定时间搜索一次,以便用户输入更多文字,也可以提高性能。

尽管 Timer 会产生基于时间的通知,但它不是实时的机制。与 input source 一样,timer 也与RunLoop Mode 关联。如果计时器触发时,RunLoop正在执行其他回调,计时器会等待 RunLoop执行其回调;如果RunLoop未运行,timer 永不触发。

计时器可以配置为一次性或重复生成事件。重复计时器会根据计划的触发时间,而非实际触发时间,自动重新计划自身。例如,计时器计划在指定时间触发,并每5秒触发一次,则触发时间永远会落在原始时间的5秒间隔上。如果触发时间被延误很多,导致缺少一个或多个触发时间,计时器将在错过的时间段内仅触发一次。此后,计时器将重新计划下一个计划的触发时间。

对于NSTimer,当我们用scheduledTimerWithTimeInterval方法初始化定时器时

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

系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以所以它就等同于下面代码:

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

3.CFRunLoopObserverRef

CFRunLoopObserverRef是观察者可以观察Runloop的各种状态,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化。

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;//监听的RunLoop
    CFIndex _rlCount;//添加该Observer的RunLoop对象个数
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;//同时间最多只能监听一个
    CFRunLoopObserverCallBack _callout;//监听的回调
    CFRunLoopObserverContext _context;//上下文用于内存管理
};

Run loop 可观察时间点有以下几个:

/* Run Loop Observer Activities */
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
    kCFRunLoopAllActivities = 0x0FFFFFFFU   // 观察所有状态变化
};

Runloop 通过监控 Source 来决定有没有任务要做,除此之外,我们还可以用 Runloop Observer 来监控 Runloop 本身的状态。 Runloop Observer 可以监控上面的 Runloop 事件,具体流程如下图:
在这里插入图片描述
这六种状态都可以被observer观察到,我们也可以利用这一方法写一些特殊事件,创建监听,监听RunLoop的状态变化:

    // 创建 observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry -- %@", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
                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:
                NSLog(@"default");
                break;
        }
    });
    // 将 observer 添加到主线程的 run loop 的 common modes
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放 observer
    CFRelease(observer);

在这里插入图片描述

四、RunLoop实现

1.获取RunLoop对象

需要知道的是RunLoop对象不能手动创建,前面提到每个线程都有一个RunLoop对象所以只能通过线程来获取对应的RunLoop对象。
Runloop对象主要有两种获取方式:

// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

NSRunloop类是Fundation框架中Runloop的对象,并且NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。

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

CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全。

CoreFoundation框架中这两个函数的具体实现:

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

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

这两个方法里都是调用了_CFRunLoopGet0这个方法实现传入线程的函数,下面看下CFRunLoopGet0的结构:

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;
 
// t==0 is a synonym for "main thread" that always works
//根据线程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    //如果存储RunLoop的字典不存在
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
        //创建一个临时字典dict
        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)) {
            //释放dict
            CFRelease(dict);
        }
        //释放mainrunloop
        CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //以上说明,第一次进来的时候,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建
    //从字典__CFRunLoops中获取传入线程t的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    //如果没有获取到
    if (!loop) {
        //根据线程t创建一个runloop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //把newLoop存入字典__CFRunLoops,key是线程t
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&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;
}


这段代码可以得出以下结论:

  • RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中
  • 主线程的RunLoop会在初始化全局字典时创建
  • 子线程的RunLoop会在第一次获取的时候创建,如果不获取的话就一直不会被创建
  • RunLoop会在线程销毁时销毁

2.添加Mode

对于Mode苹果官方提供了以下三个接口:

//向当前RunLoop的common modes中添加一个mode。
CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
//返回当前运行的mode的name
CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
//返回当前RunLoop的所有mode
CFRunLoopCopyAllModes(CFRunLoopRef rl)

我们没有办法直接创建一个CFRunLoopMode对象,但是我们可以调用CFRunLoopAddCommonMode方法传入一个字符串向RunLoop中添加Mode,传入的字符串即为Mode的名字,Mode对象应该是此时在RunLoop内部创建的。
下面是CFRunLoopAddCommonMode源码:

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

  • modeName不能重复,modeName是mode的唯一标识符
  • RunLoop的_commonModes数组存放所有被标记为common的mode的名称(name)
  • 添加commonMode会把commonModeItems数组中的所有source同步到新添加的mode中
  • CFRunLoopMode对象在CFRunLoopAddItemsToCommonMode函数中调用CFRunLoopFindMode时被创建

3.添加Run Loop Source(ModeItem)

在RunLoop Mode部分已经介绍过管理RunLoop Source的几个接口方法,这里就不再赘述。
CFRunLoopAddSource的代码结构如下:

//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    if (!__CFIsValid(rls)) return;
    Boolean doVer0Callout = false;
    __CFRunLoopLock(rl);
    //如果是kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //如果runloop的_commonModes存在,则copy一个新的复制给set
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
       //如果runl _commonModeItems为空
        if (NULL == rl->_commonModeItems) {
            //先初始化
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //把传入的CFRunLoopSourceRef加入_commonModeItems
        CFSetAddValue(rl->_commonModeItems, rls);
        //如果刚才set copy到的数组里有数据
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rls};
            /* add new item to all common-modes */
            //则把set里的所有mode都执行一遍__CFRunLoopAddItemToCommonModes函数
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        //以上分支的逻辑就是,如果你往kCFRunLoopCommonModes里面添加一个source,那么所有_commonModes里的mode都会添加这个source
    } else {
        //根据modeName查找mode
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        //如果_sources0不存在,则初始化_sources0,_sources0和_portToV1SourceMap
        if (NULL != rlm && NULL == rlm->_sources0) {
            rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
            rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
        }
        //如果_sources0和_sources1中都不包含传入的source
        if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
            //如果version是0,则加到_sources0
            if (0 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources0, rls);
                //如果version是1,则加到_sources1
            } else if (1 == rls->_context.version0.version) {
                CFSetAddValue(rlm->_sources1, rls);
                __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                if (CFPORT_NULL != src_port) {
                    //此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来
                    //可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloop
                    CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                    __CFPortSetInsert(src_port, rlm->_portSet);
                }
            }
            __CFRunLoopSourceLock(rls);
            //把runloop加入到source的_runLoops中
            if (NULL == rls->_runLoops) {
                rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
            }
            CFBagAddValue(rls->_runLoops, rl);
            __CFRunLoopSourceUnlock(rls);
            if (0 == rls->_context.version0.version) {
                if (NULL != rls->_context.version0.schedule) {
                    doVer0Callout = true;
                }
            }
        }
        if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
    }
    __CFRunLoopUnlock(rl);
    if (doVer0Callout) {
        // although it looses some protection for the source, we have no choice but
        // to do this after unlocking the run loop and mode locks, to avoid deadlocks
        // where the source wants to take a lock which is already held in another
        // thread which is itself waiting for a run loop/mode lock
        rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
    }
}

通过添加source的这段代码可以得出如下结论:

  • 如果modeName传入kCFRunLoopCommonModes,则该source会被保存到RunLoop的_commonModeItems中
  • 如果modeName传入kCFRunLoopCommonModes,则该source会被添加到所有commonMode中
  • 如果modeName传入的不是kCFRunLoopCommonModes,则会先查找该Mode,如果没有,会创建一个
    同一个source在一个mode中只能被添加一次

五、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的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
	//首先根据modeName找到对应的mode
	CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
	//如果该mode中没有source/timer/observer,直接返回
	if (__CFRunLoopModeIsEmpty(currentMode)) return;
	
	//1.通知Observers:RunLoop即将进入loop
	__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

	//调用函数__CFRunLoopRun 进入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)回调,处理Source0
			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的线程即将进入休眠
			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);

			//收到消息,处理消息
			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);

			//设置do-while之后的返回值
			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);
}


实际上RunLoop就是这样的一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里,知道超时或者被手动调用,该函数才会返回。

RunLoop回调(流程)

当App启动时,系统会默认注册五个上面说过的5个mode
当RunLoop进行回调时,一般都是通过一个很长的函数调出去(call out),当在代码中加断点调试时,通常能在调用栈上看到这些函数。这就是RunLoop的流程:

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

RunLoop启动方法

1.run,无条件
无条件地进入运行循环是最简单的选项,但也是最不理想的选择。无条件地运行runloop将线程放入永久循环,这使您无法控制运行循环本身。停止runloop的唯一方法是杀死它。也没有办法在自定义模式下运行循环。
2.runUntilDate, 设置时间限制
设置了超时时间,超过这个时间runloop结束,优于第一种
3.runMode:beforeDate:,在特定模式下
相对比较好的方式,可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会自动退出,上述两种方式都是循环调用的
实际上run方法的实现就是无限调用runMode:beforeDate:方法
runUntilDate:也会重复调用runMode:beforeDate:方法,区别在于它超时就不会再调用
RunLoop关闭方法

1.将运行循环配置为使用超时值运行。
2.手动停止。
这里需要注意,虽然删除runloop的输入源和定时器可能会导致运行循环的退出,但这并不是个可靠的方法,系统可能会添加输入源到runloop中,但在我们的代码中可能并不知道这些输入源,因此无法删除它们,导致无法退出runloop。

我们可以通过上述2、3方法来启动runloop,设置超时时间。但是如果需要对这个线程和它的RunLoop有最精确的控制,而并不是依赖超时机制,这时我们可以通过 CFRunLoopStop()方法来手动结束一个 RunLoop。但是 CFRunLoopStop()方法只会结束当前正在执行的这次runMode:beforeDate:调用,而不会结束后续runloop的调用。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐