一文看完-Runloop

5次阅读

共计 22868 个字符,预计需要花费 58 分钟才能阅读完成。

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 [图片上传失败 …(image-ad314f-1571322546197)]) 测试 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 的资料收集及部分理解,如有错误请指正,欢迎讨论。

欢迎加入 iOS 开发交流学习群(密码 123),我们一起共同学习,共同成长

收录:原文地址

正文完
 0