【iOS】RunLoop

概念

一般来说线程执行完任务就会退出,而runloop作用就在于,线程一直在处理事件并且不会退出。

要实现runloop这种模式关键点就在于怎么管理事件/消息,让线程在没有处理消息时休眠以避免资源占用,在有消息到来的时候立刻被唤醒。

runloop就是一个可以管理需要处理的消息与事件的一个对象,CFRunloopRef可以理解为CoreFoundation框架内的NSRunLoop,它提供了纯C函数的API,但是它的API都是线程安全的,而NSRunLoop却不是线程安全的。

RunLoop和线程的关系

两者是一一对应的关系,app启动之后,程序进入主线程,Apple帮我们在主线长启动了一个RunLoop,如果我们开辟新的线程,默认是不开启RunLoop的,需要我们手动开启。子线程的RunLoop是懒加载形式的。

NSThread是pthread_t的OC层面封装。

下面我们看一下CoreFoundation中获取CFRunLoop的核心逻辑:(这个方法只有调用:

NSRunLoop currentRunLoop

CFRunLoopGetCurrent()

类似方法时才执行

objc 复制代码
//根据一个pthread线程,获取其对应的RunLoop,如果还没有就创建一个,并保存到全局字典中
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {//如果传入空线程就默认获取主线程的RunLoop
        t = pthread_main_thread_np();
    }

    __CFLock(&loopsLock);

    if (!__CFRunLoops) {//如果全局字典还没创建就先创建
        __CFUnlock(&loopsLock);

        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(
            kCFAllocatorSystemDefault,
            0,
            NULL,
            &kCFTypeDictionaryValueCallBacks
        );

        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//不管咋样先创建主线程RunLoop,系统优先将它放进全局管理表中

        CFDictionarySetValue(
            dict,
            pthreadPointer(pthread_main_thread_np()),
            mainLoop
        );//dict[主线程] = 主线程RunLoop;

				//如果当前线程失败了,说明别的线程已经设置成功了,所以自动释放
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }

        CFRelease(mainLoop);//字典已经持有了mainLoop,所以释放自己的那一份

        __CFLock(&loopsLock);
    }

  //从全局字典中查找当前线程的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(
        __CFRunLoops,
        pthreadPointer(t)
    );

    __CFUnlock(&loopsLock);

  //如果没找到就创建新的RunLoop
    if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);

        __CFLock(&loopsLock);

      //双重检查,防止当前线程没有持有锁的时候有别的线程创建了这个线程的runloop,并且放进了字典中
        loop = (CFRunLoopRef)CFDictionaryGetValue(
            __CFRunLoops,
            pthreadPointer(t)
        );

      //真正执行插入字典
        if (!loop) {
            CFDictionarySetValue(
                __CFRunLoops,
                pthreadPointer(t),
                newLoop
            );

            loop = newLoop;
        }

        __CFUnlock(&loopsLock);

        CFRelease(newLoop);//释放newloop,分两种情况。第一种就是newloop被存放进入字典了,字典会持有强引用,释放是合理的。第二种情况就是newloop还没有被放入字典,那么这个newloop就是多余的,也应该释放掉
    }

    if (pthread_equal(t, pthread_self())) {//如果当前函数获取的实现当前线程自己的RunLoop,就做一些额外的处理
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);//将RunLoop存入线程本地存储,便于以后线程的直接获取

      //设置runloop声明周期清理计数器:当线程结束时,系统会调用TSD析构函数
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {//取出一个RunLoop清理计数器,如果是0,说明当前线程还没有设置过runloop的退出清理逻辑
            _CFSetTSD(
                __CFTSDKeyRunLoopCntr,
                (void *)(PTHREAD_DESTRUCTOR_ITERATIONS - 1),//线程退出时,线程本地存储的析构函数最多会被反复调用多少轮
                (void (*)(void *))__CFFinalizeRunLoop//线程销毁时,清理该线程对应的RunLoop资源
            );
        }
    }

    return loop;
}

RunLoop对外接口

在CoreFoundation中,关于RunLoop有五个类:

objc 复制代码
CFRunLoopRef           // RunLoop 本体
CFRunLoopModeRef       // RunLoop 的运行模式
CFRunLoopSourceRef     // 输入源 / 事件源
CFRunLoopTimerRef      // 定时器
CFRunLoopObserverRef   // 观察者

每一个runloop中包含若干个Mode,每一个Mode包含若干个Source、Obserser、Timer

mode(运行模式)

一个RunLoop可以包含多个 Mode,但 RunLoop 一次只能运行在一个 Mode 下。这个Mode成为CurrentMode,如果需要切换Mode,需要先退出Loop,指定一个Mode进入,这是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

objc 复制代码
struct __CFRunLoopMode {
    CFStringRef _name;   // Mode 名称
    CFMutableSetRef _sources0; //非基于Port的事件源,特点:不会主动唤醒RunLoop(需要别人唤醒),需要手动标记为待处理。 手动还从RunLoop然后RunLoop才会处理它
    CFMutableSetRef _sources1; //基于 Port 的事件源,通常和 Mach Port 有关。可以主动唤醒RunLoop,由内核消息驱动,RunLoop可以睡眠等待Source1的消息
    CFMutableArrayRef _timers;//保存当前Mode下的所有定时器
    CFMutableArrayRef _observers;//监听当前RunLoop,观察RunLoop正走到哪个阶段
};

苹果提供的几种Mode如下:

  • 常见iOS/CF/F中:
Mode 名称 所属层级 是否真实 Mode 作用 典型场景
kCFRunLoopDefaultMode Core Foundation 主线程空闲时默认运行模式 C 层 RunLoop 默认 Mode。
NSDefaultRunLoopMode Foundation 主线程空闲时默认运行模式 OC 层默认 Mode,本质对应 kCFRunLoopDefaultMode,处理常规任务(Timer、网络回调等)
UITrackingRunLoopMode UIKit UI 跟踪模式,处理滚动、触摸等交互事件 滚动、拖拽、触摸追踪时自动激活,优先保证流程性
kCFRunLoopCommonModes Core Foundation 通用模式,包含default和Tracking C 层把 Source / Timer / Observer 加入常用 Mode 集合
NSRunLoopCommonModes Foundation 通用模式,包含default和Tracking OC 层常用,用来解决 Timer 滑动时不触发问题
  • iOS内部常见:
Mode 名称 是否公开推荐使用 作用 说明
UIInitializationRunLoopMode App 启动初始化阶段使用 启动完成后一般不会继续使用
GSEventReceiveRunLoopMode 接收系统事件 和底层 GraphicsServices 事件分发有关,系统内部事件,开发者无需手动调用
UIApplicationLaunchOptionsRemoteNotificationKey 相关启动过程中的临时 Mode 启动阶段事件处理 属于系统内部调度细节
com.apple.main-thread 相关内部 Mode 系统内部主线程调度 调试时可能看到,不作为业务使用

当我们程序运行而画面静止,它处于kCFRunLoopDefaultMode,如果进行滚动,自动切换到UITrackingRunLoopMode模式,如果我们需要定时器在两种情况下都生效,则需要手动的设置到NSRunLoopCommonModes状态。

objc 复制代码
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(nextPage) userInfo:nil repeats:YES];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
[loop addTimer:self.timer forMode:NSRunLoopCommonModes];

Observer(观察者)

可以监听RunLoop的工作状态,什么时候休眠,什么时候被唤醒,通过对应状态通知系统执行对应工作。

objc 复制代码
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),           // 即将进入 Loop
    kCFRunLoopBeforeTimers = (1UL << 1),    // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2),   // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),   // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),    // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),            // 即将退出 Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   // 所有的状态
};

其中的Timer和Source就是runloop需要完成的任务

Mach Port 是 macOS / iOS 内核提供的一种"进程/线程间通信入口",RunLoop 底层就是通过等待这些 Port 上的消息来实现休眠与唤醒的。

iOS的内核是基于Mach的,Mach提供了一套IPC(进程间通信机制)

当线程睡眠时,等待某些Mach Port上出现消息,当发现又消息时,内核会唤醒这个线程,RunLoop继续往下执行

类型 是否基于 Mach Port 能否主动唤醒 RunLoop 例子
Source0 不能 自定义事件、performSelector 某些场景
Source1 Mach Port 消息、系统事件、线程间通信(有消息发送到Port时,内核会唤醒等待这个port的线程来处理)

Mach Port 是 iOS / macOS 内核层的消息通信端口,RunLoop 在休眠时会进入内核等待相关 Port 的消息。Source1 收到 Mach Port 消息可以直接唤醒 RunLoop;Timer 到时间会让 RunLoop 从定时等待中返回;CFRunLoopWakeUp 是人为把睡眠中的 RunLoop 叫醒;GCD 主队列任务会通知主线程 RunLoop 醒来执行 block;而超时则是 RunLoop 等待时间到了,即使没有新事件也要返回。

子线程保活

objc 复制代码
@property (nonatomic, strong) NSCondition *portCondition;
@property (nonatomic, assign) BOOL portReady;
@property (nonatomic, strong)NSMachPort* port;
@property (nonatomic, strong)NSThread* thread;
- (void)start;
- (void)sendDemoMessage;

- (void)start {
    self.portCondition = [[NSCondition alloc] init];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
    [self.thread start];
}

- (void)threadMain {
    @autoreleasepool {
        self.port = [NSMachPort port];
        self.port.delegate = self;
        [[NSRunLoop currentRunLoop] addPort:self.port forMode:NSDefaultRunLoopMode];
        [self.portCondition lock];
        self.portReady = YES;
        [self.portCondition signal];
        [self.portCondition unlock];
        NSLog(@"MachPort 子线程 RunLoop 已启动: %@", [NSThread currentThread]);
        [[NSRunLoop currentRunLoop] run];
    }
}

通过向当前线程的RunLoop中添加一个基于Mach Port的输入源,使得Runloop能够进入休眠等待状态,不会因为没有任务直接退出

然后通过实现代理方法接收Port消息:(当 Port 收到消息时,RunLoop 会被唤醒,然后执行这个方法)

objc 复制代码
- (void)handleMachMessage:(void *)msg {
    mach_msg_header_t *header = (mach_msg_header_t *)msg;
    NSLog(@"收到 Mach 消息,当前线程:%@", [NSThread currentThread]);
    NSLog(@"message id: %u, size: %u", header->msgh_id, header->msgh_size);
}

接下来就是发送消息给这个Port:

objc 复制代码
- (void)sendDemoMessage {
    [self.portCondition lock];
    while (!self.portReady) {
        [self.portCondition wait];
    }
    [self.portCondition unlock];

    NSPort *replyPort = [NSMachPort port];
    NSMutableArray *components = [NSMutableArray array];
    NSData *textData = [@"hello from main thread" dataUsingEncoding:NSUTF8StringEncoding];
    [components addObject:textData];

    BOOL success = [self.port sendBeforeDate:[NSDate dateWithTimeIntervalSinceNow:1] msgid:1001 components:components from:replyPort reserved:0];
    NSLog(@"主线程发送 Mach 消息%@,当前线程:%@", success ? @"成功" : @"失败", [NSThread currentThread]);
}

timer

基于时间的触发器,包含一个时间长度和一个回调,当其加入到runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行对应回调。(其实就是在对应秒数间隔的时候,将需要完成的任务加入到任务列表)

source

source 是 RunLoop 的事件源抽象,不是数据类,也不是 Objective-C 协议;它表示 RunLoop 可以从哪里接收事件,并在事件到来时执行对应回调。NSMachPort 加入 RunLoop 后,本质上就是给 RunLoop 添加了一个基于端口的 Source1。

源码中定义了两种Source:

  • source0 是 RunLoop 中"非基于端口的事件源",主要处理 App 内部逻辑事件。它本身不能靠内核消息自动唤醒 RunLoop,通常需要 App 或框架主动标记并唤醒 RunLoop。
objc 复制代码
typedef struct {
    CFIndex version;
    void *  info;//传给source的自定义数据,如果我们希望回调中能拿到某个对象、结构体、任务队列,就可以将其放到info中
    const void *(*retain)(const void *info);//当source需要持有info时调用,如果传的是普通C指针,或者自己管理生命周期的话,可以设为null
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);//source的描述信息
    Boolean (*equal)(const void *info1, const void *info2);//判断两个source的info时都相等
    CFHashCode  (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);//当这个 Source 被加入某个 RunLoop 的某个 mode 时,会调用 schedule,用于记录source被添加到了哪个线程的RunLoop上
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);//Source 从某个 RunLoop 的某个 mode 中移除时调用,主要用作一些清理工作
    void    (*perform)(void *info);//source被触发时调用
} CFRunLoopSourceContext;

CFRunLoopSourceContext说白了就是告诉RunLoop这个source0怎么保存数据,怎么加入,怎么移除,被触发时具体执行啥。

Source1:负责把系统事件从外部送进 App,并唤醒 RunLoop

Source0:负责 App 内部进一步分发和处理这个事件

使用示例

objc 复制代码
#import "CustomRunLoopSourceDemo.h"

@interface CustomRunLoopSourceDemo ()
@property (nonatomic, strong) NSThread *workerThread;
@property (nonatomic, strong) NSCondition *condition;
// 子线程对应的 RunLoop。主线程后面需要用它来 wake up。
@property (nonatomic, assign) CFRunLoopRef workerRunLoop;
// 自定义的 RunLoop Source。主线程通过 signal 它来制造"事件到来"。
@property (nonatomic, assign) CFRunLoopSourceRef source;
// 用来标记 source 是否已经创建完成,避免主线程过早触发。
@property (nonatomic, assign) BOOL sourceReady;
- (void)performSourceTask;
@end

static void CustomSourcePerformRoutine(void *info) {
    // C 级别的 RunLoop 回调不能直接捕获 Objective-C 对象,所以这里通过 context.info 把 self 桥接回来。
    CustomRunLoopSourceDemo *demo = (__bridge CustomRunLoopSourceDemo *)info;
    [demo performSourceTask];
}

@implementation CustomRunLoopSourceDemo

- (void)dealloc {
    if (_source) {
        CFRelease(_source);
    }

    if (_workerRunLoop) {
        CFRelease(_workerRunLoop);
    }
}

- (void)start {
    // 条件锁只做一件事:保证主线程触发 Source 之前,子线程已经把 RunLoop 和 Source 都准备好
    self.condition = [[NSCondition alloc] init];

    // 专门开一个常驻子线程,后续这个线程会一直跑自己的 RunLoop。
    self.workerThread = [[NSThread alloc] initWithTarget:self selector:@selector(workerThreadEntry) object:nil];
    [self.workerThread start];
}

- (void)workerThreadEntry {
    @autoreleasepool {
        // 取到"当前子线程"的 RunLoop,并持有一份引用,
        // 这样主线程后面可以对它执行 CFRunLoopWakeUp。
        self.workerRunLoop = (CFRunLoopRef)CFRetain(CFRunLoopGetCurrent());

        CFRunLoopSourceContext context = {0};
        // 把 self 塞进 context.info,供 perform 回调时取回。
        context.info = (__bridge void *)self;
        // 指定 Source 被处理时真正执行的函数。
        context.perform = CustomSourcePerformRoutine;

        // 创建一个纯手动触发的 Source,它不会像 Timer/Port 那样自动来事件,必须我们自己 signal。
        self.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

        // 把 Source 挂到这个子线程的默认 RunLoop 模式里。
        // 只有加入 RunLoop 之后,它才有机会被 RunLoop 调度执行。
        CFRunLoopAddSource(self.workerRunLoop, self.source, kCFRunLoopDefaultMode);

        [self.condition lock];
        self.sourceReady = YES;
        [self.condition signal];
        [self.condition unlock];

        NSLog(@"子线程 RunLoop 和自定义 Source 已就绪: %@", [NSThread currentThread]);

        // 启动 RunLoop,线程会在这里进入事件循环。
        // 没有事件时它会休眠;被 wake up 或有事件到来时会继续处理。
        CFRunLoopRun();
    }
}

- (void)triggerFromMainThread {
    // 如果子线程的 Source 还没准备好,主线程在这里等待。
    [self.condition lock];
    while (!self.sourceReady) {
        [self.condition wait];
    }
    [self.condition unlock];

    NSLog(@"主线程开始 Signal + WakeUp: %@", [NSThread currentThread]);

    // 第一步:signal
    // 只是把 source 标记为"待处理",并不会立刻执行回调。
    CFRunLoopSourceSignal(self.source);

    // 第二步:wake up
    // 如果子线程 RunLoop 此时正在休眠,单独 signal 还不够,
    // 需要显式唤醒它,这样它才会尽快去处理刚刚被标记的 source。
    CFRunLoopWakeUp(self.workerRunLoop);
}

- (void)performSourceTask {
    // 当子线程的 RunLoop 发现这个 source 已被 signal 后,
    // 就会回调到这里。注意这里执行线程是子线程,不是主线程。
    NSLog(@"自定义 Source 回调执行,当前线程:%@", [NSThread currentThread]);
}

@end
  
//controller中:
   self.runLoopSourceDemo = [[CustomRunLoopSourceDemo alloc] init];
  [self.runLoopSourceDemo start];
  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [self.runLoopSourceDemo triggerFromMainThread];
  });
  • Source2:由RunLoop内核管理,Mach Port驱动,包含了一个mach_port和一个回调,被用于用过内核和其他线程相互发送消息,能够主动唤醒主线程。
objc 复制代码
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底层原理

objc 复制代码
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(),
                         kCFRunLoopDefaultMode,
                         1.0e10,
                         false);//stopAfterHandle 为 false,表示处理完一个事件后不退出,而是继续循环
}

//指定mode启动runloop(RunLoop 每次只能运行在一个 Mode 下)
int CFRunLoopRunInMode(CFStringRef modeName,
                       CFTimeInterval seconds,
                       Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(),
                                modeName,
                                seconds,
                                stopAfterHandle);
}

//根据传入的modeName,找到当前runloop中对应的mode对象
int CFRunLoopRunSpecific(CFRunLoopRef runloop,
                         CFStringRef modeName,
                         CFTimeInterval seconds,
                         Boolean stopAfterHandle) {
    
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop,
                                                       modeName,
                                                       false);
    
    if (__CFRunLoopModeIsEmpty(currentMode)) {//如果mode中啥也没有,那么runloop没有任何时间可以监听,也没有理由睡眠,会直接返回
        return kCFRunLoopRunFinished;
    }
    
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);//通知所有监听了kCFRunLoopRNtry状态的observer,即将进入RunLoop
    
    int result = __CFRunLoopRun(runloop,
                                currentMode,
                                seconds,
                                stopAfterHandle);
    
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopExit);
    
    return result;
}

//真正循环部分
int __CFRunLoopRun(CFRunLoopRef runloop,
                   CFRunLoopModeRef currentMode,
                   CFTimeInterval seconds,
                   Boolean stopAfterHandle) {
    
    Boolean sourceHandledThisLoop = false;
    int retVal = 0;
    
    do {
        sourceHandledThisLoop = false;
        
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);//通知observer即将处理timer
        
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);//通知即将处理source
        
        __CFRunLoopDoBlocks(runloop, currentMode);//处理一些RunLoop内部维护的block
        
        sourceHandledThisLoop = __CFRunLoopDoSources0(runloop,
                                                      currentMode,
                                                      stopAfterHandle);//处理source0,这个过程中可能向RunLoop中添加了新的block
        
        __CFRunLoopDoBlocks(runloop, currentMode);//这里需要再处理一次可能添加的新block
        
      //某些source0执行过程中可能间接触发基于端口的消息,发现Mach Port消息准备好了,所以runloop会先尝试一些取消息
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg);
            if (hasMsg) {
                goto handle_msg;
            }
        }
        //如果本轮没有source0,那么RunLoop准备休眠(主线程的 AutoreleasePool 通常会在这个阶段释放旧池并创建新池)
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
        
      //进入内核等待
        __CFRunLoopServiceMachPort(waitSet,
                                   &msg,
                                   sizeof(msg_buffer),
                                   &livePort);
        
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);//被唤醒,通知observer
        
    handle_msg://RunLoop 醒来后,手里会拿到一个消息,需要先判断消息的类型
        
        if (msg_is_timer) {
            __CFRunLoopDoTimers(runloop,
                                currentMode,
                                mach_absolute_time());
        }
        else if (msg_is_dispatch) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        }
        else {
          //处理source1的任务
            CFRunLoopSourceRef source1 =
                __CFRunLoopModeFindSourceForMachPort(runloop,
                                                     currentMode,
                                                     livePort);
            
            sourceHandledThisLoop =
                __CFRunLoopDoSource1(runloop,
                                     currentMode,
                                     source1,
                                     msg);
            
            if (sourceHandledThisLoop) {
                mach_msg(reply,
                         MACH_SEND_MSG,
                         reply);
            }
        }
        
        __CFRunLoopDoBlocks(runloop, currentMode);
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        }
        else if (timeout) {
            retVal = kCFRunLoopRunTimedOut;
        }
        else if (__CFRunLoopIsStopped(runloop)) {
            retVal = kCFRunLoopRunStopped;
        }
        else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
            retVal = kCFRunLoopRunFinished;
        }
        
    } while (retVal == 0);
    
    return retVal;
}

先找到当前 Mode,如果 Mode 里有事件源,就进入循环;每一轮先处理 Source0 和 Blocks,没事就通过 mach_msg 进入睡眠;等 Timer、Source1、GCD 主队列任务或手动唤醒到来后,再醒来处理消息;最后根据是否超时、是否被停止、Mode 是否为空来决定继续还是退出。

RunLoop应用

  • autoreleasePool

苹果底层注册了两个Observer,第一个用来监听RunLoop的entry,在这个时候创建一个自动释放池,并提供最高优先级,保证释放池的创建发生在其他回调之前。第二个observer监听两个事件,一个 BeforeWaiting,一个 ExitBeforeWaiting 的时候,干两件事,先释放旧的池,然后创建一个新的池,所以这个时候,自动释放池就会有一次释放的操作,是在 RunLoop 即将进入休眠的时候。Exit 的时候,再次释放自动释放池,这里也有一次释放的操作

  • tableView延时加载图片,保证流畅

用户在快速滑动tableView时,滑动过的图片会一直加载,但滑动过的图片可能并不需要呈现出来,加载会浪费CPU资源,我们可以使用RunLoop的mode避免滑动时加载图片。如下,只允许指定mode情况下才能加载图片:

objc 复制代码
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName.png"] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
  • 线程保活:前面有过示例代码,减少线程反复退出,频繁创建与销毁线程造成的效率低下。
相关推荐
区块block4 小时前
iOS 27 重磅开放:第三方 AI 模型自由切换,苹果生态告别封闭
人工智能·ios
人月神话Lee7 小时前
【图像处理】亮度与对比度——图像的线性变换
ios·ai编程·图像识别
bryceZh8 小时前
iOS26适配-UISplitViewController配置分栏和分屏
ios·ui kit
songgeb8 小时前
NumberFormatter 货币格式化属性详解
ios·swift
最后一支迷迭香10 小时前
苹果的MacOS系统适合做Java开发吗
java·开发语言·macos
AirDroid_cn11 小时前
macOS Sequoia协同编辑:Pages文档冲突自动合并全攻略
macos
irpywp11 小时前
平台禁下载、格式不兼容、剪辑太麻烦,Media Downloader:mac 原生媒体下载工具,一站式解决视频下载、转码、裁剪、管理难题
macos·开源·github·音视频·媒体
for_ever_love__11 小时前
UI学习:数据驱动ce l l
学习·ui·ios·objective-c
KillerNoBlood12 小时前
2026移动端跨平台开发面经总结
android·算法·flutter·ios·移动开发·鸿蒙·kmp