iOS RunLoop 介绍

1 RunLoop 简介

RunLoop 即运行循环,用于管理事件源(如触摸、定时器、网络请求等)和线程的消息循环。它允许应用程序保持活动状态,并在没有任务执行时进入休眠状态以节省能耗。

为什么 main 函数不会退出?

objectivec 复制代码
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

UIApplicationMain 方法内部默认开启了主线程的 RunLoop,并执行了一段无限循环的代码。UIApplicationMain 函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。

2 RunLoop 原理

RunLoop 的实现依赖于底层操作系统的机制,其中 mach port 是其中的关键之一。Mach 是 macOS 和 iOS 等系统的内核,而 mach port 是一种用于进程间通信的机制。在 RunLoop 中,每个线程都有一个与之关联的 mach port,用于接收系统事件和消息。当线程进入休眠状态时,RunLoop 会将线程的 mach port 加入到内核的事件监视列表中,从而实现了休眠机制。当有事件发生时,内核会将相应的 mach port 标记为可用,RunLoop 被唤醒,线程从休眠状态中恢复,并处理相应的事件。

通过这种机制,RunLoop 实现了高效地处理事件和消息,并在需要时将线程置于休眠状态,以节省资源。这使得 iOS 和 macOS 应用能够高效地响应用户的操作,同时保持系统的稳定性和响应性。

RunLoop 通过 mach_msg() 函数接收、发送消息。它的本质是调用 mach_msg_trap() 函数,相当于是一个系统调用,会触发内核状态切换。在用户态调用 mach_msg_trap() 时会切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作。当有事件时,系统内核通过mach_msg()或者mach port方法将事件发送给对应的 Runloop,Runloop 收到事件后从休眠状态切换到唤醒状态,并从内核态->用户态

Mach消息发送机制

RunLoop 机制(do-while循环)如下:

  1. 通知 Observer:即将启动 RunLoop

  2. 通知 Observer:即将处理 Timer 事件

  3. 通知 Observer:即将处理 Source0 事件

  4. 处理 Source0 事件

  5. 如果 Source1 就绪,进入步骤9

  6. 通知 Observer:线程即将休眠

  7. 线程休眠,等待唤醒

    • Source1(mach port) 事件
    • Timer 定时触发
    • RunLoop 超时
    • 外部手动唤醒(其他线程\进程)
    • 有提交到 main queue 上的 block(当前 RunLoop 是 Main RunLoop 时才有这种情况)
  8. 通知 Observer:线程刚被唤醒

  9. 处理唤醒时收到的事件

    • 如果定时器启动,处理定时器事件并重启 RunLoop,进入步骤2
    • 如果 RunLoop 被显式唤醒而且时间还没超时,重启 RunLoop,进入步骤2
    • 如果输入源启动,传递相应的消息
  10. 通知 Observer:即将退出 RunLoop

3 RunLoop 组成

官方文档中使用下图描述 RunLoop 模型:

3.1 运行循环

RunLoop: CFRunLoopRefNSRunLoop

RunLoop 是由两个对象组成:NSRunLoop 和 CFRunLoopRef 。两者间的转换是免费桥转换(toll-free bridged)的。

RunLoop 和 Thread 一一对应,且是平级的关系。Thread 默认是没有对应的 RunLoop 的,仅当主动调用获取方法时,才会创建 RunLoop。

主线程 RunLoop 会由系统自动创建并启动以响应事件,但子线程并不会自动创建 Runloop,手动创建的 RunLoop 需要显式调用 run 方法才会启动。

objectivec 复制代码
// 苹果不允许直接创建 RunLoop,只提供了两个自动获取的函数

// NSRunLoop 获取
[NSRunLoop currentRunLoop];
[NSRunLoop mainRunLoop];

// CFRunLoopRef 获取
CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);
CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);

CFRunLoopRef 是 Core Foundation 框架中的,它是基于C语言的API,这些接口都是线程安全的。Core Foundation 是一个跨平台的通用库,不仅支持Mac和iOS,同时也支持Windows。

NSRunLoop 是基于 CFRunLoopRef 的封装,并提供面向对象的API,这些接口不是线程安全的。

3.2 运行模式

Mode:CFRunLoopModeRefNSRunLoopMode

一个 RunLoop 对象中可能包含多个 Mode。每个 Mode 有4个集合结构,对应着当前 Mode 处理的事件源(source0,source1),定时器,观察者。

RunLoop 每次调用 run 方法处理任务时,只能指定一个 Mode。Mode 指定 RunLoop 本次可以处理的任务。

处理新的任务时,需要切换 Mode,再重新调用 run 方法。主要是为了分隔开不同的 Source/Timer/Observer,让它们之间互不影响。

运行模式类型:

  1. CFRunLoopModeRef
    • kCFRunLoopDefaultMode:默认模式,处理用户交互事件(UI事件)。
    • kCFRunLoopCommonModes:占位标记,包含默认模式和其他自定义模式。
    • 自定义模式:管理特定类型的任务。
  2. NSRunLoopMode
  • NSDefaultRunLoopMode:默认模式,处理用户交互事件(UI事件)。

  • NSRunLoopCommonModes:占位标记,包含默认模式和其他自定义模式。

  • UIInitializationRunLoopMode:启动应用时的第一个 Mode,启动完成后就不再使用。

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

  • UITrackingRunLoopMode:用于处理跟踪用户界面事件,例如滚动、拖动等。

  • 自定义模式:管理特定类型的任务。

3.3 事件源

Source:CFRunLoopSourceRefNSRunLoopSource

运行循环源的引用类型,用于向运行循环发送事件。有两种类型的运行循环源:Source0Source1

  1. Source0

    • 用于处理非基于端口的自定义输入源。
    • Source0 不与端口相关联,而是直接由用户创建和管理。
    • 一种简单的源,适用于通过代码触发的事件。
    • 源的事件类型可以通过 CFRunLoopSourceContext 结构中的回调函数来定义。
    • 需要手动调用 CFRunLoopWakeUp(CFRunLoopRef rl) 来唤醒线程。
  2. Source1

  • 用于处理基于端口的输入源。
  • Source1 通常与 Mach 端口相关联,用于与其他线程或系统进行通信。
  • 通常用于硬件事件,如触摸、摇晃、旋转等。
  • Source1 与 mach_msg 机制集成,以便在源上等待事件。
  • 能通过 mach port 主动唤醒 RunLoop。

3.4 定时器

Timer:CFRunLoopTimerRefNSTimer

基于时间的触发器,两者是免费桥转换的(toll-free bridged)。

当 Mode 加入到 RunLoop 时,RunLoop 对象会在注册的定时器时间到达时唤醒关联的线程对象来执行定时器的回调。如果线程阻塞,一直等到下一个周期时间点触发;如果不在这个 Mode 下,触发点将不会执行。

常用的定时器:

  • NSTimer:由 RunLoop 处理,内部结构为 CFRunLoopTimerRef。
  • PerformSelector:afterDelay:同 NSTimer。
  • GCD Timer:由GCD自己实现,不通过RunLoop。
  • CADisplayLink:通过向 RunLoop 投递 source1 实现回调。

3.5 观察者

Observer:CFRunLoopObserverRefNSRunLoopObserver

每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接收到这个变化。

RunLoop 活动状态枚举:

ini 复制代码
 /* 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 包含所有状态  
};

系统会在应用启动时,向 Main RunLoop 里注册两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

  1. 第一个 Observer 监视的事件:

Entry 时调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

  1. 第二个 Observer 监视了两个事件:

BeforeWaiting 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。其 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

  1. 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

3.6 对象间的关系

RunLoop 和 Thread 一一对应

RunLoop 和 Mode 是一对多的

Mode 和 Source、Timer、Observer 是一对多的

4 RunLoop 数据结构

4.1 运行循环

Core Foundation中 RunLoop 类是 CFRunLoopRef,其对应的结构体是 __CFRunLoop

objectivec 复制代码
typedef struct __CFRunLoop *CFRunLoopRef;

struct __CFRunLoop {
    CFRuntimeBase _base; // Core Foundation 对象的基础结构
    pthread_mutex_t _lock; // 用于保护运行循环的互斥锁
    __CFPort _wakeUpPort; // 唤醒端口,在调用 CFRunLoopWakeUp 时使用
    Boolean _unused; // 表示该字段当前是否未被使用
    volatile _per_run_data *_perRunData; // 指向与特定运行循环相关的数据的指针
    pthread_t _pthread; // 运行循环所在的线程
    uint32_t _winthread; // Windows 平台上运行循环所在的线程标识
    CFMutableSetRef _commonModes; // 通用模式集合
    CFMutableSetRef _commonModeItems; // 通用模式中的事件源、定时器和观察者集合
    CFRunLoopModeRef _currentMode; // 当前活跃的模式
    CFMutableSetRef _modes; // 运行循环所有模式集合
    struct _block_item *_blocks_head; // 指向运行循环相关的 block 链表的头部
    struct _block_item *_blocks_tail; // 指向运行循环相关的 block 链表的尾部
    CFAbsoluteTime _runTime; // 运行循环的运行时间
    CFAbsoluteTime _sleepTime; // 运行循环的休眠时间
    CFTypeRef _counterpart; // 运行循环的对应对象
    ...
};

4.2 运行模式

__CFRunLoop 结构体中包含运行循环模式,其对应的结构体是 CFRunLoopModeRef 。其中包含事件源,定时器,观察者。

objectivec 复制代码
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base; // Core Foundation 对象的基础结构
    pthread_mutex_t _lock; // 用于保护模式的互斥锁
    CFStringRef _name; // 模式的名称
    Boolean _stopped; // 模式停止标志
    char _padding[3]; // 填充字节,保证结构体大小对齐
    CFMutableSetRef _sources0; // 非基于Port的事件源集合
    CFMutableSetRef _sources1; // 基于Port的事件源集合
    CFMutableArrayRef _observers; // 观察者数组
    CFMutableArrayRef _timers; // 定时器数组
    CFMutableDictionaryRef _portToV1SourceMap; // 基于Port的 V1 事件源映射表
    __CFPortSet _portSet; // Port 集合
    CFIndex _observerMask; // 观察者掩码,用于确定哪些类型的观察者在模式中处于激活状态
    ...
};

创建自定义的模式

objectivec 复制代码
CFRunLoopModeRef CFRunLoopModeCreate(
    CFStringRef name,
    CFAllocatorRef allocator
); 
  • name:运行循环模式的名称

  • allocator:内存分配器,传入 NULL 则使用默认的分配器

4.3 事件源

事件源是 RunLoop 的输入源的抽象类。有两事件源:Source0Source1

objectivec 复制代码
typedef struct __CFRunLoopSource *CFRunLoopSourceRef;

struct __CFRunLoopSource {
    CFRuntimeBase _base; // Core Foundation 对象的基础结构
    uint32_t _bits; // 存储事件源的状态信息
    pthread_mutex_t _lock; // 用于保护事件源的互斥锁
    CFIndex _order; // 事件源的优先级
    CFMutableBagRef _runLoops; // 存储与此事件源相关联的运行循环
    union { // 联合,用于保存source的信息,同时可以区分source是0还是1类型
        CFRunLoopSourceContext version0;
        CFRunLoopSourceContext1 version1;
    } _context;
    ...
}; 

CFRunLoopSourceContext 结构体对应事件源 Source0

objectivec 复制代码
typedef struct {
    CFIndex version; // 结构体的版本号 source0
    void *info; // 指向任意数据的指针,用于传递给源的回调函数
    const void *(*retain)(const void *info); // 保留回调函数指针,增加引用计数
    void (*release)(const void *info); // 释放回调函数指针,减少引用计数    CFStringRef (*copyDescription)(const void *info); // 获取info的描述字符串    Boolean (*equal)(const void *info1, const void *info2); // 比较两个info是否相等
    CFHashCode (*hash)(const void *info); // 计算info的哈希值
    void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); // 安排源在运行循环中运行
    void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); // 取消源在运行循环中的安排
    void (*perform)(void *info); // 执行源的操作
} CFRunLoopSourceContext; 

CFRunLoopSourceContext1 结构体对应事件源 Source1

objectivec 复制代码
typedef struct {
    CFIndex version; // 结构体的版本号 source1
    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_OSX || TARGET_OS_IPHONE
    mach_port_t (*getPort)(void *info); // 获取源的 Mach 端口的回调函数指针
    void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info); // 执行源事件处理的回调函数指针
#else
    void *(*getPort)(void *info); // 获取源的端口的回调函数指针
    void (*perform)(void *info); // 执行源事件处理的回调函数指针
#endif
} CFRunLoopSourceContext1;

创建自定义的事件源

objectivec 复制代码
CFRunLoopSourceRef CFRunLoopSourceCreate(
    CFAllocatorRef allocator, 
    CFIndex order, 
    CFRunLoopSourceContext *context
); 
  • allocator:内存分配器,可以传递 kCFAllocatorDefault
  • order:指定事件源的优先级,较低的值表示更高的优先级
  • context:事件源上下文结构体的指针,其中包含事件源的上下文信息,包括回调函数等

4.4 定时器

objectivec 复制代码
typedef struct __CFRunLoopTimer *CFRunLoopTimerRef;

struct __CFRunLoopTimer {
    CFRuntimeBase _base; // Core Foundation 对象的基础结构
    pthread_mutex_t _lock; // 用于保护定时器的互斥锁
    CFRunLoopRef _runLoop; // 指向运行循环的指针,表示定时器所属的运行循环
    CFMutableSetRef _rlModes; // 定时器所关联的运行循环模式
    CFAbsoluteTime _nextFireDate; // 定时器下一次触发的时间
    CFTimeInterval _interval; // 定时器触发的时间间隔
    CFTimeInterval _tolerance; // 定时器触发时间的容差范围
    uint64_t _fireTSR; // 定时器精确触发时间的 mach 绝对时间
    struct timeval _fireTV; // 定时器精确触发时间的 UNIX 绝对时间
    CFIndex _order; // 优先级,用于在同一运行循环中的多个定时器之间确定执行顺序
    CFRunLoopTimerCallBack _callout; // 定时器回调函数的指针    CFRunLoopTimerContext _context; // 定时器回调函数的上下文信息
    ...
};

CFRunLoopTimerContext 结构体

objectivec 复制代码
typedef struct {
    CFIndex version; // 结构体的版本号
    void *info; // 指向任意数据的指针,用于传递给定时器的回调函数
    const void *(*retain)(const void *info); // 复制info时增加引用计数
    void (*release)(const void *info); // 释放info时减少引用计数
    CFStringRef (*copyDescription)(const void *info); // 描述info的字符串
} CFRunLoopTimerContext; 

创建自定义的定时器

objectivec 复制代码
CFRunLoopTimerRef CFRunLoopTimerCreate(
    CFAllocatorRef allocator, 
    CFAbsoluteTime fireDate, 
    CFTimeInterval interval, 
    CFOptionFlags flags, 
    CFIndex order, 
    CFRunLoopTimerCallBack callout, 
    CFRunLoopTimerContext *context
); 
  • allocator:内存分配器,可以传递 kCFAllocatorDefault
  • fireDate:定时器首次触发的时间
  • interval:定时器触发的间隔时间。如果值为 0,则表示定时器只触发一次
  • flags:标志位,用于指定定时器的行为。如果值为 kCFRunLoopTimerOnce ,则表示定时器只触发一次
  • order:指定定时器的优先级,较低的值表示更高的优先级
  • callout:在定时器触发时执行相应的操作
  • context:包含定时器的上下文信息

4.5 观察者

objectivec 复制代码
typedef struct __CFRunLoopObserver *CFRunLoopObserverRef;

struct __CFRunLoopObserver {
    CFRuntimeBase _base; // Core Foundation 对象的基础结构
    pthread_mutex_t _lock; // 用于保护观察者的互斥锁
    CFRunLoopRef _runLoop; // 指向运行循环的指针,表示观察者所属的运行循环    CFOptionFlags _activities; // 位掩码,表示观察者要监听的运行循环事件
    CFIndex _order; // 优先级,用于在同一运行循环中的多个观察者之间确定执行顺序
    CFRunLoopObserverCallBack _callout; // 观察者回调函数的指针
    CFRunLoopObserverContext _context; // 观察者回调函数的上下文信息
    ...
}; 

CFRunLoopObserverContext 结构体

objectivec 复制代码
typedef struct {
    CFIndex version; // 结构体的版本号
    void *info; // 指向任意数据的指针,用于传递给观察者的回调函数
    const void *(*retain)(const void *info); // 复制info时增加引用计数
    void (*release)(const void *info); // 释放info时减少引用计数
    CFStringRef (*copyDescription)(const void *info); // 描述info的字符串
} CFRunLoopObserverContext; 

创建自定义的观察者

objectivec 复制代码
CFRunLoopObserverRef CFRunLoopObserverCreate(
    CFAllocatorRef allocator, 
    CFOptionFlags activities, 
    Boolean repeats, 
    CFIndex order, 
    CFRunLoopObserverCallBack callout, 
    CFRunLoopObserverContext *context
); 
  • allocator:内存分配器,可以传递 kCFAllocatorDefault
  • activities:指定希望观察的运行循环活动,可以传递 kCFRunLoopAllActivities
  • repeats:指定观察者是否重复执行
  • order:指定观察者的优先级,较低的值表示更高的优先级
  • callout:在运行循环活动发生时执行相应的操作
  • context:包含观察者的上下文信息

5 自定义 RunLoop

5.1 CFRunLoopRef 完整示例

objectivec 复制代码
#import <Foundation/Foundation.h>
#include <CoreFoundation/CoreFoundation.h>

// 事件源 被添加到Runloop时 回调函数
void sourceScheduleCallback(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {
    NSLog(@"Source Perform");
}

// 事件源 被取消时 回调函数
void sourceCancelCallback(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {
    NSLog(@"Source Perform");
}

// 事件源 被告知有事件要处理时 回调函数
void sourcePerformCallback(void *info) {
    NSLog(@"Source Perform");
}

// 定时器回调函数
void timerCallback(CFRunLoopTimerRef timer, void *info) {
    NSLog(@"Timer Callback");
}

// 观察者回调函数
void observerCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    NSLog(@"Observer Callback - Activity: %lu", activity);
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"RunLoop 进入运行状态");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"RunLoop 将要处理定时器事件");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"RunLoop 将要处理事件源事件");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"RunLoop 进入休眠前的状态");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"RunLoop 从休眠状态中唤醒后的状态");
            break;
        case kCFRunLoopExit:
            NSLog(@"RunLoop 即将退出运行状态");
            break;
        default:
            break;
    }
}

@interface RunLoopManager : NSObject

@property (nonatomic, assign) CFRunLoopRef runLoopRef;
@property (nonatomic, assign) CFRunLoopModeRef modeRef;
@property (nonatomic, assign) CFRunLoopSourceRef sourceRef;
@property (nonatomic, assign) CFRunLoopTimerRef timerRef;
@property (nonatomic, assign) CFRunLoopObserverRef observerRef;

- (void)createRunLoopWithError:(NSError **)error;
- (void)stopRunLoop:(CFRunLoopRef)runLoop;
- (void)releaseResources;

@end

@implementation RunLoopManager

- (instancetype)init {
    self = [super init];
    if (self) {
	_runLoopRef = NULL;
	_modeRef = NULL;
        _sourceRef = NULL;
        _timerRef = NULL;
        _observerRef = NULL;
    }
    return self;
}

// 创建并运行自定义运行循环
- (void)createRunLoopWithError:(NSError **)error {
    // 创建一个自定义模式
    CFStringRef modeRefName = CFSTR("modeRef");
    _modeRef = CFRunLoopModeCreate(modeRefName, NULL);
    if (!_modeRef) {
        if (error) {
            *error = [NSError errorWithDomain:@"RunLoopDomain" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"Failed to create run loop mode."}];
        }
    }

    // 创建一个自定义事件源
    CFRunLoopSourceContext sourceContext = {0, NULL, NULL, NULL, NULL, NULL, &sourceScheduleCallback, &sourceCancelCallback, &sourcePerformCallback};
    _sourceRef = CFRunLoopSourceCreate(NULL, 0, &sourceContext);
    if (!_sourceRef) {
        CFRelease(_modeRef);
        if (error) {
            *error = [NSError errorWithDomain:@"RunLoopDomain" code:1002 userInfo:@{NSLocalizedDescriptionKey: @"Failed to create run loop source."}];
        }
    }

    // 创建一个定时器
    CFRunLoopTimerContext timerContext = {0, NULL, NULL, NULL, NULL};
    CFTimeInterval interval = 1.0; // 每隔1秒触发一次
    _timerRef = CFRunLoopTimerCreate(NULL, CFAbsoluteTimeGetCurrent(), interval, 0, 0, &timerCallback, &timerContext);
    if (!_timerRef) {
        CFRelease(_sourceRef);
        CFRelease(_modeRef);
        if (error) {
            *error = [NSError errorWithDomain:@"RunLoopDomain" code:1003 userInfo:@{NSLocalizedDescriptionKey: @"Failed to create run loop timer."}];
        }
    }

    // 创建一个观察者
    CFRunLoopObserverContext observerContext = {0, NULL, NULL, NULL, NULL};
    _observerRef = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, true, 0, &observerCallback, &observerContext);
    if (!_observerRef) {
        // 处理错误,释放已创建的资源
        CFRelease(_timerRef);
        CFRelease(_sourceRef);
        CFRelease(_modeRef);
        if (error) {
            *error = [NSError errorWithDomain:@"RunLoopDomain" code:1004 userInfo:@{NSLocalizedDescriptionKey: @"Failed to create run loop observer."}];
        }
    }

    // 将输入源、定时器和观察者添加到自定义模式中
    CFRunLoopAddSource(CFRunLoopGetCurrent(), _sourceRef, _modeRef);
    CFRunLoopAddTimer(CFRunLoopGetCurrent(), _timerRef, _modeRef);
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), _observerRef, _modeRef);

    // 将自定义模式添加到运行循环中
    CFRunLoopAddMode(CFRunLoopGetCurrent(), _modeRef, kCFRunLoopCommonModes);

    // 获取创建的运行循环
    _runLoopRef = CFRunLoopGetCurrent();

    // 启动运行循环
    CFRunLoopRun();
}

/**
    在需要时发送事件并唤醒运行循环,确保自定义事件源的回调函数得到执行。
    CFRunLoopSourceSignal 用于标记自定义事件源已经准备好处理事件。当调用该函数时,运行循环会注意到事件源的状态已经改变,并准备调用事件源的回调函数。
    CFRunLoopWakeUp 用于唤醒当前的运行循环。如果运行循环处于休眠状态(没有待处理的事件),调用该函数可以立即唤醒运行循环,使其开始处理事件。
*/
- (void)signalAndWakeUpRunLoop {
    // 发出自定义事件源的信号
    CFRunLoopSourceSignal(_sourceRef);    
    // 唤醒运行循环
    CFRunLoopWakeUp(CFRunLoopGetCurrent());
}

// 停止指定的运行循环
- (void)stopRunLoop:(CFRunLoopRef)runLoop {
    CFRunLoopStop(runLoop);
}

// 释放资源
- (void)releaseResources {
    if (_sourceRef) {
        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), _sourceRef, _modeRef);
        CFRelease(_sourceRef);
    }
    if (_timerRef) {
        CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), _timerRef, _modeRef);
        CFRelease(_timerRef);
    }
    if (_observerRef) {
        CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), _observerRef, _modeRef);
        CFRelease(_observerRef);
    }
    if (_modeRef) {
        CFRunLoopRemoveMode(CFRunLoopGetCurrent(), _modeRef, kCFRunLoopCommonModes);
        CFRelease(_modeRef);
    }
}

@end

RunLoopManager 调用

ini 复制代码
// 创建并运行自定义运行循环
NSError *error = nil;
RunLoopManager *runLoopManager = [[RunLoopManager alloc] init];
[runLoopManager createRunLoopWithError:&error];
if (error) {    
    NSLog(@"Error: %@", error);
}
        
// 停止运行循环
[self stopRunLoop:runLoop];
        
// 释放资源
[self releaseResources];

6 Runloop 面试题

6.1 滑动 tableView 时,定时器还会生效吗?

现象:不会生效。

原因:默认情况下 RunLoop 在 kCFRunLoopDefaultMode 下运行,而当滑动 tableView 时,RunLoop 切换到 UITrackingRunLoopMode,而 NSTimer 仍是在 kCFRunLoopDefaultMode 下运行的,就无法接受处理 NSTimer 的事件。

解决方法:将 NSTimer 添加到 NSRunLoopCommonModes

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

6.2 如何使用 RunLoop 实现线程保活?

原因:每个线程都有一个 RunLoop,主线程的 RunLoop 默认是自动运行的。手动创建的线程默认没有开启 RunLoop,需要手动启动。

解决办法:

  1. 在当前线程开启一个 RunLoop

  2. 向 RunLoop 中添加一个 Port/Source,维持 RunLoop 的事件循环

  3. 启动该 RunLoop

    @autoreleasepool { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; self.messagePort = [NSPort port]; self.messagePort.delegate = self; [runLoop addPort:self.messagePort forMode:NSDefaultRunLoopMode]; [runLoop run]; // 当 Runloop 在跑圈时,下面的代码永远不会执行。 NSLog(@"runloop exit!!"); // 仅当子线程即将退出时,才会执行到这里 }

@autoreleasepool:因为系统不会为子线程自动创建 autorelease pool,为了防止内存泄漏,在所有的子线程开始前都要创建一个 autorelease pool。

[NSRunLoop currentRunLoop]:第一次调用方法时,会先去创建一个 RunLoop

addPort:forMode::如果 RunLoop 的 Mode 中一个 item 都没有,RunLoop 会自动退出。这里为当前 RunLoop 放了一个 mach port,让 RunLoop 一直空等。

如何退出一个 RunLoop?

  • 调用 void CFRunLoopStop(CFRunLoopRef rl)
  • 指定一个超时时间
  • 移除 RunLoop 所有的事件源和定时器

6.3 怎样保证子线程数据异步更新UI的时候不打断用户的滑动操作?

现象:当在子线程请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新UI,那么就会影响当前正在滑动的体验。

原因:主线程 RunLoop 由 UITrackingRunLoopMode 切换到 NSDefaultRunLoopMode 时更新 UI 影响用户体验。

解决办法:直接将更新 UI 事件放在主线程的NSDefaultRunLoopMode上执行。

objectivec 复制代码
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];

6.4 Runloop 卡顿检测

iOS RunLoop - 卡顿检测

6.5 RunLoop 在 iOS 系统中都有什么应用呢?

参考 iOS RunLoop完全指南

深入理解RunLoop

大致应用:

  1. UI 事件处理:包括触摸事件响应、手势识别等

  2. 定时器任务:定时器到期时触发的任务,包括界面刷新、数据更新等

  3. 网络请求:NSURLConnection

  4. GCD异步任务:例如 dispatch_async(dispatch_get_main_queue(), block)

  5. PerformSelector延迟任务:例如 PerformSelector:afterDealy:PerformSelectorOnThread(mainThread):

事件响应链:

  1. 用户触发事件
  2. 系统将事件转交到对应 APP 的事件队列
  3. APP 从消息队列头取出事件
  4. 交由 Main Window 进行消息分发
  5. 找到合适的 Responder 进行处理,如果没找到,则会沿着 Responder chain 返回到 APP 层,丢弃不响应该事件。
相关推荐
hunteritself1 小时前
谷歌Gemini发布iOS版App,live语音聊天免费用!
人工智能·ios·chatgpt·openai·语音识别
解压专家6662 小时前
7z 解压器手机版与解压专家:安卓解压工具对决
ios·智能手机·winrar·7-zip
袁代码3 小时前
SwiftUI开发教程系列 - 第十二章:本地化与多语言支持
开发语言·前端·ios·swiftui·swift·ios开发
️ 邪神4 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】水平布局
flutter·ios·鸿蒙·reactnative·anroid
_可乐无糖4 小时前
iOS UI自动化 Appium的元素定位方式及比较
ios·appium·自动化
软件聚导航8 小时前
uniapp 实现 ble蓝牙同时连接多台蓝牙设备,支持app、苹果(ios)和安卓手机,以及ios连接蓝牙后的一些坑
android·ios·uni-app
q5673152315 小时前
用 PHP或Python加密字符串,用iOS解密
java·python·ios·缓存·php·命令模式
Hgc5588866615 小时前
iOS 18.2 重磅更新:6个大动作
ios
️ 邪神15 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】标题栏
android·flutter·ios·鸿蒙·reatnative
lrlianmengba17 小时前
推荐一款好用的ios传输设备管理工具:AnyTrans for iOS
macos·ios·cocoa