详解RunLoop之源码分析

首发于 我的博客html

RunLoop是什么

runloop 是什么?Runloop 仍是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。通常的 while 循环会致使 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,这部分能够类比 Linux 下的 epoll。当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可让线程在须要作事的时候忙起来,不须要的话就让线程休眠git

开始以前,先想一想这几道面试题

  • runloop和线程的关系?
  • timer 与 runloop 的关系?
  • 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能没法响应要怎么解决?
  • runloop内部实现逻辑?
  • runloop 是怎么响应用户操做的, 具体流程是什么样的?
  • 说说runLoop的几种状态
  • runloop的mode做用是什么?

源码分析

回答问题以前,咱们先看源码github

RunLoop 源码 opensource.apple.com/tarballs/CF… 里面数字最大的是最 新的,下载最新的 CF-1153.18.tar.gz(写本文时候的最新版本)面试

查看源码 中的bash

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

经过app

_CFRunLoopGet0 获取的
复制代码

进去查看作了什么oop

loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	if (!loop) {
	    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
	    loop = newLoop;
	}
复制代码

发现有这么一个获取线程的方法,也就是传入一个线程做为key,获取一个loop,若是loop为空,就以这个线程为key建立runloop源码分析

小结:ui

  • 每条线程都有惟一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程做为key,RunLoop做为value
  • 线程刚建立时并无RunLoop对象,RunLoop会在第一次获取它时建立
  • RunLoop会在线程结束时销毁(下面分析这一条)

runloop的mode

接下来认识一下runloop的主要类 Core Foundation中关于RunLoop的5个类spa

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef
复制代码

看一下runloop结构体

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};
复制代码

只保留主要的就剩下了

typedef struct __CFRunLoop * CFRunLoopRef;
 struct __CFRunLoop {
	 pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode; //当前模式
    CFMutableSetRef _modes; //全部的模式
  	CFMutableSetRef _modes;
};
复制代码

理解为CFRunLoopRef中包含有_modes,modes是由 CFRunLoopModeRef组成的集合 这些modes中,只有一种是当前模式,称为 _currentMode

接下来咱们看看runloopmode中究竟有什么,一样,只保留主要的,关键就是下面4个

总结起来就是

  • CFRunLoopModeRef表明RunLoop的运行模式
  • 一个RunLoop包含若干个Mode,每一个Mode又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个Mode,做为currentMode
  • 若是须要切换Mode,只能退出当前Loop,再从新选择一个Mode进入
  • 不一样组的Source0/Source1/Timer/Observer能分隔开来,互不影响
  • 若是Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {

    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
};
复制代码

咱们能够理解为,RunLoop中有许多模式,但当前运行的只有一种,一个图来表示,就是

image.png

具体在某一种runloop中的运行逻辑,官方给出下图

image.png

那么,前面说的,_sources0、_sources一、_observers、_timers J究竟包含了什么呢? 先用一张图来总结一下,而后再详细介绍

image.png

如上图所示,_sources0 包含触摸事件,和 performSelector:onThread: 跑一下代码证实一下, 首先建立一个新项目,实现点击事件,NSLog只是为了打断点

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

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

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"这个打印只是为了打断点");
}

@end

断点暂停以后,输入lldb指令 bt 以后如图所示


复制代码

image.png

从打印日志来看 调用了__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 这个SOURCE0方法 那么接下来验证一下performSelector

image.png
由上图可知,performSelector 也是执行了source0

那咱们再看一下Timer

image.png
如上图所示,此次是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

对于其余几种状况,读者课自行验证。

用一幅图来总结RunLoop的运行逻辑

image.png

源码内部细节分析

要想分析源码首先要知道入口在哪里,由前面的断点可知

image.png
入口为 CFRunLoopRunSpecific 去源码中找到以后发现有不少。只保留关键信息

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    // 通知Observers: 进入Loop
  __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // 具体要作的事情
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
     // 通知Observers: 退出Loop
	__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}
复制代码

那咱们继续跟__CFRunLoopRun 看看作了什么,发现里面很长的东西,整理了一下,只保留关键代码,以下

/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        // 通知Observers: 即将处理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知Observers: 即将处理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        // 处理Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        //判断有无source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { // 若是有source1 就跳转到 handle_msg
            goto handle_msg;
        }
        // 通知Observers: 即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
        // 通知Observers: 结束休眠
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
    handle_msg:;
        
        if (被Timer唤醒) {
            // 处理Timers
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        } else if (被gcd唤醒) {
            //处理GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { //能来到这里,就说明被Source1唤醒
            
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            // 设置返回值,决定是否继续循环
            if (sourceHandledThisLoop && stopAfterHandle) {
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(rl)) {
                __CFRunLoopUnsetStopped(rl);
                retVal = kCFRunLoopRunStopped;
            } else if (rlm->_stopped) {
                rlm->_stopped = false;
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
                retVal = kCFRunLoopRunFinished;
            }
            voucher_mach_msg_revert(voucherState);
            os_release(voucherCopy);
        } while (0 == retVal);
        
        return retVal;
}


复制代码

截图下来的话,就是这样的

image.png

###调用细节 前面说了大概的流程,那么,具体怎么调用的呢,lldb调试堆栈的时候,那些方法怎么调用的呢?这里以 __CFRunLoopDoTimers 为例,看下源码怎么调用的

image.png
上图可知,关键代码是 CFRunLoopTimerRef 继续查看 CFRunLoopTimerRef

image.png
关键代码是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ 看到这里是否是对前面截图中的调用堆栈更清晰了呢。 其余几种也都是相似的逻辑,就不赘述了。

目前已知的Mode有五种

目前已知的Mode有5种
kCFRunLoopDefaultMode:App的默认Mode,一般主线程是在这个Mode下运行

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

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

GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,一般用不到

kCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode
复制代码

RunLoop的状态

/* 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
};
复制代码

常见的2种Mode

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,一般主线程是在这个Mode下运行

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

RunLoop 和 NSTimer

咱们知道,默认状况下,NSTimer计时器,会被UIScrollView 打断,会影响计时器的使用。缘由就是滚动时候,RunLoop切换到了UITrackingRunLoopMode模式下,但计时器在NSDefaultRunLoopMode下,因此就中止了。解决办法就是设置NSRunLoopCommonModes。特别注意的是:

NSRunLoopCommonModes并非一个真的模式,它只是一个标记

本文参考资料:

RunLoop官方源码

iOS底层原理

更多资料,欢迎关注我的公众号

相关文章
相关标签/搜索