一起进大厂之RunLoop

先说说RunLoop 是什么?

RunLoop 是与线程关联的事件循环对象。iOS App 能持续响应触摸、Timer、端口消息和主队列任务,是因为 UIApplicationMain 建立应用对象并进入主事件循环;不是 main 已经运行结束后,RunLoop 又把进程“续上”。事实上,UIApplicationMain 在应用正常运行期间不会返回。

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

Runloop是个对象,怎么获取呢?

  • Foundation [NSRunloop currentRunLoop];获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop];获得主线程的Runloop对象

  • Core Foundation CFRunLoopGetCurrent();获得当前线程的RunLoop对象 CFRunLoopGetMain();获得主线程的Runloop对象

再说说RunLoop的实现机制是什么?

image.png

为了方便Runloop机制的理解,下面写一段伪代码来表示一下RunLoop循环。

function runloop() { 
initialize(); 
	do { 
		var message = get_next_message();//从队列获取消息 
			process_message(message);//处理消息 
		} while (message != quit);//当触发quit条件时,Runloop退出 
	} 

从伪代码可以看出,RunLoop 的关键是“处理已就绪事件,在没有工作时阻塞等待,被新事件唤醒后继续处理”。这里需要纠正一个常见说法,sleep 状态下的线程同样不会持续占用 CPU;RunLoop 不能简单用固定时长的 sleep 替代,主要因为它需要同时等待 Mach port、Timer、显式唤醒和超时,并在事件到来时尽快恢复,而不是因为 sleep 会一直吃 CPU。

image.png

这里要注意用户态和内核态 这两个概念,还有mach_msg()方法。 内核态 这个机制是依靠系统内核来完成的(苹果操作系统核心组件 Darwin 中的 Mach )。

下面是RunLoop实现的流程源码:

/// RunLoop的实现 
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) { 
/// 首先根据modeName找到对应mode 
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); 
/// 如果mode里没有source/timer/observer, 直接返回。 
if (__CFRunLoopModeIsEmpty(currentMode)) return; 
/// 1\. 通知 Observers: RunLoop 即将进入 loop。 
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); 
/// 内部函数,进入loop 
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) { 
	Boolean sourceHandledThisLoop = NO; 
	int retVal = 0; do { 
		/// 2\. 通知 Observers: RunLoop 即将触发 Timer 回调。 
		__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); 
		/// 3\. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。 
		__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); 
		/// 执行被加入的block 
		__CFRunLoopDoBlocks(runloop, currentMode); 
		/// 4\. RunLoop 触发 Source0 (非port) 回调。 
		sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); 
		/// 执行被加入的block 
		__CFRunLoopDoBlocks(runloop, currentMode); 
		/// 5\. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。 
		if (__Source0DidDispatchPortLastTime) { 
			Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) 
			if (hasMsg) goto handle_msg; 
		} 
		///6\. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。 
		if (!sourceHandledThisLoop) { 
			__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); 
		} 
		/// 7\. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。 
		/// • 一个基于 port 的Source 的事件。 
		/// • 一个 Timer 到时间了 
		/// • RunLoop 自身的超时时间到了 
		/// • 被其他什么调用者手动唤醒 
		__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
			 mach_msg(msg, MACH_RCV_MSG, port); 
			 // thread wait for receive msg 
			} 
			/// 8\. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
			 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
			  /// 收到消息,处理消息。 handle_msg: 
			  /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。 
			  if (msg_is_timer) { 
				  __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()) 
			 } 
			 /// 9.2 如果有dispatch到main_queue的block,执行block。 
			 else if (msg_is_dispatch) { 
				 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); 
			} 
			/// 如果没超时,mode里没空,loop也没被停止,那继续loop。 
			} while (retVal == 0); 
		} 
		/// 10\. 通知 Observers: RunLoop 即将退出。 
		__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); 
	} 

源码我删减了很多,看源码里的注释,可以了解个Runloop运行的流程。 咱们还是围绕RunLoop的核心来理解, 既然上面提到休眠是通过内核来完成的,那唤醒条件呢? 下面几个就是主要的唤醒Runloop的事件:

  • 收到基于 port 的 Source1 的事件
  • Timer到时间执行
  • RunLoop自身的超时时间到了
  • 被其他调用者手动唤醒

关于RunLoop的source1和source0

Source0 是需要应用主动 signal 并唤醒 RunLoop 的非端口源,Source1 是由 Mach port 驱动、可由内核消息唤醒的源。触摸事件从系统服务经端口消息进入应用,随后 UIKit 会在内部转换和分发为 UIEvent,但不应概括成“Source1 一定会变成 Source0”。这是 UIKit 私有事件管线,系统版本之间也可能变化。

image.png

RunLoop的有几种Mode, RunLoop设置Mode作用是什么?

RunLoop 可以拥有多个 Mode,但一次 runMode 调用只在一个具体 Mode 中处理事件。系统还存在若干私有 Mode,因此“共有 5 种”并不是公开 API 给出的固定总数。

  • kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
  • UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
  • kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
  • UIInitializationRunLoopModeGSEventReceiveRunLoopMode 等名称属于系统实现细节,不应作为公开、稳定的五种模式清单依赖。

RunLoop 设置 Mode 的作用不是给事件设置高低优先级,而是隔离一组 Source、Timer 和 Observer。当前运行哪个 Mode,就只处理属于该 Mode 的事件;Common Modes 则是一组 Mode 的标记集合,用来把同一个事件源同步加入这些常用 Mode。

为什么只有主线程的Runloop是自动开启的?

@autoreleasepool 只负责建立自动释放池,并不会启动 RunLoop。真正建立并驱动主线程事件循环的是 UIApplicationMain。子线程也能取得自己的 RunLoop 对象,但默认不会自动持续运行,需要显式调用 runrunMode:beforeDate: 或 Core Foundation 对应 API,并确保 Mode 中存在可处理的 Source 或 Timer。

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

PerformSelector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

performSelector:onThread:... 会把 selector 调度到目标线程的 RunLoop,它不等同于 afterDelay: 的 Timer 实现。目标线程必须仍然存活并运行相应 Mode,否则任务不会按预期执行。

UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动TableView的Cell时,label是否刷新?为什么?

不刷新了。 因为NSTimer对象是以NSDefaultRunLoopMode添加到主运行循环中的时候, TableView(ScrollView)滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。如果希望在滚动时,定时器也能运行,那就应该使用common mode。 通过 CFRunloopAddTimer(runloop,timer ,commonMode) 实现。就是同步把事件源timer用同一个mode.

AFNetworking 中如何运用 Runloop?

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
	   @autoreleasepool {
	    [[NSThread currentThread] setName:@"AFNetworking"]; 
	    NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; 
	    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
	    [runLoop run]; 
	    } 
} 
+ (NSThread *)networkRequestThread { 
  static NSThread *_networkRequestThread = nil; 
  static dispatch_once_t oncePredicate; 
  dispatch_once(&oncePredicate, ^{ 
		_networkRequestThread = [[NSThread alloc] initWithTarget:self 
             selector:@selector(networkRequestThreadEntryPoint:) object:nil]; 
	    [_networkRequestThread start]; 
  }); 
  return _networkRequestThread; 
} 

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

- (void)start { 
  [self.lock lock]; 
  if ([self isCancelled]) { 
	  [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; 
	} else if ([self isReady]) { 
		self.state = AFOperationExecutingState; 
		[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; 
	} [self.lock unlock]; 
} 

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

解释一下Runloop在 NSTimer中的的作用

NSTimerCFRunLoopTimerRef toll-free bridged。Timer 只保证“不早于”计划时间触发,主线程忙、Mode 不匹配或系统合并定时器都会造成延迟。对于重复 Timer,如果错过多个周期,RunLoop 通常不会补执行同样次数的回调,而是在恢复处理时触发一次,再按原计划节奏计算后续 fire date。因此它适合 UI 刷新和普通调度,不适合依赖回调次数累计精确计时;秒表应基于单调时钟计算经过时间。

Runloop 和线程的关系?

从概念上看,一个线程至多关联一个 RunLoop,RunLoop 也只在对应线程上运行。主线程事件循环由应用框架驱动;子线程第一次调用获取 API 时可以按需创建 RunLoop 对象,但“对象存在”不代表循环正在运行。

有了线程,你觉得为什么还要有runloop?

Runloop最主要的作用 就是它如何在没有消息处理时休眠,在有消息时又能唤醒。这样可以提高CPU资源使用效率 。runloop 另外一个作用是消息处理。只有线程,是做不到这点的。

GCD 在Runloop中的使用?

提交到主队列的任务会通过 libdispatch 与主线程事件循环的集成,在主线程获得执行机会。它不是“只有子线程返回主线程才触发”,从主线程或任意线程都可以向主队列提交任务;也不应把所有主队列唤醒机制简单等同为一个公开的 Source1 事件。业务层只需记住,主线程被长任务占住时,RunLoop 和主队列任务都会延迟。

AFNetworking 中如何运用 Runloop?

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object { 
	  @autoreleasepool { 
	  [[NSThread currentThread] setName:@"AFNetworking"];
	  NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; 
	  [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
	  [runLoop run]; 
	}
 } 
 + (NSThread *)networkRequestThread { 
   static NSThread *_networkRequestThread = nil; 
   static dispatch_once_t oncePredicate; 
   dispatch_once(&oncePredicate, ^{ 
	   _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; 
	   [_networkRequestThread start]; }); return _networkRequestThread; 
} 

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

- (void)start { 
  [self.lock lock]; 
  if ([self isCancelled]) { 
	  [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; 
  } else if ([self isReady]) { 
	  self.state = AFOperationExecutingState; [
	  self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; 
  } [self.lock unlock]; 
} 

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

PerformSelector:afterDelay:这个方法在子线程中是否起作用?

不起作用,子线程默认没有 Runloop。 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方***失效。可以使用 GCD的dispatch_after来实现afterDelay这样的需求。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效,

当然是CADisplayLink 更精确。

iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。

看上面Runloop在 NSTimer中的使用的问题,就知道NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。可见 NSTimer的定时是很不靠谱的。

CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。