重学OC第二十六篇:RunLoop

文章目录

  • 前言
  • 一、RunLoop解析
    • 1.1 Modes
    • 1.2 Sources
      • 1.2.1 Input Sources
      • 1.2.2 Timer Sources
    • 1.3 Observers
    • 1.4 事件执行顺序
    • 1.5 何时使用RunLoop
  • 二、CFRunLoop源码
    • 2.1 RunLoop对应的CF类
      • 2.1.1 CFRunLoopRef
      • 2.1.2 CFRunLoopModeRef
      • 2.1.3 CFRunLoopSourceRef
      • 2.1.4 CFRunLoopTimerRef
      • 2.1.5 CFRunLoopObserverRef
    • 2.2 RunLoop事件执行
  • 总结

前言

本篇主要是对官方文档和CFRunLoop源码的学习。

一、RunLoop解析

RunLoop是线程进入的一个事件处理循环,用于响应传入事件并运行事件处理程序。
RunLoop的目的是在有工作要做时让线程忙,而在没有工作时让线程进入睡眠状态。
每个线程都有一个关联的RunLoop对象,但只有辅助线程需要显式地运行其运行循环。在应用程序启动过程中应用程序框架会自动在主线程上设置并运行运行循环。

RunLoop接收两种类型的Source:

  • Input Sources,传递异步事件。将异步事件传递给相应的处理程序,并导致runUntilDate:方法(在线程的关联NSRunLoop对象上调用)退出
  • Timer Sources,传递同步事件。将事件传递到其处理程序例程,但不会导致RunLoop退出。
    重学OC第二十六篇:RunLoop_第1张图片
    除了处理输入的source,RunLoop还会生成相关行为的通知。

1.1 Modes

RunLoop Mode是要监视的Input source和Timer的集合,以及要通知的RunLoop Observer的集合。
每次运行RunLoop时,都要(显式或隐式)指定运行的特定“模式”。
在RunLoop的整个过程中,仅监视与该模式关联的源,并允许其传递事件。 (类似地,仅将与该模式关联的观察者通知RunLoop的进度。)与其他模式关联的源将保留任何新事件,直到随后以适当的模式通过循环。
必须确保将一个或多个Input sources、Timer或RunLoop Observer添加到创建的任何模式中,如果RunLoop没有任何要监视的源,则当你尝试运行它时,它将立即退出。
重学OC第二十六篇:RunLoop_第2张图片
模式根据事件的来源而不是事件的类型进行区分。

1.2 Sources

RunLoop接收两种类型的Source:

  • Input Sources,传递异步事件
  • Timer Sources,传递同步事件

1.2.1 Input Sources

输入事件源有两种:基于Port的和自定义的。基于端口的输入源监视应用程序的Mach端口,自定义输入源监视事件的自定义源。两种信号源之间的唯一区别是信号的发送方式。基于端口的源由内核自动发出信号,而自定义源必须从另一个线程手动发出信号。

  • Port-Based Sources
    Cocoa和Core Foundation提供了内置支持,用于使用与端口相关的对象和功能创建基于端口的输入源。NSPort、CFMachPortRef、CFMessagePortRef、 CFSocketRef等。
  • Custom Input Sources
    若要创建自定义输入源,必须在Core Foundation中使用与CFRunLoopSourceRef类型关联的功能。可以使用多个回调函数配置自定义输入源。Core Foundation在不同的位置调用这些函数来配置源,处理任何传入事件,从RunLoop中删除源时拆除源。
    除了定义事件到达时自定义源的行为外,还必须定义事件传递机制。源代码的这一部分在单独的线程上运行,负责为输入源提供其数据,并在准备好处理数据时向其发出信号。
    • Cocoa Perform Selector Sources
      Cocoa定义的一个自定义输入源,该源可在任何线程上执行选择器。与基于端口的源类似,执行选择器请求在目标线程上序列化,从而减轻了在一个线程上运行多种方法时可能发生的许多同步问题。与基于端口的源不同,执行选择器源在执行选择器后将其自身从运行循环中删除。
      在另一个线程上执行选择器时,目标线程必须具有活动的运行循环。每次循环时,运行循环都会处理所有排队的执行选择器调用,而不是在每次循环迭代时都处理一个。这些perform方法实际上并不创建新的线程来执行选择器。

1.2.2 Timer Sources

计时器不是实时机制,它与RunLoop的特定模式相关联。如果计时器不在RunLoop当前正在监视的模式下,则只有在以计时器支持的一种模式运行RunLoop后,计时器才会触发。同样,如果RunLoop在执行处理程序例程的中间触发计时器,则计时器将等到下一次通过RunLoop调用其处理例程。如果RunLoop根本没有运行,则计时器永远不会触发。
可以将计时器配置为仅一次或重复生成事件。重复计时器会根据计划的触发时间(而不是实际的触发时间)自动重新计划自身。例如,如果计划将计时器在特定时间触发,然后每5秒触发一次,则即使实际触发时间被延迟,计划的触发时间也将始终落在原始的5秒时间间隔上。如果触发时间延迟得太多,以至于错过了一个或多个计划的触发时间,则计时器将在错过的时间段内仅触发一次。在错过了一段时间后触发之后,计时器将重新安排为下一个计划的触发时间。

1.3 Observers

Observers在RunLoop本身执行期间的特定位置触发。可以将Observer与RunLoop中的以下事件相关联:

  • 运行循环的进入
  • 当运行循环将要处理计时器
  • 当运行循环将要处理输入源
  • 当运行循环即将进入睡眠状态时
  • 当运行循环唤醒时,但在处理该事件之前将其唤醒
  • 运行循环的退出

1.4 事件执行顺序

每次运行它时,线程的RunLoop都会处理未决事件,并为所有关心的观察者生成通知。它的执行顺序如下所示:

  1. 通知Observers已进入RunLoop。
  2. 通知Observers任何准备就绪的Timer即将触发。
  3. 通知Observers任何不基于端口的Input sources都将被触发。
  4. 触发所有准备触发的非基于端口的输入源。
  5. 如果基于端口的输入源已准备好并等待启动,请立即处理事件。转到步骤9。
  6. 通知观察者线程即将进入睡眠状态
  7. 使线程进入睡眠状态,直到发生以下事件之一:
    • 事件到达基于端口的输入源
    • 计时器触发
    • 为RunLoop设置的超时值到期
    • RunLoop被显式唤醒
  8. 通知观察者线程刚刚醒来
  9. 处理未决事件
    • 如果触发了用户定义的计时器,请处理计时器事件并重新启动循环。转到步骤2
    • 如果触发了输入源,请传递事件
    • 如果运行循环已显式唤醒,但尚未超时,请重新启动循环。转到步骤2
  10. 通知Observers已退出RunLoop。

1.5 何时使用RunLoop

主线程会自动运行,对于辅助线程,需要确定是否需要RunLoop,如果需要,请自行配置并启动它。运行循环用于需要与线程进行更多交互的情况,比如执行以下任一操作,则需要启动运行循环:

  • 使用端口或自定义输入源与其他线程进行通信。
  • 在线程上使用计时器
  • 在Cocoa应用程序中使用任何performSelector…方法
  • 保持线程执行定期任务

二、CFRunLoop源码

2.1 RunLoop对应的CF类

在 CoreFoundation 里面关于 RunLoop 有5个类: CFRunLoopRef、CFRunLoopModeRef(没对外暴露)、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef。
它们的关系如下:
重学OC第二十六篇:RunLoop_第3张图片
一个 RunLoop 包含若干个 Mode,每个 Mode 包含若干个Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

2.1.1 CFRunLoopRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
//__CFRunLoop部分代码
struct __CFRunLoop {
       
    ......
    _CFThreadRef _pthread;   //对应的线程
    CFMutableSetRef _commonModes;   // common mode中包含的模式
    CFMutableSetRef _commonModeItems; //加入到commonMode中的source、timer、observer
    CFRunLoopModeRef _currentMode; //当前运行的模式
    CFMutableSetRef _modes;  //RunLoop中的模式
 	......
};

通过CFRunLoopCopyCurrentMode()和CFRunLoopCopyAllModes()函数可查看RunLoop的当前模式和modes中的所有模式。

2.1.2 CFRunLoopModeRef

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
//__CFRunLoopMode部分代码
struct __CFRunLoopMode {
     
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ......
};

2.1.3 CFRunLoopSourceRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;

struct __CFRunLoopSource {
     
    CFRuntimeBase _base;
    _CFRecursiveMutex _lock;
    CFIndex _order;			/* immutable */
    CFMutableBagRef _runLoops;
    union {
     
	CFRunLoopSourceContext version0;	/* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;	/* immutable, except invalidation */
    } _context;
    _Atomic(Boolean) _signaled;
};

typedef struct {
     
    ......
    void	(*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void	(*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void	(*perform)(void *info);
} CFRunLoopSourceContext;

typedef struct {
     
   	......
    mach_port_t	(*getPort)(void *info);
    void *	(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
} CFRunLoopSourceContext1;

CFRunLoopSourceRef 是Input Sources事件产生的地方。Source有两个版本:Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息,这种 Source 能主动唤醒 RunLoop 的线程

2.1.4 CFRunLoopTimerRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;

struct __CFRunLoopTimer {
     
    CFRuntimeBase _base;
    uint16_t _bits;
    _CFRecursiveMutex _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    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 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

2.1.5 CFRunLoopObserverRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;

struct __CFRunLoopObserver {
     
    CFRuntimeBase _base;
    _CFRecursiveMutex _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;			/* immutable */
    CFRunLoopObserverCallBack _callout;	/* immutable */
    CFRunLoopObserverContext _context;	/* immutable, except invalidation */
};

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
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
};

2.2 RunLoop事件执行

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

主要是CFRunLoopRunSpecific函数

SInt32 CFRunLoopRunSpecific(...) {
        
	...
	//1、进入Runloop
	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	//10、从Runloop退出
	if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

//下面代码精简了,仅供参考
static int32_t __CFRunLoopRun(...) {
     
	do {
     
		...
		//2、即将触发已就绪的Timers
		if (rlm->_observerMask & kCFRunLoopBeforeTimers) {
     
	    	__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
		}
		//3、即将触发Source0(非基于port)
		if (rlm->_observerMask & kCFRunLoopBeforeSources) {
     
	    	__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
		}	
		//执行加入的block
		__CFRunLoopDoBlocks(rl, rlm);
		//4、处理Source0事件
		Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
	    if (sourceHandledThisLoop) {
     
	       __CFRunLoopDoBlocks(rl, rlm);
	    }
	    Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
	    if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
     
	    	msg = (mach_msg_header_t *)msg_buffer;
	    	//5、如果基于端口的输入源已准备好并等待启动,请立即处理事件。转到步骤9
	    	if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL, rl, rlm)) {
     
	            goto handle_msg;
	        }
	    }
	    //6、通知观察者线程即将进入睡眠状态。
	    if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
	    __CFRunLoopSetSleeping(rl);
		//7、使线程进入睡眠状态,直到发生以下事件之一:
		//-->事件到达基于端口的输入源
		//-->计时器触发
		//-->为运行循环设置的超时值到期
		//-->运行循环被明确唤醒
	    __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy, rl, rlm);
	    //8、通知观察者线程刚刚醒来
	    if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
	    //9、处理未决事件
	    handle_msg:;
	    if (MACH_PORT_NULL == livePort) {
     
	    	CFRUNLOOP_WAKEUP_FOR_NOTHING();  //没事情需要处理
	    } else if (livePort == rl->_wakeUpPort) {
     
	    	CFRUNLOOP_WAKEUP_FOR_WAKEUP();  //
	    } else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
     
	    	//9.1 如果触发了用户定义的计时器,请处理计时器事件
	    	CFRUNLOOP_WAKEUP_FOR_TIMER();
	    	__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
		} else if (livePort == dispatchPort) {
      
			//9.2 处理异步方法唤醒,如dispatch_async
			CFRUNLOOP_WAKEUP_FOR_DISPATCH();
			__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
		} else {
     
			//9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
			CFRUNLOOP_WAKEUP_FOR_SOURCE();
			CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
			sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
			(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
		}
		//执行加入到Runloop的block
		__CFRunLoopDoBlocks(rl, rlm);
		
		if (sourceHandledThisLoop && stopAfterHandle) {
     
			//处理完事件就返回
		    retVal = kCFRunLoopRunHandledSource;
	    } else if (timeout_context->termTSR < mach_absolute_time()) {
     
	    	//超出传入参数标记的超时时间
	        retVal = kCFRunLoopRunTimedOut;
		} else if (__CFRunLoopIsStopped(rl)) {
     
			//被外部调用者强制停止了
	        __CFRunLoopUnsetStopped(rl);
		    retVal = kCFRunLoopRunStopped;
		} else if (rlm->_stopped) {
     	
		    rlm->_stopped = false;
		    retVal = kCFRunLoopRunStopped;
		} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
     
			//mode中不存在source/timer/observer
		    retVal = kCFRunLoopRunFinished;
		}
	} while (0 == retVal);
	
}

重学OC第二十六篇:RunLoop_第4张图片
Runloop的休眠调用了__CFRunLoopServiceMachPort方法,它里面其实调用的是mach_msg,而mach_msg又通过mach_msg_trap实现了用户态内核态之间的转换。

在对事件进行处理时会调用__CFRunLoopDoTimers、__CFRunLoopDoObservers等函数,而系统在其中回调时通常使用如下几个函数进行回调:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();  //observer
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();  //block
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(); //main_dispatch_queue
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(); //timer
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(); //source0
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(); //source1

总结

主线程会自动创建运行Runloop,而辅助线程Runloop需要自行配置并启动才能起作用。


参考文章:
官方runloop地址
ibireme:深入理解RunLoop 转载备用链接
iOS刨根问底-深入理解RunLoop

你可能感兴趣的:(重学OC系列)