【iOS】RunLoop学习

【iOS】RunLoop学习

前言

这篇博客起源于一个问题,为什么自动轮播图在滑动tableView时就停止滚动了。

基本概念

RunLoop本质是Event Loop事件循环机制,是管理线程事件、消息的对象。核心作用在于让线程有事做事、无事休眠,避免线程执行完任务直接退出,同时最大化节省CPU资源,是iOS/macOS事件响应的底层核心机制。

iOS、macOS专属的Event Loop机制叫作RunLoop,Node.js、Windows程序均有同类事件循环机制。

RunLoop核心能力有:

  • App启动后主线程自动绑定RunLoop,阻止线程执行完毕后退出销毁,保持程序持续运行。
  • 统一管理触摸点击、定时器、端口通信、网络回调、UI刷新等APP的各种事件。
  • 无事件时挂起线程休眠,有事件时主动唤醒线程工作,节省CPU资源,提供程序性能。

主线程RunLoop启动原理

启动一个iOS程序,系统会调用创建项目时自动生成的main.m文件:

objc 复制代码
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
  	// 函数内部自动创建并开启主线程RunLoop
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

其中UIApplicationMain函数内部会帮我们开启主线程的RunLoop。其内部拥有一个无限循环的do_while代码,这样程序就会一直保持执行状态,不会马上退出。

objc 复制代码
int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 处理触摸、定时器、UI刷新等所有事件
    } while (running); // 仅手动退出App才终止循环

    return 0;
}

RunLoop本质就是线程中的一个循环,其在循环中不断检测,通过input souce和timer source接收事件,然后通知线程进行处理事件。

RunLoop里分两类事件源:Source0自定义输入源、Source1端口驱动源,Timer是独立的CFRunLoopTimer,不属于Source,但调度逻辑和Source一起。这里的input souce=Source0/Source1,timer source=定时器事件。

  • input source:输入源,异步传递的事件。通常是从其他线程、App发送的消息。
  • timer source:计时器源,同步传递的事件。这些事件都在计划的时间间隔触发。

runUnitDate方法:让RunLoop限时运行,到指定时间自动停止。

对于input source,传递异步事件到对应的一个处理程序,并且触发runUnitDate方法退出RunLoop;而timer source传递事件到对应处理程序后不主动触发runUntiDate退出RunLoop逻辑。

其本质是input source对应runUnitDate方法的参数returnAfterSourceHandled。设为true,表示处理完任意一个input source事件,就立即退出RunLoop;设为false,表示仅超出时间才退出。这里设置为true,即只要成功处理一个Source0/Source1事件,RunLoop就认为单次任务完成,直接跳出循环;而Timer属于定时轮询事件,不属于单次触发就结束任务的事件,因此哪怕timer回调执行完毕,RunLoop依旧继续循环、等待下一次事件,只有超时、手动停止、Mode内无任何item时,RunLoop才会退出。

源码实现

RunLoop实际上就是一个对象,是基于CFFoundation框架的CFRunLoopRef类型封装的对象。分为Foundation上层封装、CoreFoundation底层C语言两套API,底层互通,可桥接转换。

NSRunLoop与CFRunLoopRef区别:

  • CFRunLoopRef:CoreFoundation纯C底层API,线程安全,RunLoop的底层实现。
  • NSRunLoop:Foundation OC上层封装,非线程安全,底层完全基于CFRunLoopRef实现。

RunLoop API

获取RunLoop对象的两种方式:

  • Foundation:
objc 复制代码
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
  • CoreFoundation:
objc 复制代码
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象

启动RunLoop的三种方式:

  • - (void)run;:该方法让RunLoop一直运行,除非有特定条件让它停止。
  • - (void)runUntilDate:(NSDate *)limitDate;:该方法设置了超时时间,超出时间停止或提前把所有事件都处理完提前停止。
  • - (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;:该方法RunLoop会运行一次,超出时间或第一个source被处理都会停止。

具体看一下CoreFoundation框架中两个函数的具体实现:

objc 复制代码
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

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这个RunLoop创建入口函数:

这里的源码是iOS9的版本,有些API已废弃,但本质原理类似。

objc 复制代码
//全局字典。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) {
  	// 第一次进入时,无论是getMainRunLoop还是get子线程的RunLoop,主线程的RunLoop总会被创建
  	// 如果pthread就默认获取主线程的RunLoop
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
  	// 如果全局字典还没创建就先创建,即程序刚开始运行
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
      	// 第一次进入时创建一个临时字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(
            kCFAllocatorSystemDefault,
            0,
            NULL,
            &kCFTypeDictionaryValueCallBacks
        );
      	// 根据传入的主线程获取主线程对应的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
      	// 保存主线程的RunLoop
        CFDictionarySetValue(
            dict,
            pthreadPointer(pthread_main_thread_np()),
            mainLoop
        );
				// 将dict写到__CFRunLoops
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
          	// 已经将dict的内存保存,临时字典无用,及时释放
            CFRelease(dict);
        }
      	// 用于获取主线程的RunLoop已经保存,及时释放mainRunLoop
        CFRelease(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)
        );
      	// 如果还是找不到,就把刚刚新建的RunLoop存入字典,key是线程t
        if (!loop) {
            CFDictionarySetValue(
                __CFRunLoops,
                pthreadPointer(t),
                newLoop
            );
          	// 并让loop指向刚才新创建的RunLoop
            loop = newLoop;
        }
        __CFUnlock(&loopsLock);
      	// 释放newLoop,两种情况:
				// 第一种:newLoop被存进字典中,字典持有强引用,合理正常释放
      	// 第二种:newLoop没有被放入字典中,这个newLoop就是多余的,也应该释放掉
        CFRelease(newLoop);
    }

  	// 判断传入的线程是否是当前线程
    if (pthread_equal(t, pthread_self())) {
      	// 将RunLoop存入线程本地存储,便于以后线程的直接获取
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);

      	// _CFSetTSD:是CF封装的线程本地存储设置函数
      	// p1:TSD键标识 p2:存入线程本地存储的值,作为析构迭代剩余次数计数器,即本地存储的析构函数最多会被反复调用几轮 p3:线程销毁时执行的析构回调函数
      	// 设置RunLoop生命周期清理计数器:当线程结束时,系统会调用TSD析构函数
      	// 取出RunLoop清理计数器。如果是0,说明当前线程还没有设置过RunLoop的退出清理逻辑
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
          	// 注册回调,当线程销毁时,销毁对应的RunLoop
            _CFSetTSD(
                __CFTSDKeyRunLoopCntr,
                (void *)(PTHREAD_DESTRUCTOR_ITERATIONS - 1),
                (void (*)(void *))__CFFinalizeRunLoop
            );
        }
    }

    return loop;
}

这样我们就可以得出RunLoop和线程的关系:

RunLoop与线程一一对应。App启动后,程序进入主线程,Apple帮我们在主线程启动一个RunLoop。主线程系统自动创建、自动启动,无需开发者操作;而如果是我们开辟的子线程,那么就必须我们手动开启RunLoop,否则子线程的RunLoop不会开启,这里采用了一个懒加载的形式

RunLoop对象

看一下RunLoop对象本体结构体源码:

objc 复制代码
struct __CFRunLoop {
    CFRuntimeBase _base; // CF框架所有对象的基类头部
    pthread_mutex_t _lock;  /* locked for accessing mode list */ // 互斥锁、RunLoop全局锁
    __CFPort _wakeUpPort;   // used for CFRunLoopWakeUp // 内核通信端口,RunLoop唤醒核心通道
    Boolean _unused;
  	// volatile:防止编译器优化内存读写
    volatile _per_run_data *_perRunData; // reset for runs of the run loop // 单次运行临时上下文数据
    pthread_t _pthread; // RunLoop对应的线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes; // 字符串集合,存储标记为Common的Mode Name
    CFMutableSetRef _commonModeItems; // 全局通用事件集合,存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode; // 当前正在运行、阻塞监听的Mode示例
    CFMutableSetRef _modes; // 存储当前RunLoop所有已创建的CFRunLoopModeRef实例
  	// RunLoop内部Block任务双向链表
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart; // 跨框架配对兼容指针
};

其中CommonModes记录了所有被标记了common的mode。一个mode可以通过将其ModeName添加到RunLoop的commonModes中来将自己标记为Common属性。每当RunLoop的内容发生变化时,RunLoop都会将_commonItems里的source、observer、timer同步到具有Common标记的所有Mode里。

看一下把指定Mode加入到RunLoop通用模式名单_commonModes的方法:

objc 复制代码
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);
}

_commonModes中存储的是所有被标记为common的mode的集合,这些mode具有跨RunLoop共享的特性,能够让特定的事件源、定时器和观察者在多个RunLoop中共同作用。

RunLoop相关类

在CoreFoundation中关于RunLoop有五个类:

  • CFRunLoopRef:RunLoop对象
  • CFRunLoopModeRef:RunLoop的运行模式
  • CFRunLoopSourceRef:RunLoop运行模式的输入源、事件源
  • CFRunLoopTimerRef:RunLoop运行模式的定时源
  • CFRunLoopObserverRef:RunLoop运行模式的观察者,监听RunLoop的状态变化

Ref:Reference,引用,指针包装。CoreFoundation整套API里所有XXRef都不是普通对象,是不透明结构体指针。

这几个类的关联在于:

一个RunLoop对象(CFRunLoopRef)中包含多个运行模式(CFRunLoopModeRef),每个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。

每次RunLoop启动时,只能指定其中一个运行模式,这个运行模式被称为当前运行模式(CurrentMode)。如果需要切换运行模式,只能退出当前Loop,再重新制定一个运行模式进入。这样做是为了分隔开不同组的输入源、定时源、观察者,让其互不影响

接下来具体看看这几个类:

CFRunLoopModeRef

objc 复制代码
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

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 */
};
  • Source0:无mach端口,不会被CPU自动唤醒,必须手动唤醒。包括触摸事件(例如点击屏幕、滑动手势等)和performSelectors。
  • Source1:绑定mach端口,CPU或其他进程发消息就能自动唤醒RunLoop,无需手动唤醒。
    Mach Port:是XNU(MAch微内核)最核心的IPC原语,由内核维护的结构体消息队列,是苹果全平台进程/内核通信的唯一底层通道。

系统默认注册5个Mode,即RunLoop可采用的五种工作模式有:

  • kCFRunLoopDefaultMode(Default模式):默认模式,该模式下RunLoop是自由的,可以处理各种如用户触摸、计时器、网络请求回调等的常规事件。例如:该模式下,可以安全执行耗时计算或其他需要占用大量资源的操作。
  • UITrackingRunLoopMode(Tracking模式):用户在进行一些需要快速响应(如滚动视图)的操作时,RunLoop会切换到该模式。该模式下,RunLoop会暂时停止处理其他不重要的事件以确保用户的操作能够得到迅速响应,避免卡顿。例如:当用户拖动列表时,该模式会保证滚动的流畅性以不被其他后台任务打断。
  • UIInitializationRunLoopMode(Initialization模式):该模式仅在初始化阶段使用。一旦初始化完成,RunLoop会退出这个模式。一般情况下,开发者不需要直接处理这个模式。例如:在应用程序刚启动或线程刚创建时,RunLoop会进入这个模式来执行初始化工作。
  • GSEventReceiveRunLoopMode(EventReceive模式):这是一个系统级别的内部模式,用于接收和处理系统内部事件,开发者通常不会直接与该模式交互。例如:处理硬件中断或系统通知等。
  • kCFRunLoopCommonModes(Common模式):该模式是一个占位符,用于将多个模式组合在一起。在实际应用中,开发者可以将某些事件源加入到Commom模式,这样这些事件源可以在多个模式下共享,有助于简化事件管理,避免在不同模式下重复添加事件源。例如:可以将定时器添加到Common模式,使其在Default和Tracking模式下都能被触发。

在主线程RunLoop上,以上模式会自动切换,但RunLoop启动时只能选择其中一个作为Mode作为currentMode。

这里就解决了我们最开始的问题:自动轮播图为什么在滑动tableView时停止滚动。

其本质原因是:

界面用NSTimer做自动轮播,NSTimer本质封装CFRunLoopTimer,属于RunLoop的item,只在当前活跃的Mode里才会被执行计时回调。而scheduledTimerWithTimeInterval方法创建定时器时,系统自动把Timer加入到主线程RunLoop。静止正常浏览页面时,RunLoop模式为NSDefaultRunLoopMode,Timer正常触发,轮播图自动翻页;手指滑动UIScrollView/UITableView/UICollectionView时,系统自动切换主线程RunLoop为UITrackingRunLoopMode,此时活跃Mode变为Tracking,DefaultMode里的Timer不在当前Mode列表,不会被调度,定时器暂停,轮播卡住。

这里有三个解决办法:

  1. 修改Timer运行Mode:
objc 复制代码
// 默认进DefaultMode
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer *t) {
    // 切换轮播下标等代码
}];
// 移出默认模式,加入通用模式
[[NSRunLoop mainRunLoop] removeTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

这里手动修改Mode,底层会把Timer同时挂载到Default和UITracking两个真实Mode中。这样无论RunLoop切换到哪个模式下,当前活跃Mode里都有计时器,滑动时计时器不停止计时,轮播图不停止播放。

  1. GCD dispatch_soucre_t定时器:
objc 复制代码
// 创建队列+定时器源
dispatch_queue_t queue = dispatch_queue_create("banner.timer", DISPATCH_QUEUE_SERIAL);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间间隔
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 2 * NSEC_PER_SEC, 0);
// 回调,切回主线程刷新UI
dispatch_source_set_event_handler(timer, ^{
    dispatch_async(dispatch_get_main_queue(), ^{
        // 更新轮播UI
    });
});
// 启动
dispatch_resume(timer);
// 销毁时务必取消,防止内存泄漏
// dispatch_source_cancel(timer);

GCD定时器不依赖RunLoop,基于系统CPU调度,完全不受Mode切换影响,滑动不会卡顿。并且可以放在子线程,不占用主线程资源。

  1. CADDisplayLink帧动画轮播
objc 复制代码
#import <QuartzCore/QuartzCore.h>
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(scrollBanner)];
link.frameInterval = 12;
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

CADisplayLink(QuartzCore.framework框架)是一个执行频率(fps)和屏幕刷新频率相同的定时器(可以通过preferredFramedPerSecond修改刷新频率),需要加入到RunLoop才能运行。底层同样是RunLoop Item,默认DefaultMode,同样通过添加到NSRunLoopCommonModes来使轮播图滑动界面时持续执行。

CFRunLoopSourceRef

CFRunLoopSourceRef是input sources的抽象。主要分为source0和source1两个版本。

objc 复制代码
struct __CFRunLoopSource 
    CFRuntimeBase _base;
    uint32_t _bits; // 用于标记Signaled状态,source0只有被标记为true时,才可以被唤醒
    pthread_mutex_t _lock;
    CFIndex _order;         /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;     /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};
  • source0:App内部事件,是非基于port的,只包含了一个回调函数指针,并不能触发事件。由App自己管理的UIEvent都是source0。当一个source0事件准备执行时,必须要先把它标记为signaled状态。然后使用时先调用CFRunLoopSourceSignal(source),将这个source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop。
  • source1:除了包含回调指针,也包含了一个mach-port。source1可以监听系统端口和通过CPU和其他线程通信、接收、分发系统事件,能够主动唤醒RunLoop。

CFRunLoopTimerRef

CFRunLoopTimer是基于时间的触发器,包含一个时间长度、一个回调函数指针。当其加入runloop时,runloop会注册对应的时间点。当时间点到时,runloop会被唤醒,然后执行那个回调。

CFRunLoopTimer和NSTimer是对象桥接,可以相互转换。

objc 复制代码
struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes; // 包含timer的mode集合
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;        /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    CFIndex _order;            /* immutable */
    CFRunLoopTimerCallBack _callout; // timer的回调  
    CFRunLoopTimerContext _context; // 上下文对象
};

CFRunLoopObserverRef

CFRunLoopObserverRef使观察者可以观察RunLoop的各种状态,每个observer都包含了一个回调函数指针。当RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化。

objc 复制代码
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop; // 监听的RunLoop
    CFIndex _rlCount; // 添加该Observer的RunLoop对象个数
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order; // 同时间最多只能监听一个
    CFRunLoopObserverCallBack _callout; // 监听的回调
    CFRunLoopObserverContext _context; // 用于内存管理的上下文
};

监听RunLoop的六种状态:

objc 复制代码
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7), // 即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 全部状态的集合,监听所有生命周期节点
};

RunLoop底层原理

看下底层源码CFRunLoopRunSpecific:

  1. 根据传入的modeName找到对应的CFRunLoopRef。
  2. 触发observer监听kCFRunLoopEntry即将进入。
  3. 调用__CFRunLoopRun函数。
  4. 循环结束后,触发observer监听kCFRunLoopExit即将退出。
objc 复制代码
// 返回循环终止状态码
// p1:要启动运行的RunLoop实例 p2:本次循环执行的Mode名称 p3:超时等待时长 p4:判断单次source处理后是否退出循环
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { {
    
    // 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    // 通知Observers:RunLoop即将进入loop
  	// p1:CFRunLoopRef类型,当前操作的RunLoop实例对象 p2:当前RunLoop正在运行的模式名 p3:标记RunLoop当前所处生命周期阶段
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 调用__CFRunLoopRun函数
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 通知Observers:RunLoop即将退出
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}

其中核心调用__CFRunLoopRun方法:

其内部是一个do-while循环,直到超时或手动停止,该函数才会返回;否则一直死循环。

objc 复制代码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {
        // 通知Observers:RunLoop即将触发Timer回调
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
      	 // 通知Observers:RunLoop即将触发Source0回调
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 执行RunLoop挂载的Block任务链表
        __CFRunLoopDoBlocks(rl, rlm);
      	// 处理所有标记signaled的Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        // 如果本轮处理了source0,就再次执行一遍Block
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks(rl, rlm);
        }
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        // 快速检测是否有已经就绪的source1
        if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
            // 有就直接跳过休眠,去处理消息
            goto handle_msg;
        }

        // 触发Observers:RunLoop的线程即将进入休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
      	// 标记线程状态为sleeping
        __CFRunLoopSetSleeping(rl);
	
        // 调用__CFRunLoopServiceMachPort阻塞mach_msg,线程休眠等待唤醒 
      	// 被唤醒的四个条件:timer计时到期、source1端口收到mach消息、外部调用CFRunLoopWakeUp手动唤醒、全局超时时间到
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        // 取消sleep标记
        __CFRunLoopUnsetSleeping(rl);
        // 通知Observers:结束休眠,RunLoop的线程刚从休眠中唤醒
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    // 判断唤醒来源并处理事件
    handle_msg:;
      	// 被Timer唤醒就执行__CFRunLoopDoTimers遍历所有到期CFRunLoopTimer
        if (/* 被timer唤醒 */) {
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        // 被GCD主队列唤醒就执行主队列调度任务
        } else if (/* 被gcd唤醒 */) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        // 被source1端口消息唤醒就执行__CFRunLoopDoSource1处理端口事件
        } else {  
            sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
	    }
        // 处理完唤醒事件,再执行一轮Block任务
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 四种退出场景:
      	// 处理完一次input source,参数要求立刻退出
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        // 全局设定的超出时间已到
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
        // 外部调用__CFRunLoopIsStopped(rl)强制停止
        } else if (__CFRunLoopIsStopped(rl)) {
            retVal = kCFRunLoopRunStopped;
        // 当前mode内无任何source、timer、observer,空模式
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }
        // 0 == retVal表示一切正常,那就继续下一轮完整循环
    } while (0 == retVal);

    return retVal;
}

因此,我们可以看到RunLoop的实现原理是:

当RunLoop无待处理事件时,会通过mach_msg系统调用从用户态切换至内核态,由Mach内核将线程休眠;当监听的端口收到 Mach消息后,内核唤醒线程,系统调用返回、切换回用户态,RunLoop处理对应事件。

RunLoop实际应用

  1. AutoreleasePool

主线程创建时,会自动生成RunLoop,并提前注册两个全局observer管理自动释放池。子线程无自动创建,需手动@autoreleasepool。

  • 第一个observer:监听kCFRunLoopEntry,提供最高优先级,保证释放池的创建发生在其他回调之前。RunLoop刚进入循环时,调用_objc_autoreleasePoolPush()新建释放池,保证本轮所有自动释放对象都能被管理。
  • 第二个observer:监听kCFRunLoopBeforeWaiting、kCFRunLoopExit。
    • BeforeWaiting:即将休眠,pop销毁旧池、push新建新池。休眠长时间无操作,及时回收临时对象,避免内存暴涨。
    • Exit:循环结束,pop销毁最后一个池子,回收本轮全部自动释放对象。

即进入RunLoop的时候创建一个池子,休眠前销毁重建,退出RunLoop的时候彻底销毁该池子。

  1. tableView延迟加载图片

由于图片渲染到屏幕需要消耗较多资源,为了提高用户的体验,当用户滚动tableView时,只在后台下载图片,但不显示,当用户停下来时才显现。

限定setImage只能在NSDefaultRunLoopMode模式下使用,而滚动tableView时,程序运行在tracking模式下面,所以setImage方法不会执行。

objc 复制代码
[self.imageView performSelector:@selector(setImage:) withObject:image afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
  1. 常驻子线程

频繁创建销毁线程开销大,创建一条长期存活子线程来随时接收异步任务。其实现核心在于Mode内如果没有任何source、timer、observer时,RunLoop会直接退出,必须添加占位NSPort来保证循环不终止。

objc 复制代码
// 创建线程
- (void)createThread {
    self.workThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntry) object:nil];
    [self.workThread start];
}

// 线程入口
- (void)threadEntry {
    @autoreleasepool {
        NSLog(@"线程启动");
        NSPort *port = [NSPort port];
        [[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
        while (!self.stop) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        // [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        NSLog(@"线程退出");
    }
}

// 派发任务
- (void)sendTask {
    [self performSelector:@selector(doTask) onThread:self.workThread withObject:nil waitUntilDone:NO];
}

- (void)doTask {
    NSLog(@"执行任务 %@",[NSThread currentThread]);
}

// 停止线程
- (void)stopThread {
    self.stop = YES;
    [self performSelector:@selector(innerStop) onThread:self.workThread withObject:nil waitUntilDone:YES];
    self.workThread = nil;
}

- (void)innerStop {
    CFRunLoopStop(CFRunLoopGetCurrent());
}

具体看下效果对比:

  • 常驻子线程
  • 非常驻子线程

我们可以看出:如果是常驻子线程,点击执行任务,无论隔多久都会执行,说明线程一直活着,RunLoop一直没退出,除非手动销毁线程;而非常驻子线程启动后线程立即结束。

可是非常驻子线程线程销毁后,点击执行任务无效,而点击销毁线程报错是什么原因?

对比一下这两行代码:

区别在于waitUntilDone这个参数:

  • 点击执行任务:设置为NO为异步投递,系统不会等结果,因此无反应,不执行、不崩溃、没日志。
  • 点击销毁线程:设置为YES为同步投递,把任务发给目标线程,但发现目标线程已经死亡,于是直接抛出异常。
  1. 网络请求回调调度

对比一下NSURLConnection和NSURLSession:

  • NSURLConnection:回调分发依赖调用线程的RunLoop,子线程需手动开启RunLoop,受Mode切换影响,滚动列表会延迟回调。
  • NSURLSession:底层基于GCD调度,回调分发脱离RunLoop,子线程无需启动RunLoop,不受Mode影响,无滑动阻塞回调问题。

NSURLConnection在iOS9正式废弃,虽然代码仍可运行,但新项目一律不用。其底层依赖调用线程的RunLoop存在子线程无回调、滑动阻塞回调等大量问题;而NSURLSession基于GCD调度,不再强依赖RunLoop,同时支持后台下载等新特性,是当前标准网络API,所有第三方网络框架都基于它封装。

总结

以上就是我学习RunLoop的总结,后续我将继续补充完善我的博客。

相关推荐
AI棒棒牛2 小时前
第 03 讲《监督学习:数据、标签、Loss与训练循环》
人工智能·学习·yolo·目标检测·yolo26
你是个什么橙2 小时前
Python入门学习2:Python 基础语法全解析——从代码结构到输入输出
开发语言·python·学习
宝贝儿好2 小时前
【LLM】第二章:HuggingFace入门学习
人工智能·深度学习·神经网络·学习·算法·自然语言处理
秋波。未央2 小时前
Java Agent 开发 · Day 1 学习笔记(含作业完整标准答案)
java·笔记·学习
beethobe2 小时前
PythonQt 学习之旅(一):从零构建 C++ 与 Python 的桥梁
c++·python·学习
如果你想拥有什么先让自己配得上拥有2 小时前
创业全周期证券学习法评价与系统观分析
学习
踏着七彩祥云的小丑2 小时前
嵌入式测试学习第 37 天:异常场景测试:断电、拔插、干扰、非法指令
单片机·嵌入式硬件·学习
黑科技iOS上架3 小时前
iOS应用周末提交什么情况算卡审
经验分享·ios
Kobebryant-Manba3 小时前
学习门控循环单元gru
深度学习·学习·gru