[iOS]-RunLoop

参考的博客:

iOS RunLoop详解
[iOS开发]Runloop
iOS八股文(二十)Runloop探究
iOS RunLoop详解
看苹果官方文档怎么说RunLoop

RunLoop初探

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是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠

App运行的过程中,主线程的Runloop保证了主线程不被销毁从而保证应用的存活,从而能实时接收到用户的响应事件,能够触发定时事件。如果没有Runloop的话,程序执行完代码就会立马return

RunLoop对象的获取

iOS中有2套关于RunloopAPICoreFoundationCFRunloopRefFundationdeNSRunloop

- (void)runloopApi {
	//Foundation
    NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [runloop run];//运行RunLoop
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

	//Core Foundation
    CFRunLoopRef runLoopSecond = CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopRun();//运行RunLoop(在默认模式下无限期地运行当前线程的CFRunLoop对象)
    CFRunLoopRef runLoopThird = CFRunLoopGetMain();// 获得主线程的RunLoop对象
}

在这里插入图片描述

接着我们来看一下CoreFoundation中这两种获取RunLoop对象的实现:

//获取当前线程的RunLoop对象
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

//获取主线程的RunLoop对象
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这个函数,后面再进行讲解。

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的自动启动情况:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}


int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

其中的UIApplicationMain函数内部帮我们开启了主线程的RunLoop
UIApplicationMain内部拥有一个无限循环的代码:

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

程序会一直在do-while循环中执行。

苹果官方对于RunLoop与线程的关系如下图所示:
在这里插入图片描述
从上图中可以看出,RunLoop在线程中不断检测,通过input sourcetimer source接受事件,然后通知线程进行处理事件

RunLoop的结构

RunLoop源码下载地址: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,只是在处理上有所不同,不过在某一时刻Runloopmode是只有一个的(也就是上面的成员变量_currentMode

在上面的那一众成员变量中,我们可以聚焦观察这三个:

//RunLoop对应的线程
pthread_t _pthread;
CFRunLoopModeRef _currentMode;//当前运行的模式
CFMutableSetRef _modes;//存储的是CFRunLoopModeRef

下面我们浅浅总结一下:

Runloop和线程的关系

Runloop和线程是一对一的关系,每个线程中都有一个唯一的Runloop,每个Runloop都对应着一个线程,其中主线程的Runloop是默认开启的,子线程的Runloop默认是关闭状态的,如果要在子线程使用Runloop一定要记得开启Runloop。这一点从CFRunloop的数据结构中也能看出。

Runloop和RunloopMode的关系

RunloopRunloopMode是一对多的关系,Runloop中有多个RunloopMode(注意成员变量_commonModes的类型为Set)。而在某一时刻Runloopmode是只有一个的(也就是上面的成员变量_currentMode

RunLoop 有五种运行模式(Mode):

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
  5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultModeUITrackingRunLoopMode用,并不是一种真正的Mode

注意: 在使用NSTimer的时候,需要将Timer放在Runloop中,并且需要指定Mode,由于是Fundation框架的API,在FundationMode只有两种NSRunLoopCommonModes、NSDefaultRunLoopMode。如果使用NSDefaultRunLoopMode,在滑动ScrollView的时候,Runloop会切换到UITrackingRunLoopMode模式,对应的Timer会短暂性失效,等到Runloop再次切换到NSDefaultRunLoopMode的时候才会起作用。所以我们在添加Timer的时候最好使用NSRunLoopCommonModes

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,那么就形成了如下如所示的结构:
在这里插入图片描述
上面这个结构体的成员变量中我们可以聚焦于下面几个:

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

简言之就是:RunloopMode里面有sources0sources1observertimer组成

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是输入源的抽象,分为source0source1两个版本

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

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

CFRunLoopTimer是基于时间的触发器,它和NSTimer可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调,同时苹果官方文档也有提到CFRunLoopTimerNSTimertoll-free bridged的,这就一位着两者之间可以相互转换。

对于NSTimer
scheduledTimerWithTimeIntervalRunLoop的关系是会自动加入NSDefaultRunLoopMode

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

下方写法的效果和上方一样:

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

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

RunLoopsource事件源来监测是否有需要执行的任务,而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
};

可以看到除了进入退出外还有所有状态外有4种运行中的状态。

小结

RunLoop的相关类

RunLoop相关的类有5个:

  1. CFRunLoopRef 代表了RunLoop的对象
  2. CFRunLoopModeRefRunLoop的运行模式
  3. CFRunLoopSourceRef 就是RunLoop模型图中提到的输入源(事件源)
  4. CFRunLoopTimerRef 定时源
  5. CFRunLoopObserverRef观察者,监听RunLoop状态的改变

RunLoop构成小结

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

下面这张图很好地展现了RunLoop的结构:
在这里插入图片描述

RunLoop底层实现原理

最上面我们提到了无论是获取当前线程的RunLoop还是获取主线程的RunLoop,它们的接口函数里都是调用了_CFRunLoopGet0,而且该函数的参数分别是:pthread_self()(获取当前线程的RunLoop时)和pthread_main_thread_np()(获取主线程的RunLoop时),由此可见,CFRunLoopGetMain函数不管是在主线程还是子线程中都可以获取到主线程RunLoop

接下来我们就来看一下_CFRunLoopGet0函数的源码。

_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和线程是一一对应的,是以线程为keyRunLoop对象为value存放在一个全局字典中的。
  2. 主线程的RunLoop会在初始化全局化字典时创建。
  3. 子线程的RunLoop会在第一次获取时创建。
  4. 当线程销毁时,对应的RunLoop也会随之销毁。

接着我们再梳理一遍RunLoop的内部运行逻辑,为后面主要源码的讲解做一个铺垫。

The Run Loop Sequence of Events(RunLoop的内部运行逻辑)

该部分内容摘选自:苹果官方文档-RunLoop
每一次运行RunLoop,线程对应的RunLoop就会处理挂起的事件,并通知观察者。它执行的顺序如下:

  1. 通知Observer即将进入RunLoop
  2. 通知Observer即将处理Timer
  3. 通知Observer即将处理Source0(非端口的输入源)
  4. 处理Source0(非端口的输入源)
  5. 如果有Source1(基于端口的输入源)准备就绪并等待被触发,立即处理该事件,并跳到步骤9
  6. 通知Observer即将休眠
  7. 线程休眠(实际上是进入了一个do-while循环等待接收端口的消息,接收到时就跳出循环),直到发生以下事件之一:
    • 一个事件到达Source1(基于端口的输入源)
    • 一个定时器(Timer)触发
    • RunLoop超时唤醒
    • RunLoop被手动唤醒(例如添加一个Source0非端口的输入源)
  8. 通知Observer线程刚刚唤醒
  9. 处理待处理的事件
  • 如果用户定义的Timer触发了,则处理这个定时器事件并重新启动RunLoop循环,跳到步骤2
  • 如果输入源触发了,则传递事件
  • 如果RunLoop被手动唤醒,但尚未超时,重新启动RunLoop循环,跳到步骤2
  1. 通知Observer RunLoop已经退出

下面这张经典的图描述的很好,但介于原图上有几处错误,下图展示的是本人修改之后的:
请添加图片描述

接着我们来看一下CFRunLoopRun函数,也就是RunLoop的启动函数:

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

//用指定的Mode启动,允许设置RunLoop超时时间
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

接着来看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(上面总结的RunLoop运行逻辑的第一步)
    if (currentMode->_observerMask & kCFRunLoopEntry)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //下方调用了__CFRunLoopRun函数处理任务
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知observer已经退出RunLoop(上面总结的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的名称
secondsRunLoop的超时时间
returnAfterSourceHandled:是否在处理完事件之后返回

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

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

上面的处理任务的关键就是调用 __CFRunLoopRun方法,所以我们接着看 __CFRunLoopRun方法的实现

__CFRunLoopRun

__CFRunLoopRun方法的源码如下(这个方法非常繁重),我们可以配合着上方讲解的RunLoop十条内部运行逻辑来看,其中十条步骤的第一步第十步都在CFRunLoopRunSpecific方法中,__CFRunLoopRun方法中只有第二步第九步

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.
                //清空内部队列。如果其中一个callout块设置了timerFired标志,则中断并为计时器提供服务。
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    //将livePort作为队列端口,并将服务计时器放在下面
                    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休眠时进入的小do-while循环),当调用该方法时,线程就会一直留在这个循环里面,直到超时或者手动被停止,该方法才会返回。在这里循环里面,线程在空闲的时候处于休眠状态,在有事件需要处理的时候,处理事件。该方法是整个RunLoop运行的核心方法。苹果官方文档-RunLoop对于RunLoop处理各类事件的流程有着详细的描述。

具体的RunLoop内部运行逻辑的十个步骤的总结见上方总结。

__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的消息,内核会将线程置于等待状态。

RunLoop事件处理

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

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

这些方法的实现我们不必关系,但是这些方法在处理事件后如何回调给上层,才是我们需要关心的。比如说__CFRunLoopDoSources0处理的是系统的事件,那么触发一个UIButton的点击事件后,查看函数调用栈应该可以知道回到给上层是如何进行的,例子如下图所示:
请添加图片描述
如上图所示UIButton的点击事件的函数调用栈,我们可以清楚的看到__CFRunLoopDoSources0方法的调用,然后调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 回到UIKit

关于上述方法回调上层的方法对应如下图所示:
在这里插入图片描述

下面我们总结一下RunLoop的回调流程:

RunLoop回调流程

  • App启动时,系统会默认注册五个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);
}

小结

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

RunLoop应用

RunLoop测试样例(使用NSTimer)

我们使用对RunLoop添加监听者的方法,同时使用定时器来看一下RunLoop运行流程中的详细过程:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self runloopObserver];
    [self runloopFunc];
}

//创建一个定时器并用NSRunLoopCommonModes模式添加到RunLoop中,如果使用NSDefaultRunLoopMode模式添加的话,当我们操作其他视图例如滑动scrollView时,就会因为当前线程的RunLoop切换到UITrackingRunLoopMode模式下,从而导致我们的NSTimer暂时性停止工作,当操作完scrollView时RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,从而NSTimer又开始正常工作
- (void)runloopFunc {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

}

//定时器执行的事件函数
- (void)timerAction {
    NSThread *thread = [NSThread currentThread];
//    [thread sleep];
    NSLog(@"当前线程为%@",thread);
}

//给Runloop添加观察者来观察其运行的状态
- (void)runloopObserver {
  //创建监听者
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            CFRunLoopMode str = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"RunLoopMode_%@",str);
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"RunLoop进入");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"RunLoop即将处理Timers了");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"RunLoop即将处理Sources了");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"RunLoop即将休眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"RunLoop被唤醒了");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"RunLoop退出了");
                    break;
                default:
                    NSLog(@"RunLoop其他");
                    break;
            }
        });
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
        CFRelease(observer);
}

@end

运行结果如下:
请添加图片描述
然后我们想视图上添加了一个scrollerView,在滑动scrollerView时我们可以观察到RunLoop“交接班”的过程,代码例子如下:

//我们在上方的例子中多添加了一个滑动视图
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self runloopObserver];
    [self runloopFunc];
    
    //创建一个滑动视图
    UIScrollView *scrollViewTest = [[UIScrollView alloc] init];
    scrollViewTest.delegate = self;
    scrollViewTest.frame = CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height * 0.7);
    scrollViewTest.backgroundColor = [UIColor yellowColor];
    scrollViewTest.contentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height * 2);
    [self.view addSubview:scrollViewTest];
}

运行结果如下:
请添加图片描述

ImageView延迟显示

当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能出现卡顿的情况

解决这个问题的话我们就可以采用延迟图片的显示的方法,当我们滑动时不要加载图片, 拖动结束在显示,代码如下:

//用NSDefaultRunLoopMode模式添加到RunLoop,当我们不滑动tableView的时候才生效加载图片,当我们滑动tableView的时候RunLoop就会切换到UITrackingRunLoopMode模式下,从而导致显示图片的操作暂时不生效,须等到滑动结束后才可以生效
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName.png"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode]];

上面的代码的效果是:用户点击屏幕,在主线程中,两秒之后显示图片,但是当用户点击屏幕之后如果两秒内开始了tableView的滑动,就会导致RunLoop切换到UITrackingRunLoopMode模式而造成两秒到时也不能显示出来图片,除非等tableView停止滑动恢复原来的RunLoop模式,方可显示图片。

常驻线程

开发应用程序的过程中,如果后台操作十分频繁,比如后台播放音乐、下载文件等等,我们希望这条线程在其进程存在时一直常驻内存

我们可以添加一条用于常驻内存的强引用子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop

@interface ViewController ()
//创建一个用strong修饰的强引用子线程
@property (nonatomic, strong) NSThread *thread;

@end


- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    [self.thread start];
}

- (void)run1 {
    NSLog(@"----run1-----");

    /*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
          下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/

        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    // 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
    // 方法2
    //[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    // 方法3
    //[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    
    [[NSRunLoop currentRunLoop] run];
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环,在终止RunLoop之前不会执行到这个NSLog。
    NSLog(@"未开启RunLoop");
}

//我们同时在我们自己新建立的这个线程中写一下touchesBegan这个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) run2 {
    NSLog(@"----run2------");
}

上述的三种开始运行RunLoop的方法也就是三种:

方法名介绍中文翻译
runUnconditionally无条件
runUntilDateWith a set time limit设定时间限制
runMode:beforeDate:In a particular mode在特定模式下
  • 无条件进入是最简单的做法,但也最不推荐。这会使线程进入死循环,从而不利于控制RunLoop,结束RunLoop的唯一方式是kill它
  • 如果我们设置了超时事件,那么RunLoop会在处理完事件或超时后结束,此时我们可以重新选择开启RunLoop。这种方式要优于前一种
  • 这是相对来说比较优秀的方式,相比于第二种启动方式,我们可以指定RunLoop以哪种模式运行
  • 通过查看Run方法的文档我们可以指定,其本质就是无限调用runMode:beforeDate方法,同样的,runUntilDate:也会重复调用runMode:beforeDate,区别在于它超时后就不会再调用。也就意味着,只有runMode:beforeDate方法是单次调用,其他两种都是循环调用

我们必须保证线程不消亡,才可以在后台接受时间处理,所以如果没有实现添加NSPort或者NSTimer,会发现执行完run方法,线程就会消亡,后续再执行touchbegan方法无效

实现了上面三个方法之一,就可以发现执行完了RunLooprun方法,这个时候再点击屏幕,可以不断执行我们为这个强引用子线程添加的run2方法,因为线程self.thread一直常驻后台,等待事件加入其中,然后执行

线程保活

我们上面介绍了线程常驻,这样写相当于虽然该任务执行完了,但是RunLoop一直卡在这里,也不能去执行别的任务

我们这时如果想要使运行循环退出:

  • 设置超时事件
  • 手动设置结束

如果使用方法二和方法三来启动RunLoop,那么在启动时候就可以设置超时时间。然而我们期望“使用RunLoop进行线程保活",希望对线程和它的RunLoop有最精准的控制,比如在完成任务后立刻结束,而不是依赖于超时机制

据文档描述,我们有一个CFRunLoopStop()方法来手动结束一个RunLoop
CFRunLoopStop()方法只会结束当前的runMode:beforeDate:调用,而不会结束后续的调用

  • 我们想要控制RunLoop就需要使用runMode:beforeDate方法,因为其他两种方法一个无法停止,一个只能依赖超时机制
    CFRunLoopStop()方法只会结束当前的一次runMode:beforeDate的调用

对于这两点,我们有下面的解答:

  1. 因为runMode:beforeDate方法是单次调用,我们需要给他加上一个循环,否则调用一次就结束了,和不使用RunLoop的效果大同小异
  2. 循环的条件可以默认设置为YES,当调用stop方法时,执行CFRunLoopStop方法并且将循环条件改为NO,就可以是循环停止,RunLoop退出
@interface ViewController ()
@property (strong, nonatomic) NSThread *aThread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 添加一个停止RunLoop的按钮
    UIButton *stopButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [self.view addSubview:stopButton];
    stopButton.frame = CGRectMake(180, 180, 100, 50);
    stopButton.titleLabel.font = [UIFont systemFontOfSize:20];
    [stopButton setTitle:@"stop" forState:UIControlStateNormal];
    stopButton.tintColor = [UIColor blackColor];
    [stopButton addTarget:self action:@selector(stop) forControlEvents:UIControlEventTouchUpInside];
    
    self.stopped = NO;
    __weak typeof(self) weakSelf = self;
    self.aThread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"go");
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"ok");
    }];
    [self.aThread start];
}

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

// 子线程需要执行的任务
- (void)doSomething {
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void)stop {
    // 在子线程调用stop
    [self performSelector:@selector(stopThread) onThread:self.aThread withObject:nil waitUntilDone:YES];
}

// 用于停止子线程的RunLoop
- (void)stopThread {
    // 设置标记为NO
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
}

运行结果如下:
请添加图片描述
点击stop按钮之后,线程保活就结束了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值