第二十七节—RunLoop(二)

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

上一节已经探索完了RunLoop的结构,那么本节将带着上一节探索得到的内容,找个入口,进入RunLoop逻辑的探索。

首先,根据RunLoop的基本概念,一个RunLoop和一个线程一一对应,而我们的程序进程至少拥有一条主线程,那么就证明 : 一个应用程序,它至少拥有一个RunLoop,就是主线程的RunLoop,那么本节就从这里为入口,找到主线程RunLoop的处理事件的逻辑。

准备 : 随意创建一个iOSProject--->App

准备 : CoreFoudation源码

一、 主程序RunLoop的介入时机

对于项目工程,大家都了解,程序的入口处是main()函数。现在来重新审视一下这个main()函数到底有什么 :

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    //一个AppDelegate类名变量
    NSString * appDelegateClassName;
    //一个自动释放池
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    //返回了一个方法调用
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

程序启动正常逻辑应该是进入了这里。

iOS程序是单进程,进程处理任务需要有线程,线程需要RunLoop来维持生存,这三个点是必不可少的。

那么从main里面,唯一有可能实现这些"必不可少"的条件的函数,只有那个被返回的UIApplicationMain方法。

我们走到ViewController里面的viewDidLoad方法里面,给[super viewDidLoad];来个断点,看看"啥也没有"的App启动起来都调用了什么。

操作 :

lldb输入bt,查看程序的调用堆栈,也可以直接看xcodeDebug Navigator

结果 :

1.0.0.png

结论 :

主程序RunLoop的调用时机 :

  1. dyld链接动态库
  2. main()函数启动
  3. 调用Graphics Services库。这个库大概是处理一个硬件输入的,比如说点击事件之类的。
  4. 开始进入RunLoop

二、主线程RunLoop的入口

根据图1.0.0所示,可以找到主线程的RunLoop入口是CoreFoundation框架下的CFRunLoopRunSpecific函数。打开CoreFoundation源码,查找CFRunLoopRunSpecific

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    //一个do{xxx}while(0),检查是不是多进程的状态,多进程不能用这个CoreFoundation,好吧,这个无所谓,就一个检查
    CHECK_FOR_FORK();
    //判断如果现在是正在dealloc一个RunLoop对象的话,证明RunLoop做完事情了,直接返回RunLoop已经执行完毕了。
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    //给RunLoop对象加锁
    __CFRunLoopLock(rl);
    //根据RunLoop对象和mode的名字去查找当前的mode是哪一个,获取当前RunLoopMode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果RunLoop对象中的currentMode不存在,或者,mode集合中没有当前的mode,直接返回执行完成,RunLoop休眠
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
    Boolean did = false;
    if (currentMode) __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopUnlock(rl);
    return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    //压栈。这里就是为了切换RunLoopMode而做的了,切换mode的时候,RunLoop是休眠状态,不是停止!!!
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    //取到上一次RunLoop运行的模式,先存着
    CFRunLoopModeRef previousMode = rl->_currentMode;
    //设置当前的RunLoop的运行模式
    rl->_currentMode = currentMode;

    //默认定义RunLoop的运行结果是运行完毕的
    int32_t result = kCFRunLoopRunFinished;

    //判断RunLoop在当前mode下是即将进入的状态。通知观察者 : RunLoop即将进入
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //执行__CFRunLoopRun函数,获取结果
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //判断RunLoop在当前mode下是即将退出状态。通知观察者 : RunLoop即将退出
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    //给当前的mode开锁
    __CFRunLoopModeUnlock(currentMode);
    //出栈
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    //把RunLoop的mode还原到本次执行事件之前的样子
    rl->_currentMode = previousMode;
    //解锁RunLoop对象
    __CFRunLoopUnlock(rl);
    return result;
}

每一句都加了注释了,想看的可以参考一下。但是这里面还是要剃出来重点。

到这里了,我探索的就是主线程RunLoop启动了要做什么?所以,找RunLoop实际做的事情。

前面的代码很容易看出来,全都根据CFRunLoopMode的状态做一些处理的。再根据堆栈信息里面再对比一下,不难发现,CFRunLoopRunSpecific堆栈信息之后,就是__CFRunLoopRun

所以,重点就是那个__CFRunLoopRun函数。

也就是 :

    //判断RunLoop在当前mode下是即将进入的状态。通知观察者 : RunLoop即将进入
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //执行__CFRunLoopRun函数,获取结果
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //判断RunLoop在当前mode下是即将退出状态。通知观察者 : RunLoop即将退出
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

结论 :

主程序RunLoopCFRunLoopRunSpecific中先处理mode,当mode符合条件后,通知observer观察者即将进入RunLoop,然后执行__CFRunLoopRun,执行完成后,通知观察者即将退出RunLoop

三、主线程RunLoop主体循环的逻辑

接着上面的思路,现在知道了__CFRunLoopRun是处理RunLoop事务的函数,那就看一下__CFRunLoopRun的源码实现。

但是这个源码真的超级长,几百行,全放文章里大家看起来不方便,我就拿其中一些直接触碰RunLoop逻辑的代码出来。

想看全部的小伙伴可以直接用文章顶部的准备条件跳转苹果官方,下载Core Foundation的源码,找CFRunLoop.c查看。

1. 先看一下总体结构 :

/**
 *  __CFRunLoopRun
 *
 *  @param rl                        CFRunLoopRef : 运行的 RunLoop 对象
 *  @param rlm                       CFRunLoopModeRef : 运行的 mode
 *  @param seconds                   CFTimeInterval : RunLoop 超时时间
 *  @param stopAfterHandle           Boolean类型 : (1)true: RunLoop 处理完事件就退出  
 *                                                (2)false:一直运行直到超时或者被手动终止
 *  @param previousMode              上一次运行的 mode
 *
 *  @return 返回 4 种状态
 */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, 
                              CFTimeInterval seconds, Boolean stopAfterHandle, 
                              CFRunLoopModeRef previousMode) 
{

    /**
     * 判断当前RunLoop或者它的RunLoopMode是不是休眠状态
     * 如果是休眠状态,直接返回RunLoop正在休眠,不再做其他的事情
     **/
    if (__CFRunLoopIsStopped(rl)) 
    {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) 
    { 
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }

    ... ...      //省略中间的步骤,主要看结构

    //定义一个记录RunLoop状态的变量,最后的返回值就是它
    int32_t retVal = 0;
    do {
          /**这里就是RunLoop循环的核心逻辑**/
          ... ...
    }while (0 == retVal);      //这里可以看出来,只要retVal == 0,就会一直循环
    
    /*这里会对定义的定时器和结构体做释放*/
    ... ...
    return retVal;

}

通过整体结构不难发现 : RunLoop真正处理事件的核心逻辑是一个do{...}while(...)循环。

2. RunLoop循环逻辑

那么接着看do{...}while{...}里面都做了什么。内容很多,还有很多宏判断,所以还是挑重要的说。

    //先定义了一个布尔变量
    Boolean didDispatchPortLastTime = true;
    //定义一个记录RunLoop状态的变量,最后的返回值就是它
    int32_t retVal = 0;
    do {

        //1. 设置RunLoop为可唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);

        //2. 通知observer观察者,即将触发timer的回调
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3. 通知observer观察者,即将触发source回调
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        //4. 执行加入当前RunLoop的Block
        __CFRunLoopDoBlocks(rl, rlm);

        /**5. 处理source0的事件
         * true  : source0有事件
         * false : source0没有事件
         **/
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        //判断source0有事件要处理
        if (sourceHandledThisLoop) {
            //执行在前面处理source0过程中,又追加到RunLoop的block事件。
            __CFRunLoopDoBlocks(rl, rlm);
        }
        /**
         * 如果source0有事件要处理,或者超时时间设置的是0,则poll是true
         * 如果source0没有事件要处理,并且设置的超时时间不是0,poll是false
         **/
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        //第一次进入do while是不会走进if的
        //因为didDispatchPortLastTime上面定义了是true,并且到这里还没改变过
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            //接收dispatchPort这个端口的事件(source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                //第一次反正进不来了,后面循环如果进来了,发现有消息事件要处理
                //直接跳到handle_msg去处理消息事件。
                goto handle_msg;
            }
        }

        //6. 没有事件要处理,通知observer观察者,即将进入休眠状态
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //设置RunLoop为休眠状态
        __CFRunLoopSetSleeping(rl);

        //7. 这里放一个内循环,用来接收等待端口的消息,只有接到新的消息,才会break,跳出循环向下执行
        do {
            ... ...
           //上面接收dispatch端口消息的时候见到过吧,说过了是为了接收消息的。
           //它除了接收消息之外,还会睡眠我们的线程。睡眠的时间就取决于参数里面的那个poll是0还是TIMEOUT_INFINITY
           //就是说我在poll的时间限制内,还是没有读取到消息,它就会睡眠当前的线程,如果是0就不会睡眠当前的线程。
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            ... ...
        } while (1);

        //8. 唤醒RunLoop
        __CFRunLoopUnsetSleeping(rl);

        //9. 通知观察者,RunLoop被唤醒了
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        //10. 处理接收到的消息(这就是上面那个if跳到的地方)
        handle_msg:;

        //11. 判断唤醒RunLoop的事件类型,处理唤醒RunLoop的事件
        if (Timer)
        {
            //用户自定义的定时器唤醒的,处理完Timer事件的回调之后,重启RunLoop
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        }
        else if (livePort == dispatchPort)
        {
           //GCD主线程唤醒的
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        }
        else
        {
           //被输入源唤醒的
           //先去从端口找到输入源
           CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
           //判断输入源存在,证明真发消息过来了
           if (rls) {
              //处理source1输入源的事件
              __CFRunLoopDoSource1(rl, rlm, rls)
           }
        }

         //12. 处理RunLoop中的回调
         __CFRunLoopDoBlocks(rl, rlm);

         //13. 设置RunLoop返回值
         if (sourceHandledThisLoop && stopAfterHandle) {
             //进入RunLoop时就带有的事件回调,处理好了就返回
             retVal = kCFRunLoopRunHandledSource;
         } else if (timeout_context->termTSR < mach_absolute_time()) {
             //RunLoop超时 (比传入参数的超时时间还长)
             retVal = kCFRunLoopRunTimedOut;
         } else if (__CFRunLoopIsStopped(rl)) {
             //RunLoop被外界强行手动终止
             __CFRunLoopUnsetStopped(rl);
             retVal = kCFRunLoopRunStopped;
         } else if (rlm->_stopped) {
             //RunLoopMode被终止
             rlm->_stopped = false;
             retVal = kCFRunLoopRunStopped;
         } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
             //RunLoopMode中就没有 source/timer 要处理的事件
             retVal = kCFRunLoopRunFinished;
         }

    }while(0 == retVal)

总结 :

直接上图吧 :

RunLoop循环图.png

来个文字的 :

RunLoop处理事件的逻辑

  1. 发送通知,通知观察者(observer),RunLoop即将启动
  2. 唤醒RunLoop。
  3. 发送通知,通知观察者(observer),即将处理Timer的事件。
  4. 发送通知,通知观察者(observer),即将处理Source的事件。
  5. 执行所有输入源的Block回调
  6. 立即处理source0的事件。如果在主线程的情况下,判断是否存在source1的事件。如果存在,则进入handlemag:函数,放在下面的步骤中处理。
  7. 发送通知,通知观察者(observer),RunLoop即将休眠
  8. RunLoop休眠,等待事件来将其唤醒,可以唤醒RunLoop的事件如下 :
    (1). 某个事件到达了基于端口的源。
    (2). 触发了定时器。
    (3). 到达RunLoop设置的超时时间点。
    (4). RunLoop被手动唤醒。
  9. 接收到了新消息,RunLoop唤醒。
  10. 发送通知,通知观察者(observer),RunLoop刚刚被唤醒。
  11. 处理未被处理的事件 :
    (1). 如果是用户自定义的Timer事件,先处理定时器事件,然后重启RunLoop,重新进入步骤3
    (2). 如果RunLoop被唤醒了,但是还没有超时,重启RunLoop,重新进入步骤3。这里注意,这还有一个判断条件是livePort == dispatchPort的情况,所以只有我们的主线程上的任务才依赖RunLoop的唤醒,而对于子线程的线程调度,还是由GCD来调度。
    (3). 如果是source1唤醒的,直接处理source1
  12. 根据处理事件的类型,以及处理结果,设置RunLoop的返回值,只要返回值还是0,则继续循环,如果返回值不是0,则跳出循环。
  13. 销毁定时器和上下文。
  14. 退出RunLoop。
  15. 发送通知,通知观察者(observer),RunLoop即将退出。

四、RunLoop循环中的重要函数

可以从上面的逻辑中看到,RunLoop本身是一个循环,在处理完第一波的事件之后,RunLoop会进入休眠,在休眠状态下,还会维持一个内部的do {...} while(1)循环,用以循环接收新的可以唤醒它的消息。

那么,这个内部的do {...} while(1)循环就是RunLoop在休眠状态下,接收新消息的入口,所以看一下这里面的重要函数,接收指定内核端口的消息,并且可以睡眠当前线程的函数——__CFRunLoopServiceMachPort

__CFRunLoopServiceMachPort

看下源码 :

/**
 * 接收指定内核端口的消息
 * @param port          : 接收消息的端口的名字
 * @param buffer        : 消息缓冲区
 * @param buffer_size   : 缓冲区大小
 * @param livePort      : 活跃的端口吧。
 * @param timeout       : 超时时间,单位是ms,如果超时,RunLoop会进入休眠状态
 * 
 * @return              : 如果接收到消息,返回true。其他情况,返回false
**/

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
    
    //初始化的缓冲区默认设置成存在
    Boolean originalBuffer = true;

    //一个typedef的int类型,默认是0
    kern_return_t ret = KERN_SUCCESS;

    //循环,看看RunLoop的休眠过程中,会发生什么。
    //官方注释写的很文艺啊。。。
    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;                       //身份标识

        //判断是否超时
        //如果超时了,RunLoop就可以休眠了
        //如果没超时,进入一个do{...}while()循环
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }

        //这句就是内核接收消息的核心
        //发送或者接收的消息都是指针类型
        //如果频繁的直接发送或者接收消息体,会频繁的进行内存复制,消耗性能
        //所以XNU选用了单一内核的方式解决问题
        //所有的内核组件都共享同一块地址空间,因此传递消息的时候,就只需要传递消息的指针
        ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|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);

        // Take care of all voucher-related work right after mach_msg.
        // If we don't release the previous voucher we're going to leak it.

        //mach_msg之后,立即处理所有和凭证关联的工作
        //如果不释放之前的凭证,那么就会造成内存泄漏
        voucher_mach_msg_revert(*voucherState);
        
        // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
        //有人将负责调用voucher_mach_msg_revert。
        //这个调用会使接收到的凭证变成当前的凭证
        *voucherState = voucher_mach_msg_adopt(msg);
        
        //调用者会要求提供一份凭证的复印件
        //在mach_msg之后立即执行这步操作可以确保mach_msg和复印这份凭证中间不产生其他操作
        //就是为了保证这份复制凭证的正确性
        if (voucherCopy) {
            if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) {
                // Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy.
                // CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged.
                *voucherCopy = voucher_copy();
            } else {
                *voucherCopy = NULL;
            }
        }

        //唤醒RunLoop
        CFRUNLOOP_WAKEUP(ret);
        //接收/发送消息成功,设置livePort = msgh_local_port
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }

        //接收/发送消息超时,设置livePort = MACH_PORT_NULL,释放缓冲区
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        
        //如果接收/发送的消息体量大,缓冲区没有这么大,break出去,也就是只返回了消息头
        if (MACH_RCV_TOO_LARGE != ret) break;

        //扩容缓冲区
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);

        //清空缓冲区
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}

都写了注释了,根据官方文档的描述,加上自己的理解写的,不敢保证都对,注释仅供参考。

在这段代码中,可以知道的是 :

mach_msg是用来将线程的控制权在内核态用户态之间转换的。

  1. 没有消息需要处理的情况下,RunLoop会进入休眠,通过mach_msg从用户态切换到内核态,接收消息。
  2. 当接收到消息的时候,则立即唤醒RunLoop,调用mach_msg回到用户态,处理消息。
  3. 也就是这样,RunLoop就不仅仅是一个do{...}while(),因为它在休眠的时候,线程不做任何事情,CPU是不会分配资源的。

五、关于source0和source1

还是要单独给他们开一个标题。

经过上面的RunLoop的核心逻辑的探索,其实我们可以发现一点,source0source1虽然都是事件源,但是处理的内容是不同的,而且具有的功能也是不同的。

经过(四)中的介绍,很明显的知道了,__CFRunLoopServiceMachPort可以休眠RunLoop,它控制着线程的控制权在用户态和内核态之间切换,而可以唤醒RunLoop的则是goto handlemsg:,那么根据处理事物的情况,可以发现source1才是唤醒的关键。

我们来做个实验。做这个验证需要知道下面几个RunLoop处理的事件在Core Foudation框架中的定义变量都是什么 :

  1. Observer :
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
  2. Block :
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__() __attribute__((noinline));
  3. GCD主队列 :
    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() __attribute__((noinline));
  4. Timer :
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
  5. source0 :
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline));
  6. source1 :
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__() __attribute__((noinline));

我们就拿touchBegin来做实验,看看触摸事件是怎么使用到source0source1经过RunLoop处理回调给我们的 :

  1. 下一个__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__的符号断点。
  2. 在ViewController中实现touchBegin的方法。随便写个东西就行。

看结果 :


图5.0.1.png

然后,我们再给source0下断点。

  1. 不要停止项目,直接下一个__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__的符号断点。
  2. 然后把断点跳到这里。

看结果 :


图5.0.2.png

得出结论 :

  • source0 :
    • 不具备唤醒RunLoop的能力。
    • 接收source1传来的事件,分发给App处理。
  • source1 :
    • 具备唤醒RunLoop的能力。
    • 利用捕获线程捕获事件,将事件传递给主线程RunLoop中的source0。

六、RunLoop核心逻辑总结

  1. RunLoop本身维持着一个不普通的do{...}while()循环,线程执行任务就在这个循环里面执行,直到超时或者外界强行手动退出RunLoop。
  2. RunLoop可以接收到消息被唤醒是因为mach_msg的存在,如果mach_msg发现消息发送过来,则会唤醒RunLoop,让RunLoop中的线程处理事件,如果mach_msg没有接到消息,则内核会让RunLoop处于休眠状态。
  3. source1具备唤醒RunLoop的能力,可以捕获消息事件,发送给不具备唤醒RunLoop能力的source0分发事件处理。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容