iOS--runloop的初步认识

runloop的初步认识

简单认识runloop

runloop是什么,Runloop,即运行循环,是苹果操作系统(如iOS、macOS)中一个重要的基础架构组件,它是线程相关的,负责管理和调度线程上的各种事件和任务,确保线程在有工作时忙碌,无工作时休眠,以此达到高效利用系统资源的目的。

这里引用一下苹果文档的解释:

RunLoop是与线程息息相关的基本结构的一部分。RunLoop是一个调度任务和处理任务的事件循环。RunLoop的目的是为了在有工作的时候让线程忙起来,而在没有工作的时候让线程进入休眠状态。

这里举一个最常见的例子,我们打开xcode中的一个项目,找到main文件:

objectivec 复制代码
#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

这段代码的作用是保存下来该应用程序的代理类的类名,并在返回中返回了这个应用程序的主线程上的runloop,其中UIApplicationMain(argc, argv, nil, appDelegateClassName) 是启动一个iOS应用程序的中心函数调用,它在main函数中被调用,负责初始化整个应用程序环境并开始执行应用的事件循环。或者说UIApplicationMain 函数创建应用程序对象和主运行循环,并传递控制权给应用程序的委托类(AppDelegate)来处理应用程序的逻辑。

同时,UIApplicationMain函数内部帮我们开启了主线程的RunLoop。

我通过Xcode内查询声明,无法找到该函数的具体源码,但通过网上的资料可以了解到该函数内部应该是存在一个无限循环 ;

objectivec 复制代码
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

关于这个循环,首先我们要了解一下Event loop

Event loop

Event Loop(事件循环)是许多现代编程环境和框架中的一个核心概念,特别是在需要处理并发和异步操作的场景下,如Web浏览器的JavaScript引擎、Node.js服务端环境、以及某些图形界面和操作系统中。其基本职责是持续监听和处理各种输入事件,协调和调度任务的执行,确保程序的响应性和高效运行。其基本组成包括:

  • 消息队列(Event Queue):所有外部事件(如用户输入、网络响应、计时器到期)和待处理的任务都会被放置到一个或多个消息队列中等待处理。这些队列是先进先出(FIFO)的。
  • 事件循环:这是一个持续运行的循环,其主要任务是从消息队列中取出事件或任务,然后分配给相应的处理函数执行。一旦执行完毕,循环会继续检查队列中是否有更多事件,有的话则继续处理,没有则等待。
  • 回调函数/事件处理器:当事件循环从队列中取出一个事件时,会执行与该事件关联的回调函数或事件处理器,完成具体的业务逻辑处理。

其具体的工作流程和结构在runloop中也基本体现了 ,这里就不讲了 ;、

runloop其实就是个对象

这点是很中要的,首先,在最外层我们接触到的runloop,应该是NSrunloop类对象,但往深处了解的话,其实这个类其实依赖于CFFoundation框架CFRunLoopRef类型 ;这个类我没能直接查看到源码,在网上查询资料了解到了大概的结构 ;

objectivec 复制代码
struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  
    __CFPort _wakeUpPort;   // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
    Boolean _unused;
    volatile _per_run_data *_perRunData; 
    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;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

大概看一下上面有注释说明的几个成员变量,它们在之后会再重点分析 ;

一个RunLoop对象,主要包含了一个线程,若干个Mode,若干个commonMode,还有一个当前运行的Mode。

NSRunloop和CFRunLoopRef的依赖关系

objectivec 复制代码
[NSRunLoop currentRunLoop];//获得当前RunLoop对象
[NSRunLoop mainRunLoop];//获得主线程的RunLoop对象
//NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。
objectivec 复制代码
CFRunLoopGetCurrent();//获得当前线程的RunLoop对象
CFRunLoopGetMain();//获得主线程的RunLoop对象
//CFRunLoopRef是在CoreFoundation框架内的,其提供了纯C语言函数的API,所有这些API都是线程安全的。

具体实现就不解释了,其实上面方法的实现就是对下面方法实现的封装 ;

然后就是分析runloop对象中的成员变量了 ;

runloop与线程

objectivec 复制代码
pthread_t _pthread;             //RunLoop对应的线程

首先要知道几点:

  • Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。
  • 主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。

然后就要看看CFRunLoopGetCurrent();//获得当前线程的RunLoop对象

CFRunLoopGetMain();//获得主线程的RunLoop对象

中都会调用的函数:_CFRunLoopGet0(不过我这些方法函数实现我在xcode中无法查看具体实现,所以只能看别人公开的部分源码)

objectivec 复制代码
//全局的Dictionary,key是pthread_t,value是CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问__CFRunLoops的锁
static CFSpinLock_t loopsLock = CFSpinLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//t==0是始终有效的"主线程"的同义词

//获取pthread对应的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    //pthread为空时,获取主线程
    t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
        //第一次进入时,创建一个临时字典dict
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    //根据传入的主线程获取主线程对应的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    //保存主线程,将主线程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    //此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
    //释放dict
        CFRelease(dict);
    }
    //释放mainRunLoop
    CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建

	//从全局字典里获取对应的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
    //如果取不到,就创建一个新的RunLoop
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    //创建好之后,以线程为key,runLoop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runLoop
    if (!loop) {
    //把newLoop存入字典__CFRunLoops,key是线程t
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&loopsLock);
    CFRelease(newLoop);
    }

	//如果传入线程就是当前线程
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
        //注册一个回调,当线程销毁时,销毁对应的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

这里还要注意几点,除了我上面说到的Runloop 和线程是绑定在一起的。

  • RunLoop保存在一个全局的Dictionary里面,线程作为key,RunLoop作为Value
  • 线程刚创建的并没有RunLoop对象。RunLoop会在第一次获取线程的RunLoop创建,在线程结束的时候销毁。
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop.

runloop mode

runloop mode是runloop中非常重要的组成部分,我的理解是它是runloop的运行模式 ;

它的结构如下:

objectivec 复制代码
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,若干source0、source1、timer、observer和若干port,可见事件都是由Mode在管理,而RunLoop管理Mode。

结构如图:

苹果文档中提到的 Mode 有五个,分别是:

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

这五个mode的使用场景和运行职责也各自不同,这里用到网上的一张图来解释:

我看还有一种说法是系统默认注册五个Mode:

kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行

UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响(与用户交互事件的Mode)

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

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

kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode (伪模式,并不是一种真正的模式)

其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes是我们开发中需要用到的模式。

个人觉得这两个其实差不多,第一个更详细具体,但第二种明显更好理解 ;

到这里也大概能理解我上面说mode是runloop的运行模式了 ;

这里要就看一下kCFRunLoopCommonModes;这个上面的解释其实也不是很清楚,其功能实现包括以下两点:

  • 一个Mode可以将自己标记为Common属性,通过将其ModeName添加到RunLoop的commonModes中。
  • 每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems里的Source/Observer/Timer同步到具有Common标记的所有Mode里。

然后也可以看看网上的源码版本,也更能理解其作用;

objectivec 复制代码
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
    //获取所有的_commonModeItems
    CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
    //获取所有的_commonModes
    CFSetAddValue(rl->_commonModes, modeName);
    if (NULL != set) {
        CFTypeRef context[2] = {rl, modeName};
        //将所有的_commonModeItems逐一添加到_commonModes里的每一个Mode
        CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
        CFRelease(set);
    }
    } else {
    }
    __CFRunLoopUnlock(rl);
}

这里可以举一个简单的例子,如果是在默认的设置下,当我们滑动某个控件时,我们在同一个线程上的定时器在滑动时停止,在定时轮播图的实现中应该都遇到过 ;这里的原因就是也就是上面说的,runloop管理mode,mode管理Source/Observer/Timer,当我们直接添加定时器的时候:

objectivec 复制代码
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//系统会自动加入NSDefaltRunLoopMode.
//[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

在主线程中,不执行滑动操作,runloop中的mode为kCFRunLoopDefaultMode;我们此时将timer加入该mode进行管理,但当我们执行滑动时,runloop会切换mode为UITrackingRunLoopMode,而在这个mode中没有添加的timer,因此timer会停止 ;

最简单的解决方法就是,将定时器加入commonmode中管理,这样在每次切换mode时,都会将commonmode中的Source/Observer/Timer添加到新的mode中 ;

objectivec 复制代码
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

如果对这个切换mode的过程不清楚,可以看看之前展示的思维导图,也就是runloop的工作流程 ;

runloop source

Run Loop Source分为Source、Observer、Timer三种,他们统称为ModeItem。

CFRunLoopSource

CFRunLoopSource分source0和source1;

结构如下:

objectivec 复制代码
```c
/* 
定义了一个CFRunLoopSource结构体,这是Core Foundation框架中用于处理事件源的基础结构,是Runloop能够识别和管理的事件的抽象。
*/
struct __CFRunLoopSource {
    CFRuntimeBase _base; // CFRuntimeBase是Core Foundation框架中所有对象共有的基类结构,包含引用计数等基本信息。

    uint32_t _bits; // 该字段用于标记Source的状态,特别是Signaled状态。对于source0(一次性事件源),只有当它被标记为Signaled时,Runloop才会去处理它。

    pthread_mutex_t _lock; // 互斥锁,保护CFRunLoopSource内部状态的线程安全,确保在多线程环境下对_source的修改是原子的。

    CFIndex _order;         /* immutable */ // 源的执行优先级顺序,不可变。数值越小,优先级越高,影响源在相同模式下的执行顺序。

    CFMutableBagRef _runLoops; // 这个字段存储了所有引用了当前source的RunLoop对象。一个source可以被多个RunLoop共享,这个bag记录了这些关联。

    union { // 联合体,用于存储不同版本的上下文结构,以支持向后兼容及功能扩展。
        CFRunLoopSourceContext version0;     /* immutable, except invalidation */ // CFRunLoopSource的原始版本的上下文结构,包含source操作的各种回调函数指针等。
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */ // 更新版本的上下文结构,可能包含更多的回调或信息,也是不可变的,除了失效操作。
    } _context; // 上下文信息,是source行为的核心,包含了当source被触发时需要执行的操作细节,如处理函数、取消函数等。
};

source0:

objectivec 复制代码
```c
/*
定义了一个CFRunLoopSourceContext结构体,这是CFRunLoopSource的核心部分,包含了控制和管理事件源所需的一系列函数指针。
这个结构体允许开发者自定义事件源的行为,当事件源被触发时,RunLoop会调用这些函数执行相应操作。
*/

typedef struct {
    CFIndex version; // 结构体的版本号,用于兼容性检查,通常初始化为0。

    void *info; // 一个通用指针,开发者可以用来存储任何与事件源相关的自定义数据。

    const void *(*retain)(const void *info); // 类似于Objective-C的retain方法,用于增加引用计数,保持info对象的生命期。

    void (*release)(const void *info); // 类似于release方法,用于减少引用计数,当不再需要时帮助释放资源。

    CFStringRef (*copyDescription)(const void *info); // 返回一个描述info的CFStringRef,通常用于日志和调试。

    Boolean (*equal)(const void *info1, const void *info2); // 比较两个info指针指向的内容是否相等,用于确定源的唯一性等。

    CFHashCode (*hash)(const void *info); // 计算info的哈希值,用于快速比较或作为字典键等。

    void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode); // 将事件源添加到指定的RunLoop和运行模式中,使之能够被监控和触发。

    void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode); // 从RunLoop中移除事件源,停止监控和处理。

    void (*perform)(void *info); // 当事件源被触发时,RunLoop调用此函数执行实际的任务逻辑。
} CFRunLoopSourceContext;

source1:

objectivec 复制代码
```c
/*
定义了CFRunLoopSourceContext1结构体,它是CFRunLoopSourceContext的一个扩展版本,专为特定平台提供了额外的功能。
此结构体在基本的CFRunLoopSourceContext基础上,根据目标操作系统条件增加了与mach_port相关的功能,
旨在支持更广泛的系统间通信和同步需求,尤其是在macOS和iOS平台上。
*/

typedef struct {
    CFIndex version; // 结构体版本号,用于版本控制和向前/向后兼容。

    void *info; // 保留给开发者使用的通用指针,存储与事件源相关的自定义数据。

    const void *(*retain)(const void *info); // 引用计数管理,类似于retain操作,增加info对象的引用计数。

    void (*release)(const void *info); // 引用计数管理,减少info对象的引用计数,必要时释放资源。

    CFStringRef (*copyDescription)(const void *info); // 返回描述info的CFStringRef,常用于日志输出和调试。

    Boolean (*equal)(const void *info1, const void *info2); // 判断两个info指针所指内容是否相等,用于比较。

    CFHashCode (*hash)(const void *info); // 计算info的哈希值,用于快速比较或作为字典键。

    // 下面的函数根据目标操作系统有所不同,提供了mach_port相关的接口或标准的perform调用。

#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
    mach_port_t (*getPort)(void *info); // 获取与事件源关联的mach_port,用于更底层的系统通信和同步。
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info); // 扩展的perform函数,接受消息、大小、分配器和info,提供更灵活的执行机制。
#else
    void *  (*getPort)(void *info); // 获取与事件源关联的标准port,虽然命名相同,但此处不特别指mach_port。
    void    (*perform)(void *info); // 标准的perform函数,直接执行与事件源关联的任务。
#endif
} CFRunLoopSourceContext1;

source0和source1的源码中大概了解引用技术方法和perform方法就行了;

  • source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用
    CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用
    CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop(由操作系统内核进行管理

通过了解工作流程,给我的感觉像是source1分析任务并唤醒并分发解析后的任务给source0去处理 ;

CFRunLoopObserver

CFRunLoopObserver是观察者,可以观察RunLoop的各种状态,并抛出回调。结构如下:

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

观察状态:

objectivec 复制代码
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即将进入run loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理source
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒但是还没开始处理事件
    kCFRunLoopExit = (1UL << 7),//run loop已经退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Runloop 通过监控 Source 来决定有没有任务要做,除此之外,我们还可以用 Runloop Observer 来监控 Runloop 本身的状态。 Runloop Observer 可以监控上面的 Runloop 事件

CFRunLoopTimer

CFRunLoopTimer是定时器,可以在设定的时间点抛出回调

结构如下:

objectivec 复制代码
struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;  //标记fire状态
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;        //添加该timer的runloop
    CFMutableSetRef _rlModes;     //存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;     //理想时间间隔  /* immutable */
    CFTimeInterval _tolerance;    //时间偏差      /* mutable */
    uint64_t _fireTSR;          /* TSR units */
    CFIndex _order;         /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
  • CFRunLoopTimerRef是基于时间的触发器,它和NSTimer可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

runloop的实现

实现包括三个点,runloop的获取,添加mode,添加runloopSource ;

runloop的获取

在开放方法或函数上,苹果不允许我们直接创建runloop对象,具体可以使用的创建方法前面也讲到过:

  • CFRunLoopRef CFRunLoopGetCurrent(void)
  • CFRunLoopRef CFRunLoopGetMain(void)
  • +(NSRunLoop *)currentRunLoop
  • +(NSRunLoop *)mainRunLoop

后面两个方法是前面两个方法的封装,现在给出后两个方法的源码:

objectivec 复制代码
//取当前所在线程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    //传入当前线程
    return _CFRunLoopGet0(pthread_self());
}
objectivec 复制代码
//取主线程的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这里就不再给出了 ;

添加Mode

在Core Foundation中,针对Mode的操作,苹果只开放了以下3个API:

  • CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
  • CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
  • CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl)

我们没有办法直接创建一个CFRunLoopMode对象,但是我们可以调用CFRunLoopAddCommonMode传入一个字符串向RunLoop中添加Mode,传入的字符串即为Mode的名字,Mode对象应该是此时在RunLoop内部创建的。

CFRunLoopAddCommonMode
objectivec 复制代码
`CFRunLoopAddCommonMode`函数是Apple的macOS和iOS操作系统中Core Foundation框架的一部分。它用于管理运行循环(run loops),这是一种基本的事件处理循环,负责协调接收和处理系统事件,如用户输入、网络连接、定时器等。

下面是该函数的中文注释版本:

```c
// 向给定的运行循环中添加一个公共模式。
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    // 检查是否有进程fork发生,这是一个内部检查,确保运行循环在fork后不会在错误的进程中运行。
    CHECK_FOR_FORK();
    
    // 如果运行循环正在被释放,则直接返回不做任何操作。
    if (__CFRunLoopIsDeallocating(rl)) return;
    
    // 加锁,以确保线程安全。
    __CFRunLoopLock(rl);
    
    // 检查此运行循环是否已包含指定的模式,如果已包含则不执行任何操作。
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
        // 如果已有公共模式项存在,则先复制这些项到一个新的集合中。
        CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
        
        // 将新模式名添加到运行循环的公共模式集合中。
        CFSetAddValue(rl->_commonModes, modeName);
        
        // 如果之前有公共模式项被复制出来,则遍历这些项,并将它们添加到新添加的公共模式下。
        if (NULL != set) {
            // 准备一个上下文数组,用于传递给回调函数,包含运行循环引用和模式名称。
            CFTypeRef context[2] = {rl, modeName};
            
            // 遍历并添加项目到新模式下。这会间接调用CFRunLoopAddSource等函数,
            // 并在需要时创建CFRunLoopMode对象。
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
            
            // 释放之前复制的集合。
            CFRelease(set);
        }
    } else {
        // 如果模式已经存在,则什么也不做。
    }
    
    // 解锁,完成操作。
    __CFRunLoopUnlock(rl);
}

注意的点:1.modeName不能重复,modeName是mode的唯一标识符 2.CFRunLoopMode对象在CFRunLoopAddItemsToCommonMode函数中调用CFRunLoopFindMode时被创建 3.添加commonMode会把commonModeItems数组中的所有source同步到新添加的mode中 4.RunLoop的_commonModes数组存放所有被标记为common的mode的名称

CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes这两个函数逻辑比较简单,直接取RunLoop的_currentMode和_modes返回 ;

添加Run Loop Source(ModeItem)

  • void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source,
    CFStringRef mode)
  • void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef
    source, CFStringRef mode)
  • void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef
    observer, CFStringRef mode)
  • void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef
    observer, CFStringRef * mode)
  • void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer,
    CFStringRef mode)
  • void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer,
    CFStringRef mode)

runloop运行

这里看别人博客有两种说法,这里展示比较底层的那种

在Core Foundation中我们可以通过以下2个API来让RunLoop运行:

void CFRunLoopRun(void)

在默认的mode下运行当前线程的RunLoop。

CFRunLoopRunResult CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled)

在指定mode下运行当前线程的RunLoop。

objectivec 复制代码
//默认运行runloop的kCFRunLoopDefaultMode
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        //默认在kCFRunLoopDefaultMode下运行runloop
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
objectivec 复制代码
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

这里还可以看出,虽然RunLoop有很多个mode,但是RunLoop在run的时候必须只能指定其中一个mode,运行起来之后,被指定的mode即为currentMode。

这两个函数都调用了一个更加底层的函数:

CFRunLoopRunSpecific

objectivec 复制代码
以下是该段代码的中文注释版,描述了如何在指定模式下运行运行循环及其相关流程:

```c
/*
 * 在指定模式下运行运行循环
 * @param rl 当前运行的运行循环引用
 * @param modeName 需要运行的模式名称
 * @param seconds 运行循环的超时时间,单位秒
 * @param returnAfterSourceHandled 处理完事件后是否立即返回
 * @return 运行结果
 */
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
    // 检查是否有进程fork发生
    CHECK_FOR_FORK();
    
    // 如果运行循环正在被释放,则直接返回运行结束状态
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    
    // 加锁以保护运行循环的状态
    __CFRunLoopLock(rl);
    
    // 根据模式名称查找对应的运行模式
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    // 如果找不到模式或模式中没有注册任何事件源,则不运行循环直接返回
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode); // 解锁模式(如果已锁定)
        __CFRunLoopUnlock(rl); // 解锁运行循环
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    
    // 保存本次运行前的数据状态
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    
    // 记录上一次运行的模式
    CFRunLoopModeRef previousMode = rl->_currentMode;
    
    // 更新当前运行模式
    rl->_currentMode = currentMode;
    
    // 初始化运行结果为"已完成"
    int32_t result = kCFRunLoopRunFinished;
    
    // 1. 通知观察者:运行循环即将进入
    if (currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 2. 实际运行运行循环
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 10. 通知观察者:运行循环已退出
    if (currentMode->_observerMask & kCFRunLoopExit) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    // 解锁当前模式
    __CFRunLoopModeUnlock(currentMode);
    
    // 恢复之前保存的运行数据状态
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    
    // 还原上一次的运行模式
    rl->_currentMode = previousMode;
    
    // 最终解锁运行循环
    __CFRunLoopUnlock(rl);
    
    // 返回运行结果
    return result;
}

//这段代码详细展示了在给定模式下启动并运行一个运行循环的流程,包括初始化、通知观察者、实际执行循环、以及最终清理和返回结果等关键步骤。

如果指定了一个不存在的mode来运行RunLoop,那么会失败,mode不会被创建,所以这里传入的mode必须是存在的

如果指定了一个mode,但是这个mode中不包含任何modeItem,那么RunLoop也不会运行,所以必须要* 传入至少包含一个modeItem的mode

在进入run loop之前通知observer,状态为kCFRunLoopEntry

在退出run loop之后通知observer,状态为kCFRunLoopExit

__CFRunLoopRun

RunLoop的运行的最核心函数是__CFRunLoopRun,这个函数直接执行了部分工作流程;

源码如下(这里直接用别人公开的源码以及部分注释):

objectivec 复制代码
/**
 *  运行run loop
 *
 *  @param rl              运行的RunLoop对象
 *  @param rlm             运行的mode
 *  @param seconds         run loop超时时间
 *  @param stopAfterHandle true:run loop处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  @param previousMode    上一次运行的mode
 *
 *  @return 返回4种状态
 */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //获取系统启动后的CPU运行时间,用于控制超时时间
    uint64_t startTSR = mach_absolute_time();
    
    //如果RunLoop或者mode是stop状态,则直接return,不进入循环
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    //mach端口,在内核中,消息在端口之间传递。 初始为0
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    //判断是否为主线程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    //如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        //mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    
    //GCD管理的定时器,用于实现runloop超时机制
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    
    //立即超时
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    }
    //seconds为超时时间,超时时执行__CFRunLoopTimeout函数
    else if (seconds <= TIMER_INTERVAL_LIMIT) {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    }
    //永不超时
    else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
    
    //标志位默认为true
    Boolean didDispatchPortLastTime = true;
    //记录最后runloop状态,用于return
    int32_t retVal = 0;
    do {
        //初始化一个存放内核消息的缓冲池
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
        //取所有需要监听的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //设置RunLoop为可以被唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        //2.通知observer,即将触发timer回调,处理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3.通知observer,即将触发Source0回调
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //执行加入当前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //4.处理source0事件
        //有事件处理返回true,没有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //执行加入当前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //如果没有Sources0事件处理 并且 没有超时,poll为false
        //如果有Sources0事件处理 或者 超时,poll都为true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        //第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //从缓冲区读取消息
            msg = (mach_msg_header_t *)msg_buffer;
            //5.接收dispatchPort端口的消息,(接收source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                //如果接收到了消息的话,前往第9步开始处理msg
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }
        
        didDispatchPortLastTime = false;
        
        //6.通知观察者RunLoop即将进入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //设置RunLoop为休眠状态
        __CFRunLoopSetSleeping(rl);
        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //这里有个内循环,用于接收等待端口的消息
        //进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            //7.接收waitSet端口的消息
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            //收到消息之后,livePort的值为msg->msgh_local_port,
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.
        
        __CFPortSetRemove(dispatchPort, waitSet);
        
 
        __CFRunLoopSetIgnoreWakeUps(rl);
        
        // user callouts now OK again
        //取消runloop的休眠状态
        __CFRunLoopUnsetSleeping(rl);
        //8.通知观察者runloop被唤醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
      
        //9.处理收到的消息
    handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);
        
#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            
            // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
            // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
            // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
            __CFRunLoopSetSleeping(rl);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            __CFRunLoopUnsetSleeping(rl);
            // If we have a new live port then it will be handled below as normal
        }
        
        
#endif
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
            //通过CFRunloopWake唤醒
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            //什么都不干,跳回2重新循环
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //如果是定时器事件
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            //9.1 处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        //如果是定时器事件
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
           //9.1处理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        //如果是dispatch到main queue的block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            //9.2执行block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            // 有source1事件待处理
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
                mach_msg_header_t *reply = NULL;
                //9.2 处理source1事件
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                if (NULL != reply) {
                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
                }
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
            }
        }
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
        __CFRunLoopDoBlocks(rl, rlm);
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            //进入run loop时传入的参数,处理完事件就返回
            retVal = kCFRunLoopRunHandledSource;
        }else if (timeout_context->termTSR < mach_absolute_time()) {
            //run loop超时
            retVal = kCFRunLoopRunTimedOut;
        }else if (__CFRunLoopIsStopped(rl)) {
            //run loop被手动终止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        }else if (rlm->_stopped) {
            //mode被终止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        }else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //mode中没有要处理的事件
            retVal = kCFRunLoopRunFinished;
        }
        //除了上面这几种情况,都继续循环
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}

其中还用到了__CFRunLoopServiceMachPort

__CFRunLoopServiceMachPort

这个函数在run loop中起到了至关重要的作用;

objectivec 复制代码
/**
 *  接收指定内核端口的消息
 *
 *  @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;  //唯一id
       
        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的应用

线程保活

有时我们会需要经常在一个子线程中执行任务,频繁的创建和销毁线程就会造成很多的开销,这时我们可以通过runloop来控制线程的生命周期。

objectivec 复制代码
```objective-c
// 定义属性:一个用于存储线程实例,一个用于控制RunLoop是否停止
@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, assign) BOOL stopped;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 设置视图背景色为绿色
    self.view.backgroundColor = [UIColor greenColor];
    
    // 创建一个按钮用于触发打印操作
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [self.view addSubview:button];
    [button addTarget:self action:@selector(pressPrint) forControlEvents:UIControlEventTouchUpInside]; // 点击按钮时调用pressPrint方法
    [button setTitle:@"执行任务" forState:UIControlStateNormal];
    button.frame = CGRectMake(100, 200, 100, 20); // 设置按钮位置和大小
    
    // 创建另一个按钮用于停止RunLoop
    UIButton *stopButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [self.view addSubview:stopButton];
    [stopButton addTarget:self action:@selector(pressStop) forControlEvents:UIControlEventTouchUpInside]; // 点击按钮时调用pressStop方法
    [stopButton setTitle:@"停止RunLoop" forState:UIControlStateNormal];
    stopButton.frame = CGRectMake(100, 400, 100, 20); // 设置按钮位置和大小
    
    // 初始化停止标志为NO
    self.stopped = NO;
    
    // 使用弱引用来避免循环引用
    __weak typeof(self) weakSelf = self;
    
    // 创建并启动一个新线程,在线程中执行RunLoop
    self.thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"线程---开始");
        
        // 向当前RunLoop添加端口,保持RunLoop活跃。使用NSDefaultRunLoopMode模式
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.stopped) {
            // 运行RunLoop直到指定日期或被显式停止,这里使用一个遥远的未来日期,意味着RunLoop会一直运行直到stopped为YES
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        
        NSLog(@"线程---结束");
    }];
    [self.thread start]; // 启动线程
}

- (void)pressPrint {
    // 在子线程中执行print方法
    [self performSelector:@selector(print) onThread:_thread withObject:nil waitUntilDone:NO];
}

// 子线程中执行的任务:打印当前线程信息
- (void)print {
    NSLog(@"%s, %@", __func__, [NSThread currentThread]);
}

- (void)pressStop {
    // 点击停止按钮时尝试停止子线程的RunLoop
    if (!_stopped) {
        // 在子线程上调用stop方法
        [self performSelector:@selector(stop) onThread:_thread withObject:nil waitUntilDone:YES];
    }
}

// 停止子线程的RunLoop
- (void)stop {
    // 设置停止标志为YES
    self.stopped = YES;
    
    // 使用CoreFoundation框架的函数直接停止当前RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s, %@", __func__, [NSThread currentThread]);
    
    // 清除对线程的强引用,允许线程在RunLoop停止后被释放
    self.thread = nil;
}

- (void)dealloc {
    // 当控制器被释放时打印dealloc日志
    NSLog(@"%s", __func__);
}


//这段代码通过在自定义的子线程中运行RunLoop,实现了按钮控制下的异步任务执行与RunLoop的动态停止功能。它展示了一个典型的手动管理RunLoop的场景,特别是在需要长时间后台处理或监听某些事件时非常有用。
相关推荐
Java小白笔记7 小时前
Mac中安装homebrew
macos
HerayChen10 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
hairenjing112311 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
DisonTangor12 小时前
苹果发布iOS 18.2首个公测版:Siri接入ChatGPT、iPhone 16拍照按钮有用了
ios·chatgpt·iphone
- 羊羊不超越 -12 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
小李飞刀李寻欢13 小时前
Mac电脑如何解压rar压缩包
macos·rar·解压
Java小白笔记13 小时前
Mac中禁用系统更新
macos
AndyFrank13 小时前
mac crontab 不能使用问题简记
linux·运维·macos
Mac新人13 小时前
一招解决Mac没有剪切板历史记录的问题
macos·mac
王拴柱13 小时前
Mac保护电池健康,延长电池使用寿命的好方法
macos·mac