Runloop循环是线程基础的一部分,它维持了一个事件处理循环,用于计划工作和协调传入的事件,它的目标就是为了让线程有序高效的运行,在有工作的时候要保持忙碌状态,没有工作的时候让线程保持休眠状态。

每个线程(包括应用程序的主线程)都有一个关联的运行循环对象,在App启动的时候,系统会默认的创建一个主线程的RunLoop循环,用于处理主线程的任务,以及接收用户事件。子线程则需要显式地运行其运行循环。

RunLoop实现

  • RunLoop它是用于处理线程活动状态,响应当前线程传入事件的循环,它是线程的一部分,本质上就是一个doWhile循环, 它处理了2种不同的事件,一种是输入源传递的异步事件,通常来自另外一个线程或者是其他应用程序的消息; 另外一种时候计时器提供的在预定事件重复或间隔发送的同步事件。

iOS 面试题 子线程互相通讯 子线程looper_objective-c

这里需要重点区分soure0和soure1,以及runLoop各种事件之间的关系

RunLoop Modes

  • 它是用来的指定SourceTimer的运行模式,也是定义RunLoop观察着的模式,将不同类型的事件标记为不同的Mode,每个mode有着各自的优先级,方便CPU进行调度和管理,合理的利用CPU的时间片去处理各自的任务。所以Mode是用来影响任务执行的时间片

Mode

Name

Description

Default

kCFRunLoopDefaultMode/NSDefaultRunLoopMode

默认的运行模式

Connection

NSConnectionReplyMode

NSConnection监听回调时间所用的模式

Modal

NSModalPanelRunLoopMode

识别模态面板时所指定的模式

Event tracking

NSEventTrackingRunLoopMode

鼠标拖动,手势跟踪时所用的模式

Common modes

NSRunLoopCommonModes/kCFRunLoopCommonModes

共同模式,共同处理多个mode的模式

Source0和Source1的区别

从代码上来看source1的context中比source0多了一个port

在官方文档中https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1也有这样一句话印证了这一点

Port-based sources are signaled automatically by the kernel, and custom sources must be signaled manually from another thread.

NSTimer

基于runLoop运行,它的底层是由XNU 内核的 mk_timer来驱动的,NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件,RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性

  • CADisplayLink
    优点是精度高,每次刷新结束后都调用,适合不停重绘的计时,例如视频
    缺点容易不小心造成循环引用。selector循环间隔大于重绘每帧的间隔时间,会导致跳过若干次调用机会。不可以设置单次执行。 CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息
  • dispatch_source_t
    基于GCD,精度高,不依赖runloop,简单好使,最喜欢的计时器
    需要注意的点是使用的时候必须持有计时器,不然就会提前释放。

Run Loop Observers

  • 监听RunLoop的生命周期,
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;			/* immutable */
    CFRunLoopObserverCallBack _callout;	/* immutable */
    CFRunLoopObserverContext _context;	/* immutable, except invalidation */
};
  • 对应6种状态的监听
kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
  • 监听的流程也是RunLopp的运行流程
  1. 通知观察者运行循环已经进入__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
  2. 通知观察者事件触发器已经就绪__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
  3. 通知观观察者非端口事件已经就绪__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
  4. 执行非端口且准备完成的事件源,(block事件)__CFRunLoopDoBlocks(rl, rlm);
  5. 如果一个基于口的输入源以及准备完毕并且等待执行,跳转到第9步if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL))
  6. 通知观察者RunLoop将要开始休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting),__CFRunLoopSetSleeping(rl)
  7. 保持线程一直处于睡眠状态,知道接收到如下事件
  • 一个机基于端口的事件__CFRunLoopServiceMachPort
  • timer事件触发 if (livePort == rl->_wakeUpPort)
  • 为运行循环设置的超时值过期
  • RunLoop显示的被唤醒
  1. 通知观察者线程被唤醒
  2. 处理pending的事件
  • 处理timer事件
  • 处理input source事件
  1. 通知观察者RunLoop已经退出运行循环

RunLoop与GCD的关系

  • GCD是Grand Center Dispatch,由系统底层API调用,精度更高,在RunLoop的实现中,才用了GCD来监听RunLoop是否超时
  • 执行GCD派发的block任务时,如dispatch_async(dispatch_get_main_queue(), block)会向主线成的RunLoop发送信息,并在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE执行具体的block事件
  • 其他线程如果有设置RunLoop也是如此.主要是看GCD的block派发到哪个线程,子线程默认不会开启RunLoop

RunLoop与内存关系

  • 通过在RunLoop开启和结束注册2个不同Observers来实现的RunLoop创建和结束监听
  • 监听到开始事件时通过_objc_autoreleasePoolPush创建自动释放池,
  • 监听到结束事件时通过_objc_autoreleasePoolPop销毁释放池

RunLoop与事件响应

  • 系统注册了一个Source1的输入源,用来接收系统事件,其回调函数是__IOHIDEventSystemClientQueueCallback,当接收到来自硬件的事件时,首先由IOKit.framework生成一个IOHIDEvent事件,并交由SpringBoard接收,
  • SpringBoard接收按键(锁屏/静音等),触摸,加速,接近传感器等事件,通过mach port 转发给需要的App进程。
  • 进而触发Source1事件的回调,通过_UIApplicationHandleEventQueue将事件派发到应用程序的事件对类上,UIApplication接收到事件后再往下传递
  • _UIApplicationHandleEventQueue会将IOHIDEvent包装成UIEvent进行分发。

利用RunLoop保持线程常驻

  • 例如在AFNetworking中开启了一个常驻线程用来处理网络请求事件,避免了线程的频繁创建销毁所带来的开销
  • 在对应的线程开启一个RunLoop,设置MatchPort作为它的输入源,然后开启一个运行循环
  • addPort:forMode: 在RunLoop指定模式下添加一个输入源
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
   @autoreleasepool {
       [[NSThread currentThread] setName:@"AFNetworking"];
       NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
       //通常情况其它线程访问这个pot就能给这个线程的RunLoop发送消息,此处只是为了让那个RunLoop有事件源被监听,避免退出
       [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
       [runLoop run];
   }
}
  • 通过dispatch once token,原子执行,安全的生成一个线程,下面的例子开启了一个信息的线程AFNetworking(在这个线程内部的bloc块中已经设置了它的名字)
+ (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卡顿监测方案

第一种方案简单粗暴,才用子线程去ping主线程,如果超时没有回应则说明卡住

第二种方案,捕获machPort消息发送前后的观察事件,如果卡住,则runLoop长时间应该是处于挂起状态的

kCFRunLoopBeforeSources, //  Source0 回调

kCFRunLoopBeforeWaiting, // 等待 mach_port 

kCFRunLoopAfterWaiting, // 接收 mach_port