一文看完 Runloop

3,506 阅读18分钟

Runloop 是和线程紧密相关的一个基础组件,是很多线程有关功能的幕后功臣。 本文将从以下几个方面来总结runloop:

  • 什么是runloop
  • runloop的作用
  • runloop和线程的关系
  • runloop详细介绍及源码分析
  • runloop原理分析
  • runloop应用

什么是runloop

runloop 苹果官方文档地址

  • Runloop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的do..while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待。

runlooprun方法源码如下所示,是一个do..while循环

void CFRunLoopRun(void) {	/* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
  • 当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
  • runloop实际上是一个对象,这个对象提供了一个入口函数。

runloop的作用

  • 保持程序的持续运行,循环避免线程销毁
  • 处理APP的各种事件(触摸、定时器、performSelector)
  • 节省cpu资源、提供程序的性能(该做事就做事,该休息就休息)

runloop在系统里的使用

在iOS系统里,下面的这些都有使用runloop,通过断点查看堆栈可以看到调用的方法名:

  • block应用: CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK

  • 调用timer: CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION

  • 响应source0: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION

  • 响应source1: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION

  • GCD主队列: CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE

  • observer源: CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION

断点查看runloop信息

timerblock里添加断点,然后左边箭头指示的按钮不选中(默认是选中的),可以看到runloop的调用信息__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ 源码如下:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
    if (func) {
        func(timer, info);
    }
    getpid(); // thwart tail-call optimization
}

关于上面总结的其他几种调用的runloop方法名,都可以用上面的这种调试方式查看一下。

runloop和线程的关系

  1. runloop和线程是一一对应的
  2. runloop在首次被线程获取时创建,在线程结束时被销毁
  3. 主线程默认启动runloop,子线程手动启动(程序启动时,启动主线程runloop,[[NSRunLoop currentRunLoop] run])

图中展现了 Runloop 在线程中的作用:从 input sourcetimer source 接受事件,然后在线程中处理事件。

获取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(),这里是传一个主线程pthread_main_thread_np()进去,如下定义了它是主线程

#if DEPLOYMENT_TARGET_WINDOWS || DEPLOYMENT_TARGET_IPHONESIMULATOR
CF_EXPORT pthread_t _CF_pthread_main_thread_np(void);
#define pthread_main_thread_np() _CF_pthread_main_thread_np()

还有一个获取当前线程runloop的方法:同样是调用了_CFRunLoopGet0,只不过传进去的是当前线程pthread_self()

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

接下来看获取线程runloop的函数_CFRunLoopGet0(包括主线程和子线程)的源码

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    //根据线程获取runloop
	t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    //如果存储RunLoop的字典不存在
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
	CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
	 //创建主线程的RunLoop
	CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
	CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	    CFRelease(dict);
	}
	CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //字典里找runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
	CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
	loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	if (!loop) {
	    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
	    loop = newLoop;
	}
        __CFSpinUnlock(&loopsLock);
	CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
  • 如果当前存储的字典容器不存在,首先就创建了一个容器CFMutableDictionaryRef可变字典

  • 第二步使用主线程创建了一个主线程runloopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

  • 第三步CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);把主线程和它的runloop用key-value形式保存在这个CFMutableDictionaryRef字典容器里

  • 以上说明,第一次进来的时候,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建

  • 再看到CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));,可以用线程把保存在字典里的runloop取出来

  • 如果字典里没有找到runloop,就根据当前的子线程创建一个新的runloop对象并保存到字典里

  • 最后一步if (pthread_equal(t, pthread_self())) {...}判断当前的线程是不是传递进来的线程,如果是则创建一个回调,如果线程销毁,就销毁当前的runloop

  • 这里验证了上面的结论1和2: runloop和线程是一一对应的(字典保存)。 runloop在首次被线程获取时创建(并且: 不管获取的是主线程runloop还是子线程runloop,总是会创建主线程的runloop),在线程结束时被销毁(通过回调销毁)

runloop代码验证

AppDelegate打断点,可以看到主线程是有调用__CFRunloopRun方法的,所以证明了上面的结论三: 主线程是默认开启runloop ![]p1-jj.byteimg.com/tos-cn-i-t2…) 测试runloop代码如下

- (vod)viewDidLoad {
    super viewDidLoad];
DLThread *thread = [[DLThread alloc]initWithBlock:^{
       NSLog(@"%@",[NSThread currentThread]);
       [NSTimer scheduledTimerWithTimeInterval:1repeats:YES block:^(NSTimer * _Nonnul) {
            NSLog(@"timer");
        }];
        
    }];
    thread.name = @"Test";
    [thread start];

DLThread.m里只写了如下代码

-(void)dealloc{
    NSLog(@"线程销毁了");
}

运行上面的代码,发现timer并没有打印,说明子线程里开启timer没成功,然后添加了代码运行当前线程的runloop,如下所示:

DLThread *thread = [[DLThread alloc] initWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer");
        }];
        [[NSRunLoop currentRunLoop] run];
    }];
    thread.name = @"Test";
    [thread start];

发现timer一直在打印了,这里证明了两个结论: timer的运行是和runloop有关的,子线程的runloop是需要手动开启的

那么如何停止timer呢?新增了一个标记值isStopping用来退出线程

DLThread *thread = [[DLThread alloc] initWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer");
            if(self.isStopping){
                [NSThread exit];
            }
        }];
        [[NSRunLoop currentRunLoop] run];
    }];
    thread.name = @"Test";
    [thread start];

运行发现,在线程销毁后,timer也停止了,这里侧面证明了上面的结论二: runloop是在线程结束时销毁的

runloop源码分析

点击下载runloop源码:密码 3kww

在runloop源码里需要探索的:

  • CFRunLoop
  • CFRunLoopMode
  • CFRunLoopSource
  • CFRunLoopObserver
  • CFRunLoopTimer

CFRunLoop

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就是一个结构体对象,里面包含了一个线程,一个当前正在运行的mode, N个mode,N个commonMode。

  • runloop和线程一一对应
  • runloop包含多个mode, mode包含多个 mode item(sources,timers,observers)
  • runloop一次只能运行在一个model下:
    • 切换mode:停止loop -> 设置mode -> 重启runloop
    • runloop通过切换mode来筛选要处理的事件,让其互不影响
    • iOS运行流畅的关键

CFRunLoopMode

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

一个CFRunLoopMode对象有一个name,N个source0、N个source1、timer、observer和port,可见事件都是由Mode在管理,而RunLoop管理Mode

它们之间的关系如下图:

mode是允许定制的,不过至少要包含一个mode item(source/timer/observer)。 同一个mode item可以被多个mode持有

苹果公开的三种 RunLoop Mode:

  • NSDefaultRunLoopMode(kCFRunloopDefaultMode):默认状态,app通常在这个mode下运行
  • UITrackingRunLoopMode:界面跟踪mode(例如滑动scrollview时不被其他mode影响)
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):是前两个mode的集合,可以把自定义mode用CFRunLoopAddCommonMode函数加入到集合中

还有两种mode,只需做了解即可:

  • GSEventReceiveRunLoopMode:接收系统内部mode,通常用不到
  • UIInitializationRunLoopMode:私有,只在app启动时使用,使用完就不在集合中了

CFRunLoopSource

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

CFRunloopSourceRef是runloop的数据源抽象类对象(protocol),由源码可以看到共用体(union:在相同的内存位置存储不同的数据类型),可见Source分为两类:

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, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

source0: 处理App内部事件、APP自己负责管理(触发)例如:UIEvent CFSocket。 打断点基本都会看到它。

  • source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。

  • CFRunLoopSourceSignal(source)将这个事件标记为待处理

  • CFRunLoopWakeUp来唤醒runloop,让他处理事件

自定义source实现步骤:

  1. 创建一个底层source0 源 CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
  2. 把我们的创建的source0添加到runloop CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode)
  3. 执行信号,标记待处理CFRunLoopSourceSignal
  4. 唤醒runloop去处理CFRunLoopWakeUp
  5. 取消移除源CFRunLoopRemoveSource
  6. 释放runloopCFRelease(rlp)
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:

  • 由runloop和 Mach port管理,Mach port驱动,包含一个 mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。

  • 它能够主动唤醒RunLoop(由操作系统内核进行管理,例如: CFMachPort,CFMessagePort)

  • 还允许实现自己的Source,但一般不会这么做

CFRunLoopObserver

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;			/* immutable */
    CFRunLoopObserverCallBack _callout;	/* immutable */
    CFRunLoopObserverContext _context;	/* immutable, except invalidation */
};

它是一个观察者,能够监听Runloop的状态改变,可以向外部报告runloop状态的更改,框架中很多机制都由它触发(如CAAnimation)

CFRunloop.h文件里可以看到observer监听的状态如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

正好和下图runloop流程里的observer所对应:

CFRunLoopTimer

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    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是定时器,可以在设定的时间点抛出回调
  • CFRunLoopTimerNSTimer是toll-free bridged的,可以相互转换
  • CFRunLoopTimer的封装有三种: NSTimer,performSelector和CADisplayLink
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti   
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti 
 invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument  
 afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

简单总结了这三种timer,如下图:

runloop原理分析

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

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRunCFRunLoopRunInMode都调用了CFRunLoopRunSpecific函数

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);
    
    
    /// 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    /// 内部函数,进入loop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    /// 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}

上面的源码是简化代码后的源码,实际源码复杂一些,根据源码可得出如下结论:

  • 在进入run loop之前通知observer,状态为kCFRunLoopEntry
  • 在退出run loop之后通知observer,状态为kCFRunLoopExit
  • 进入runloop的时候调用了__CFRunLoopRun函数

__CFRunLoopRun(核心重点)

/// 核心函数
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    int32_t retVal = 0;
    do {
        
        /// 通知 Observers: 即将处理timer事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        /// 通知 Observers: 即将处理Source事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
        
        /// 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        /// 处理sources0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        
        /// 处理sources0返回为YES
        if (sourceHandledThisLoop) {
            /// 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        
        /// 判断有无端口消息(Source1)
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            /// 处理消息
            goto handle_msg;
        }
        
        /// 通知 Observers: 即将进入休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
        
        /// 等待被唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
        
        // user callouts now OK again
        __CFRunLoopUnsetSleeping(rl);
        
        /// 通知 Observers: 被唤醒,结束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg:
        if (被Timer唤醒) {
            /// 处理Timers
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        } else if (被GCD唤醒) {
            /// 处理gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else if (被Source1唤醒) {
            /// 被Source1唤醒,处理Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
        }
        
        /// 处理block
        __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;
        }
        
    } while (0 == retVal);
    
    return retVal;
}

以上是runloop核心函数的简写源码(比较清晰易懂)点击下载runloop源码:密码 3kww 还有一个监听唤醒端口消息的函数__CFRunLoopServiceMachPort比较重要,系统内核将这个线程挂起,停留在mach_msg_trap状态,等待接受 mach_port(用于唤醒的端口) 的消息。线程将进入休眠, 直到被其他线程或另一个进程的某个线程向该端口发送mach_msg消息唤醒

__CFRunLoopServiceMachPort

/**
 *  接收指定内核端口的消息
 *
 *  @param port        接收消息的端口
 *  @param buffer      消息缓冲区
 *  @param buffer_size 消息缓冲区大小
 *  @param livePort    暂且理解为活动的端口,接收消息成功时候值为msg->msgh_local_port,超时时为MACH_PORT_NULL
 *  @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) {
    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;
        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;
}

runloop应用

事件响应

  • 当一个硬件事件(触摸/锁屏/摇晃/加速)发生后,首先有IOKit.framework生成一个IOHIDEvent事件并由SpringBoard接受,之后由mach port转发给需要的App进程。

  • 苹果注册了一个 Source1 来接受系统事件,通过回调函数触发Source0(所以Event实际上是基于Source0)的,调用_UIApplicationHandleEventQueue() 进行应用内部的分发。 _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。

手势识别

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

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

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

界面刷新

  • 当UI发生改变时(Frame变化,UIView/CALayer的结构变化)时,或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标记为待处理。

  • 苹果注册了一个用来监听BeforeWaitingExit的Observer,在他的回调函数里会遍历所有待处理的UIView/CALayer来执行实际的绘制和调整,并更新UI界面。

AutoreleasePool

  • 主线程Runloop注册了两个Observers,其回调都是_wrapRunloopWithAutoreleasePoolHandler

  • Observers1 监听Entry事件: 优先级最高,确保在所有的回调前创建释放池,回调内调用 _objc_autoreleasePoolPush()创建自动释放池

  • Observers2监听BeforeWaitingExit事件: 优先级最低,保证在所有回调后释放释放池。BeforeWaiting事件:调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧池并创建新池,Exit事件: 调用_objc_autoreleasePoolPop(),释放自动释放池

tableView延迟加载图片,保证流畅

ImageView加载图片的方法用PerformSelector设置当前线程的RunLoop的运行模式kCFRunLoopDefaultMode,这样滑动时候就不会执行加载图片的方法 [self.imgView performSelector:@selector(setImage:) withObject:cellImg afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

Timer不被ScrollView的滑动影响

  • +timerWihtTimerInterval... 创建timer
  • [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]把timer加到当前runloop,使用占位模式
  • runloop run/runUntilData手动开启子线程runloop
  • 使用GCD创建定时器,GCD创建的定时器不会受RunLoop的影响
// 获得队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    // 创建一个定时器(dispatch_source_t本质还是个OC对象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
    // GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
    // 比当前时间晚1秒开始执行
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    
    //每隔一秒执行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);
    
    // 设置回调
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);

    });
    
    // 启动定时器
    dispatch_resume(self.timer);

GCD

  • dispatch_async(dispatch_get_main_queue)使用到了RunLoop

  • libDispatch向主线程的Runloop发送消息将其唤醒,并从消息中取得block,并在回调__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()里执行这个block

NSURLConnection

  • 使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate就会不停收到事件回调。

  • start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。 CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

  • 当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoadercom.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

AFNetworking

  • 使用runloop开启常驻线程
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runloop run];
  • 给 runloop 添加[NSMachPort port](source1)使runloop不退出,实际并没有给这个port发消息

AsyncDisplayKit

仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaitingkCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

卡顿检测

  • dispatch_semaphore_t 是一个信号量机制,信号量到达、或者 超时会继续向下进行,否则等待,如果超时则返回的结果必定不为0,信号量到达结果为0。GCD信号量-dispatch_semaphore_t

  • 通过监听mainRunloop的状态和信号量阻塞线程的特点来检测卡顿,通过kCFRunLoopBeforeSourcekCFRunLoopBeforeWaiting的间隔时长超过自定义阀值则记录堆栈信息。

  • 推荐文章: RunLoop实战:实时卡顿监控

FPS检测

  • 创建CADisplayLink对象的时候会指定一个selector,把创建的CADisplayLink对象加入runloop,所以就实现了以屏幕刷新的频率调用某个方法。

  • 在调用的方法中计算执行的次数,用次数除以时间,就算出了FPS。

  • 注:iOS正常刷新率为每秒60次。

@implementation ViewController {
    UILabel *_fpsLbe;
    
    CADisplayLink *_link;
    NSTimeInterval _lastTime;
    float _fps;
}

- (void)startMonitoring {
    if (_link) {
        [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        [_link invalidate];
        _link = nil;
    }
    _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    
    self.count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    _fps = _count / delta;
    NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps);
    self.count = 0;
    _fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}

防崩溃处理

  • NSSetUncaughtExceptionHandler(&HandleException);监听异常信号SIGILL,SIGTRAP,SIGABRT,SIGBUS,SIGSEGVSIGFPE

  • 回调方法内创建一个Runloop,将主线程的所有Runmode都拿过来跑,作为应用程序主Runloop的替代。

CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allModesRef = CFRunLoopCopyAllModes(runloop);

while (captor.needKeepAlive) {
    for (NSString *mode in (__bridge NSArray *)allModesRef) {
        if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
            continue;
        }
        CFStringRef modeRef  = (__bridge CFStringRef)mode;
        CFRunLoopRunInMode(modeRef, keepAliveReloadRenderingInterval, false);
    }
}
  • 可以记录堆栈信息,上传服务器或者弹出友好提示页面等一系列操作。

常驻线程

可以把自己创建的线程添加到Runloop中,做一些频繁处理的任务,例如:检测网络状态,定时上传一些信息等。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
    NSLog(@"----------run----%@", [NSThread currentThread]);
    @autoreleasepool{
    /*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
      下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    // 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
    [[NSRunLoop currentRunLoop] run];
   }

    
    // 方法2
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    
    // 方法3
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    
    NSLog(@"---------");
}

- (void)test
{
    NSLog(@"----------test----%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

以上均为个人对runloop的资料收集及部分理解,如有错误请指正,欢迎讨论。