CFRunloop 源码阅读笔记

本文是关于iOS Runloop的学习笔记,介绍了runloop的基本概念、CFRunLoop的运行模式以及核心函数__CFRunLoopRun()的工作流程,阐述了如何在不同场景下处理事件,包括Timer、Input Source和Blocks。文章探讨了runloop如何被唤醒并执行任务,以及其与系统框架如CoreAnimation的交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第一次接触 runloop 时,是在用 Timer 写重复动画的时候(用 Timer 写动画?很傻逼是吧,我也这么觉得)。大概是这么一行代码启动一个定时器,然后每隔0.1秒去翻转一张 Loading 图的角度。

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:.1f target:self selector:@selector(rotationLoading:) userInfo:nil repeats:YES]

那时候经常遇到问题。当首页的 TableView 在滚动的时候,Loading 图却不转了。经过一翻查找,大概知道了 Runloop 这个概念。当时刚入门,找到解决方法后也没继续深入。最近又经常接触到这家伙,便去找了 Core Foundation 的源码来看了个大概。

runloop 是什么

runloop 大致跟 Windows 下的消息循环机制一样。一个runloop是一种消息机制,用于线程间、异步通信。runloop的主要功能是等待事件的发生,并将该事件分发到相应的处理方法。比如,用户点击按钮,定时器,线程间通信,异步代码,网络请求回调,等等。runloop接收到消息后,将该消息投递到相关的处理方法中去。简单来说,runloop在线程中可以简单等价于以下伪代码:

run (runloop) {
    do {
        runloop.waitForSomeEvent();
        var message = runloop.messageQueue.dequeue();
        dispatchMessage(message);
    } while (YES);
}

post (runloop, message) {
     runloop.messageQueue.queue(message);
     runloop.wakeup();
}
在iOS中,替线程干这种脏活的叫,NSRunloop。NSRunloop是对CFRunloop的封装。并远强大于上述的伪代码。除了初始化、手动启动的线程及GCD中分发到后台线程的block,你写的大部分代码都是在合适的时间被CFRunloop调用。

 CFRunloop

CFRunLoop中有一项重要的特性——运行模式(CFRunLoopModes)。同一时间内CFRunLoop运行在特定的RunLoopModes下。CFRunLoop中运行着不同的Run Loop Sources。 Sources与一个或多个runloopModes绑定。也就是说runloop会自动过滤和其他Mode相关的事件源,而只监视和当前设置Mode相关的源。此外CFRunLoop也可以在应用中手动激活。你仅需调用CFRunLoopRun()方法。

在CFRunLoop里面定义了以下6种函数:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

这些销魂的名字仅仅是用来帮助调试的。CFRunLoop都是通过这些函数来调用对应的事件。

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(
    CFRunLoopObserverCallBack func,
    CFRunLoopObserverRef observer,
    CFRunLoopActivity activity,
    void *info);

这个Observers有些特殊。CFRunLoop 提供CFRunLoopObserver API能让你在应用中观察CFRunLoop的状态。当Run Loop 进入的执行,Run Loop 处理一个Timer,Run Loop 处理一个Input Source,Run Loop 进入睡眠,Run Loop 被唤醒,在唤醒它的事件被处理之前Run Loop 停止等等。这些状态非常有用,系统某些框架工作,比如CoreAnimation就运行在观察者通知回调中。

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(
    void (^block)(void));

正如名字一样,这个函数则是用来调用那些由CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void))添加的 Blocks。

static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(
    void *msg);
这个函数当然是用来调用GCD中Main Dispatch Queue的Blocks。显然,在主线程中,至少同时运行着GCD 和 CFRunLoop。尽管GCD可以创建没有CFRunLoop的线程,在只有一条线程时(也就是只有主线程时),GCD将被阻塞。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(
    CFRunLoopTimerCallBack func,
    CFRunLoopTimerRef timer,
    void *info);
用来调用定时器的方法。在iOS中,上层的类NSTimer或方法performSelector:afterDelay:都是用CFRunLoop timers来实现。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(
    void (*perform)(void *),
    void *info);
这个函数是用来处理 Version0的事件源。CFRunLoopSources有两个版本的源。Version0 和 Version1。虽然他们长得很像,而且有共同的API,但实际上它们是不太相同的东西。Version0的Sources只是在应用内只能由手动处理的消息机制。比如UI事件。当你收到一个Version0的Sources的信号( 可由CFRunLoopSourceSignal()发起),必须唤醒CFRunLoop(调用CFRunLoopWakeUp()函数)来处理Version0的Sources。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
    void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
    mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
    void (*perform)(void *),
    void *info);
有处理Version0的函数当然也就有处理 Version1的函数。Version1的Sources通过mach ports来处理内核事件。由代码看来,这对CFRunLoop非常重要。当没有时间发生时,CFRunLoop将在mach_msg处等待事件发生。

__CFRunLoopRun()

不管你是用CFRunLoopRun() 或 CFRunLoopRunInMode()来启动runloop,CFRunLoop的核心是__CFRunLoopRun()函数。

__CFRunLoopRun()退出的情况有:

kCFRunLoopRunTimedOut:超时
kCFRunLoopRunFinished:所有的Sources都被移除:
kCFRunLoopRunHandledSource:与returnAfterSourceHandled共同决定一旦事件被分发处理
kCFRunLoopRunStopped: 手动调用CFRunLoopStop()
如果没出现这几种情况,runloop将会一直运行。并且内部将依次按以下顺序处理消息

  1. 调用通过 CFRunLoopPerformBlock() API 这个函数添加的 Blocks。
  2. 检查Version0 Sources,如果有的话,则调用 perform 函数。
  3. 轮询内部分发队列。
  4. 如果没事件发生则进入睡眠。当有消息到达时,runloop被唤醒。(因为要兼容Win32,所以源代码加了许多#ifdef,#elif, #endif。但主要是通过mach_msg()来配置等待的ports。这样做可以让Timer,GCD,手动唤醒,或version0 Sources都能在同一时间唤醒runloop。)
  5. runloop 被唤醒,依次检查以下:
    • 手动唤醒。仅是继续执行runloop blocks或者version0 Source。
    • 一个或多个定时事件唤醒。执行事件函数。
    • GCD唤醒,执行Blocks。调用的是 dispatch_queue API 中特殊的_dispatch_main_queue_callback_4CF()函数。
    • 查找并执行由内核发出version0 的Sources。
  6. 再次调用由CFRunLoopPerformBlock() API 这个函数添加的 Blocks。
  7. 检查退出条件(Finished, Stopped, TimedOut, HandledSource)。
  8. 进行下一次循环。

伪代码如下:

setupTimeoutTimer();
do {
    observerCallbacks(kCFRunLoopBeforeTimers);
    observerCallbacks(kCFRunLoopBeforeSources);

    performBlocks();

    var hasHandledSource0 = performVersion0Sources();

    if (hasHandledSources0) performBlocks();

    if (checkIfExistMessageInMainQueue) {

        do {
            performNextBlockInMainQueue();
        }while(hasBlocksInMainQueue);

    } else {

        observerCallbacks(kCFRunLoopBeforeWaiting);
        var event = sleepAndWaitForEventWakeup();//这里runloop 进入睡眠直到有 Timer 的 fire 事件、GCD 中dispatch block 到 main_queue 或 source 信号。
        observerCallbacks(kCFRunLoopAfterWaiting);
        if (event == timer) {
            performTimers();
        } else if (event == mainDispatchQueue) {
            do {
                performNextBlockInMainQueue();
            } while(hasBlocksInMainQueue);
        } else {
            performVersion1Sources();
        }
    }
    performBlocks();
} while (true);


以上便是__CFRunLoopRun()的主要流程。很容易,对不对?但是CoreFoundation用C实现的,并不是很好理解。目前也没有看到苹果在 Swift 中重写这一部分内容。只能自己多研究研究了~

 注:本文是关于iOS Runloop的学习笔记。有不对的地方,请指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值