写点什么

iOS——解密 RunLoop 原理

作者:iOSer
  • 2022 年 1 月 17 日
  • 本文字数:12046 字

    阅读完需:约 40 分钟

本文篇幅比较长,创作的目的为了自己日后温习知识所用,希望这篇文章能对你有所帮助。如发现任何有误之处,恳请留言纠正,谢谢。

前言

RunLoop 作为 iOS 中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多数开发者很少直接使用 RunLoop,但是理解 RunLoop 可以帮助开发者更好的利用多线程编程模型,同时也可以帮助开发者解答日常开发中的一些疑惑。


这篇文章将从 CFRunLoop 的源码入手,介绍 RunLoop 的概念以及底层实现原理。之后会介绍一下在 iOS 中,苹果是如何利用 RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的

一、什么是 RunLoop?

可以理解为字面意思:Run 表示运行,Loop 表示循环。结合在一起就是运行的循环的意思。NSRunloop 是 CFRunloop 的封装,CFRunloop 是一套 C 接口。


RunLoop 这个对象,在 iOS 里由 CFRunLoop 实现。简单来说,RunLoop 是用来监听输入源,进行调度处理的。这里的输入源可以是输入设备、网络、周期性或者延迟时间、异步回调。RunLoop 会接收两种类型的输入源:一种是来自另一个线程或者来自不同应用的异步消息;另一种是来自预订时间或者重复间隔的同步事件。

二、源码解析 runloop 流程

苹果runloop源码

1、入口方法 CFRunLoopRun

void CFRunLoopRun(void) {    /* DOES CALLOUT */    int32_t result;    do {        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);        CHECK_FOR_FORK();    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);}
复制代码


通过代码可以看出来,如果 kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result,则一直会循环执行 CFRunLoopRunSpecific 函数

2、循环执行的函数

CFRunLoopRun -> CFRunLoopRunSpecific -> CFRunLoopRun


  • 2.1 CFRunLoopRunSpecific 源码


SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */    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;
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result;}
复制代码


主要逻辑代码:


    //通知 observers     if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);    //进入 loop    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
复制代码


通知 observers:RunLoop 要开始进入 loop 了。紧接着就进入 loop。


  • 2.1 CFRunLoopRun 源码(由于源码量比较大,这里就不全部贴出来了,只贴出来核心步骤)


第一步


开启一个 do while 来保活线程。通知 Observers:RunLoop 会触发 Timer 回调、Source0 回调,接着执行加入的 block


// 通知 Observers RunLoop 会触发 Timer 回调if (rlm->_observerMask & kCFRunLoopBeforeTimers)     __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers RunLoop 会触发 Source0 回调if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 执行 block__CFRunLoopDoBlocks(rl, rlm);
复制代码


接下来,触发 Source0 回调,如果有 Source1 是 ready 状态的话,就会跳转到 handle_msg 去处理消息。


if (MACH_PORT_NULL != dispatchPort ) {    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)    if (hasMsg) goto handle_msg;}
复制代码


source0 和 source1 的区别,后面会介绍!!


第二步回调触发后,通知 Observers:RunLoop 的线程将进入休眠(sleep)状态。


Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);if (!poll && (currentMode->_observerMask & kCFRunLoopBeforeWaiting)) {    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);}
复制代码


第三步进入休眠后,会等待 mach_port 的消息,以再次唤醒。只有在下面四个事件出现时才会被再次唤醒:


  • 基于 port 的 Source 事件;

  • Timer 时间到;

  • RunLoop 超时;

  • 被调用者唤醒。


do {    __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {        // 基于 port 的 Source 事件、调用者唤醒        if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {            break;        }        // Timer 时间到、RunLoop 超时        if (currentMode->_timerFired) {            break;        }} while (1);
复制代码


第四步


唤醒时通知 Observer:RunLoop 的线程刚刚被唤醒了。


if (!poll && (currentMode->_observerMask & kCFRunLoopAfterWaiting))    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
复制代码


第五步 RunLoop 被唤醒后就要开始处理消息了:


  • 如果是 Timer 时间到的话,就触发 Timer 的回调;

  • 如果是 dispatch 的话,就执行 block;

  • 如果是 source1 事件的话,就处理这个事件。


消息执行完后,就执行加到 loop 里的 block。


handle_msg:// 如果 Timer 时间到,就触发 Timer 回调if (msg-is-timer) {    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())} // 如果 dispatch 就执行 blockelse if (msg_is_dispatch) {    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);} 
// Source1 事件的话,就处理这个事件else { CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); }}
复制代码


第六步根据当前 RunLoop 的状态来判断是否需要走下一个 loop。当被外部强制停止或 loop 超时时,就不继续下一个 loop 了,否则继续走下一个 loop 。


if (sourceHandledThisLoop && stopAfterHandle) {     // 事件已处理完    retVal = kCFRunLoopRunHandledSource;} else if (timeout) {    // 超时    retVal = kCFRunLoopRunTimedOut;} else if (__CFRunLoopIsStopped(runloop)) {    // 外部调用者强制停止    retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {    // mode 为空,RunLoop 结束    retVal = kCFRunLoopRunFinished;}
复制代码


整个 RunLoop 过程,我们可以总结为如下所示的一张图片。



总结将整个流程总结成伪代码如下:


int32_t __CFRunLoopRun(){    // 通知即将进入runloop    __CFRunLoopDoObservers(KCFRunLoopEntry);
do { // 通知将要处理timer和source __CFRunLoopDoObservers(kCFRunLoopBeforeTimers); __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 处理非延迟的主线程调用 __CFRunLoopDoBlocks(); // 处理Source0事件 __CFRunLoopDoSource0();
// 处理Source0事件 if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(); }
/// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。 if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(); if (hasMsg) goto handle_msg; }
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。 if (!sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); }
// GCD dispatch main queue CheckIfExistMessagesInMainDispatchQueue();
// 即将进入休眠 __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
// 等待内核mach_msg事件 mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
// 等待。。。
// 从等待中醒来 __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 处理因timer的唤醒 if (wakeUpPort == timerPort) __CFRunLoopDoTimers();
// 处理异步方法唤醒,如dispatch_async else if (wakeUpPort == mainDispatchQueuePort) __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// 处理Source1 else __CFRunLoopDoSource1();
// 再次确保是否有同步的方法需要调用 __CFRunLoopDoBlocks();
} while (!stop && !timeout);
// 通知即将退出runloop __CFRunLoopDoObservers(CFRunLoopExit);}
复制代码


下图描述了 Runloop 运行流程



注意的是尽管 CFRunLoopPerformBlock 在上图中作为唤醒机制有所体现,但事实上执行 CFRunLoopPerformBlock 只是入队,下次 RunLoop 运行才会执行,而如果需要立即执行则必须调用 CFRunLoopWakeUp。

如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

三、Runloop Mode

1、一"码"当先


    struct __CFRunLoop {        CFRuntimeBase _base;        pthread_mutex_t _lock;          /* locked for accessing mode list */        __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp         Boolean _unused;        volatile _per_run_data *_perRunData;              // reset for runs of the run loop        pthread_t _pthread;    //线程        uint32_t _winthread;        CFMutableSetRef _commonModes;   // commonModes下的两个mode(kCFRunloopDefaultMode和UITrackingMode)        CFMutableSetRef _commonModeItems;  // 在commonModes状态下运行的对象(例如Timer)        CFRunLoopModeRef _currentMode;  //在当前loop下运行的mode        CFMutableSetRef _modes;  // 运行的所有模式(CFRunloopModeRef类)        struct _block_item *_blocks_head;        struct _block_item *_blocks_tail;        CFAbsoluteTime _runTime;        CFAbsoluteTime _sleepTime;        CFTypeRef _counterpart;    };
struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; /* must have the run loop locked before locking this */ CFStringRef _name; Boolean _stopped; char _padding[3]; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; CFMutableDictionaryRef _portToV1SourceMap; __CFPortSet _portSet; 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 */ };
复制代码


系统默认提供的 Run Loop Modes 有 kCFRunLoopDefaultMode(NSDefaultRunLoopMode)UITrackingRunLoopMode,需要切换到对应的 Mode 时只需要传入对应的名称即可。前者是系统默认的 Runloop Mode,例如进入 iOS 程序默认不做任何操作就处于这种 Mode 中,此时滑动 UIScrollView,主线程就切换 Runloop 到到 UITrackingRunLoopMode,不再接受其他事件操作(除非你将其他 Source/Timer 设置到 UITrackingRunLoopMode 下)。


但是对于开发者而言经常用到的 Mode 还有一个 kCFRunLoopCommonModes(NSRunLoopCommonModes),其实这个并不是某种具体的 Mode,而是一种模式组合,在 iOS 系统中默认包含了


NSDefaultRunLoopMode 和 UITrackingRunLoopMode(注意:并不是说 Runloop 会运行在 kCFRunLoopCommonModes 这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。当然你也可以通过调用 CFRunLoopAddCommonMode()方法将自定义 Mode 放到 kCFRunLoopCommonModes 组合)。


CFRunLoopRef 和 CFRunloopMode、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef 关系如下图:




2、RunLoop Source 苹果文档将 RunLoop 能够处理的事件分为 Input sources 和 timer 事件。下面这张图取自苹果官网:



根据 CF 的源码,Input source 在 RunLoop 中被分类成 source0 和 source1 两大类。source0 和 source1 均有结构体__CFRunLoopSource 表示:


struct __CFRunLoopSource {    CFRuntimeBase _base;    uint32_t _bits;    pthread_mutex_t _lock;    CFIndex _order;         /* 优先级,越小,优先级越高。可以是负数。immutable */    CFMutableBagRef _runLoops;    union {  // 联合,用于保存source的信息,同时可以区分source是0还是1类型        CFRunLoopSourceContext version0;    /* immutable, except invalidation */        CFRunLoopSourceContext1 version1;   /* immutable, except invalidation */    } _context;};
typedef struct { CFIndex version; // 类型:source0 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); void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); void (*perform)(void *info); // call out } CFRunLoopSourceContext;
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_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); // call out#endif} CFRunLoopSourceContext1;
复制代码


source0 和 source1 由联合_context 来做代码区分:


  • 相同

  • 均是__CFRunLoopSource 类型,这就像一个协议,我们甚至可以自己拓展__CFRunLoopSource,定义自己的 source。

  • 均是需要被 Signaled 后,才能够被处理。

  • 处理时,均是调用__CFRunLoopSource._context.version(0?1).perform,其实这就是调用一个函数指针。

  • 不同

  • source0 需要手动 signaled,source1 系统会自动 signaled

  • source0 需要手动唤醒 RunLoop,才能够被处理: CFRunLoopWakeUp(CFRunLoopRef rl)。而 source1 会自动唤醒(通过 mach port)RunLoop 来处理。

  • 总结:

  • Source1 :基于 mach_Port 的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的 RunLoop(iOS 里进程间通信开发过程中我们一般不主动使用)。mach_port 大家就理解成进程间相互发送消息的一种机制就好, 比如屏幕点击, 网络数据的传输都会触发 sourse1。

  • Source0 :非基于 Port 的 处理事件,什么叫非基于 Port 的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。一般是 APP 内部的事件, 比如 hitTest:withEvent 的处理, performSelectors 的事件。


3、RunLoop Timer


我们经常使用的 timer 有几种?


  • NSTimer & PerformSelector:afterDelay:(由 RunLoop 处理,内部结构为 CFRunLoopTimerRef)

  • GCD Timer(由 GCD 自己实现,不通过 RunLoop)

  • CADisplayLink(通过向 RunLoop 投递 source1 实现回调)


NSObject perform 系列函数中的 dealy 类型, 其实也是一种 Timer 事件,可能不那么明显:


- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
复制代码


这种 Perform delay 的函数底层的实现是和 NSTimer 一样的,根据苹果官方文档所述:


This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.If you want the message to be dequeued when the run loop is in a mode other than the default mode, use the performSelector:withObject:afterDelay:inModes: method instead.


翻译:


此方法设置一个计时器,以便在当前线程的 run loop 上执行 aSelector 消息。timer 配置为在默认模式(NSDefaultRunLoopMode)下运行。当 timer 触发时,线程尝试从运行循环中退出消息队列并执行 selector。如果 run loop 正在运行且处于 default mode,则会成功;否则,计时器将等待运行循环处于 default mode。如果希望在 run loop 处于 default mode 以外的模式时将消息退出队列,请改用 performSelector:withObject:afterDelay:inModes:方法。


NSTimer & PerformSelector:afterDelay:NSTimer 在 CF 源码中的结构是这样的:


struct __CFRunLoopTimer {    CFRuntimeBase _base;    uint16_t _bits;    pthread_mutex_t _lock;    CFRunLoopRef _runLoop;    CFMutableSetRef _rlModes;    CFAbsoluteTime _nextFireDate;    CFTimeInterval _interval;       /* immutable */    CFTimeInterval _tolerance;          /* mutable */    uint64_t _fireTSR;          /* 触发时间,TSR units */    CFIndex _order;         /* immutable */    CFRunLoopTimerCallBack _callout;    /* immutable */ // timer 回调    CFRunLoopTimerContext _context; /* immutable, except invalidation */};
复制代码


Timer 的触发流程大致是这样的:


  • 用户添加 timer 到 runloop 的某个或几个 mode 下

  • 根据 timer 是否设置了 tolerance,如果没有设置,则调用底层 xnu 内核的 mk_timer 注册一个 mach-port 事件,如果设置了 tolerance,则注册一个 GCD timer

  • 当由 XNU 内核或 GCD 管理的 timer 的 fire time 到了,通过对应的 mach port 唤醒 RunLoop(mk_timer 对应 rlm 的_timerPort, GCD timer 对应 GCD queue port)

  • RunLoop 执行__CFRunLoopDoTimers,其中会调用__CFRunLoopDoTimer, DoTimer 方法里面会根据当前 mach time 和 Timer 的 fireTSR 属性,判断 fireTSR 是否< 当前的 mach time,如果小于,则触发 timer,同时更新下一次 fire 时间 fireTSR。


关于 Timer 的计时,是通过内核的 mach time 或 GCD time 来实现的。


在 RunLoop 中,NSTimer 在激活时,会将休眠中的 RunLoop 通过_timerPort 唤醒,(如果是通过 GCD 实现的 NSTimer,则会通过另一个 CGD queue 专用 mach port)。


如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

4、RunLoop Observer


Observer 在 CF 中的结构如下:


struct __CFRunLoopObserver {    CFRuntimeBase _base;    pthread_mutex_t _lock;    CFRunLoopRef _runLoop;    CFIndex _rlCount;    CFOptionFlags _activities;      /*所监听的事件,通过位异或,可以监听多种事件 immutable */    CFIndex _order;         /* 优先级 immutable */    CFRunLoopObserverCallBack _callout; /* observer 回调 immutable */    CFRunLoopObserverContext _context;  /* immutable, except invalidation */};
复制代码


Observer 的作用是可以让外部监听 RunLoop 的运行状态,从而根据不同的时机,做一些操作。系统会在 APP 启动时,向 main RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。


  • 第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用_objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

  • 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出 Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。


Observer 可以监听的事件在 CF 中以位异或表示:


/* Run Loop Observer Activities */    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    };
复制代码


  • kCFRunLoopEntry、kCFRunLoopExit 在每次 RunLoop 循环中仅调用一次,用于表示即将进入循环和退出循环。

  • kCFRunLoopBeforeTimers、kCFRunLoopBeforeSources、kCFRunLoopBeforeWaiting、kCFRunLoopAfterWaiting 这些通知会在循环内部发出,可能会调用多次。


相对来说 CFRunloopObserverRef 理解起来并不复杂,它相当于消息循环中的一个监听器,随时通知外部当前 RunLoop 的运行状态(它包含一个函数指针_callout_将当前状态及时告诉观察者)。


5、Call out 在开发过程中几乎所有的操作都是通过 Call out 进行回调的(无论是 Observer 的状态通知还是 Timer、Source 的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听 Observer 也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数):


    static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
复制代码


例如在控制器的 touchBegin 中打入断点查看堆栈(由于 UIEvent 是 Source0,所以可以看到一个 Source0 的 Call out 函数 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION 调用):



四、Runloop 和线程的关系

RunLoop 和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以,我们就有了 RunLoop。


iOS 开发中能遇到两个线程对象: pthread_t 和 NSThread,pthread_t 和 NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np()或 [NSThread mainThread]来获取主线程;也可以通过 pthread_self()或[NSThread currentThread]来获取当前线程。CFRunLoop 是基于 pthread 来管理的。


线程与 RunLoop 是一一对应的关系(对应关系保存在一个全局的 Dictionary 里),线程创建之后是没有 RunLoop 的(主线程除外),RunLoop 的创建是发生在第一次获取时,销毁则是在线程结束的时候。只能在当前线程中操作当前线程的 RunLoop,而不能去操作其他线程的 RunLoop。


一"码"当先


/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRefstatic CFMutableDictionaryRef loopsDic;/// 访问 loopsDic 时的锁static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock);
if (!loopsDic) { // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。 loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); }
/// 直接从 Dictionary 里获取。 CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) { /// 取不到时,创建一个 loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。 _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); }
OSSpinLockUnLock(&loopsLock); return loop;}
CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np());}
CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self());}
复制代码


苹果开发的接口中并没有直接创建 Runloop 的接口,如果需要使用 Runloop 通常 CFRunLoopGetMain()和 CFRunLoopGetCurrent()两个方法来获取(通过上面的源代码也可以看到,核心逻辑在_CFRunLoopGet_当中),通过代码并不难发现其实只有当我们使用线程的方法主动 get Runloop 时才会在第一次创建该线程的 Runloop,同时将它保存在全局的 Dictionary 中(线程和 Runloop 二者一一对应),默认情况下线程并不会创建 Runloop(主线程的 Runloop 比较特殊,任何线程创建之前都会保证主线程已经存在 Runloop),同时在线程结束的时候也会销毁对应的 Runloop。


iOS 开发过程中对于开发者而言更多的使用的是 NSRunloop,它默认提供了三个常用的 run 方法:


- (void)run; - (void)runUntilDate:(NSDate *)limitDate;- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
复制代码


  • 1.使用第一种方式 runLoop 会一直运行下去,在此期间会处理来自输入源的数据,并且会在 NSDefaultRunLoopMode 模式下重复调用 - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate 方法

  • 2.使用第二种启动方式,可以设置超时时间,在超时时间到达之前,runLoop 会一直运行,在此期间 runLoop 会处理来自输入源的数据,并且也会在 NSDefaultRunLoopMode 模式下重复调用 - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;方法

  • 3.使用第三种方法 runLoop 会运行一次,超时时间到达或者一个输入源被处理,则 runLoop 就会自动退出


至此 runloop 的底层实现原理已经大致做了介绍,之后会更新 runloop 的应用篇,详细介绍 iOS 中苹果对 runloop 的应用及一些知名三方 SDK 对 runloop 的实战应用!!


  • 面试基础

    iOS 面试基础知识 (一)

    https://github.com/iOS-Mayday/heji

    iOS 面试基础知识 (二)

    https://github.com/iOS-Mayday/heji

    iOS 面试基础知识 (三)

    https://github.com/iOS-Mayday/heji

    iOS 面试基础知识 (四)

    https://github.com/iOS-Mayday/heji

    iOS 面试基础知识 (五)

    https://github.com/iOS-Mayday/heji

用户头像

iOSer

关注

微信搜索添加微信 mayday1739 进微信群哦 2020.09.12 加入

更多大厂面试资料进企鹅群931542608

评论

发布
暂无评论
iOS——解密RunLoop原理