RunLoop介绍
RunLoop是与线程相关的基本基础结构的一部分。RunLoop直译为运行循环,是线程内用于运行事件处理以响应传入事件的一个循环。RunLoop的作用就是为了在有事件到达时唤醒线程以处理各种事件(如touch事件,计时器,performSelector等),事件处理完让线程进入休眠以节省cpu资源。
RunLoop与线程是一一对应的,主线程的Runloop默认是开启,子线程需要手动开启。为什么perfromSelector:withObject:afterDelay:
在子线程为什么不好使?原因就是该方法会在当前线程的RunLoop的defaultMode下添加一个Timer,而子线程RunLoop没有开启,因此RunLoop会直接退出不会执行,可以在这个方法后面使用[[[NSRunLoop currentRunLoop] run];
手动开启解决。
RunLoop Modes
RunLoop有多种Mode,Mode是Input sources、Timer sources和Observers的集合。RunLoop在同一时刻只能在一种Mode下运行,并且只有在该Mode下的事件才能被处理,只有该Mode下的观察者才会被发送通知。苹果定义的常见Mode有五种:
- NSDefaultRunLoopMode:它是默认的,通常情况下都是在这个模式下运行
- NSConnectionReplyMode:用来监控NSConnection对象的回复的,很少能够用到
- NSModalPanelRunLoopMode:用于标明和Mode Panel相关的事件
- NSEventTrackingRunLoopMode:用于跟踪触摸事件触发的模式
- NSRunLoopCommonModes:这是一个可配置的模式,默认会包含default、modal、track三种,添加到这种模式下的事件在它包含的mode中都可以被处理,另外还可以使用
CFRunLoopAddCommonMode
函数添加自定义模式。
RunLoop 输入源
RunLoop从两种不同的源接收事件,分别是Input sources
和Timer sources
。Input sources
传递异步事件,通常是来自其他线程或其他应用程序;Timer sources
传递同步事件,以预定时间或重复间隔触发。这两种类型的源在事件到达时会使用特定的handler
来处理事件。下图是苹果介绍Runloop与输入源的经典结构图
- Port-Based Sources:Cocoa中可以简单的创建一个NSPort对象添加到RunLoop,port对象会处理所需输入源的创建和配置;而在CoreFoundation中,必须手动创建端口及其运行循环源。
- Custom Input Sources:自定义输入源必须要使用CoreFoundation中的
CFRunLoopSourceRef
相关联的函数 - Cocoa Perform Selector Sources:使用NSObject提供的performSelector方法
- Timer Sources:使用NSTimer或CFRunLoopTimer创建,Timer会有预定的时间触发,若为重复定时器,那么将会在指定的次数*时间间隔后执行,哪怕是被延迟。如果触发时间延迟太多以致错过了一个或多个预定的触发时间,则计时器在错过的时间段内只触发一次,之后将被重新安排为下一个计划的触发时间。
RunLoop除了处理输入源之外,还可以为RunLoop的行为生成了一些通知,为这些通知添加观察者就可以在线程上做一些其他处理,注册观察者的api在Core Foundation
框架中。如使用通过监听RunLoop的状态变化来监测主线程是否发生了卡顿。RunLoop Observer的几种状态:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//启动
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timers
kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//休眠状态 等待被唤醒
kCFRunLoopExit = (1UL << 7),//退出
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态集合
};
RunLoop原理
查看RunLoop的run底层源码,当CFRunLoopRunResult不等于stopped和finished时会执行do...while循环,调用CFRunLoopRunSpecific()函数
typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
kCFRunLoopRunFinished = 1,
kCFRunLoopRunStopped = 2,
kCFRunLoopRunTimedOut = 3,
kCFRunLoopRunHandledSource = 4
};
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()函数中,其内部主要逻辑为
- 首先根据modeName找到对应的mode
- 通知Observers,RunLoop即将进入kCFRunLoopEntry
- 进入循环,调用核心函数__CFRunLoopRun()
- 通知Observers,RunLoop即将退出kCFRunLoopExit
核心函数__CFRunLoopRun()内部循环处理流程
- 0 启动一个10^10的计时器,并开启循环
- 1 通知Observers:将要处理Timers事件
- 2 通知Observers:将要处理Source0事件
- 3 执行所有准备就绪的Source0
- 4 如果有Source1准备就绪,立即开始处理Source1
- 5 通知Observers:线程即将进入休眠
- 6 休眠,等待唤醒
- 7 休眠中被唤醒kCFRunLoopAfterWaiting,若为Timers则执行1,若为source1则执行4,若为GCD则执行GCD回调函数,若被其他调用者手动唤醒,则继续从1开始循环
- 8 事件被处理或超时等改变了RunLoop状态则跳出循环,返回已处理/超时/停止/结束状态。
通过源码可以看到RunLoop处理事件的回调函数有六种,也可以通过debug断点回调,然后查看汇编代码进行佐证,分别为:
- GCD主队列:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
- observer源:CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
- 调用timer:CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
- block应用: CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
- source0:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
- source1:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
RunLoop相关面试题
- 如何创建一个常驻线程?
NSRunLoop无法通过alloc创建,直接在子线程中使用CFRunLoopGetCurrent()或[NSRunLoop currentRunLoop]在第一次时即会进行创建,随后添加一个Port/Source到Runloop,最后调用run启动。如果Runloop的mode中一个item都没有,那么runloop会直接退出。
2.performSelector:withObject:afterDelay:这个方法在子线程中调用有什么问题?
会不起作用,因为这个方法的实现是在当前的runloop的defaultmode下添加一个timer,因为runloop在子线程默认是不开启的,因此不会执行,可在这此方法之后添加调用[[NSRunLoop currentRunLoop] run];或者使用GCD来实现.
- 利用runloop解释界面的渲染过程?
在调用[UIView setNeedDisplay],或者直接调用调用[CAlayer setNeedsDisplay]时,相当于给当前的layer打上了一个脏标记,标识为需要重绘,但此时并没有直接进行绘制。而是会在当前的Runloop即将进行休眠时,即CFRunLoopBeforeWaiting状态时才开始绘制。-
[CAlayer setNeedsDisplay]
会调用[CALayer display]
- 若layer没有delegate,则会
[CALayer drawInContext:]
- 有delegate判断是否实现了
- (void)displayLayer:(CALayer *)layer
异步绘制代理方法 - 若没有实现异步绘制的代理方法,则会继续进行系统绘制的流程,CALayer内部会创建一个Backing Store用于获取图形上下文
- 调用delegate方法
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
,并返回- (void)drawRect:(CGRect)rect
的回调 - 最终 CALayer 都会将位图提交到 Backing Store ,最后提交给 GPU,绘制结束
-
4.解释一下事件响应的过程?
苹果注册了一个基于mach port的source1用来接收系统事件,当一个硬件事件发生(触摸/摇晃/锁屏等),首先会由系统的IOKit.framework封装成一个IOHIDEvent,并且由内核接受,内核接收到事件之后会通过mach port转发给App进程,随后source1即会被触发,然后在回调的内部会把IOHIDEvent处理并包装成UIEvent事件,然后发送给UIWindow进行响应处理。
5.autoreleasePool是在何时进行释放?
App启动后,苹果在主线程的Runloop注册了两个observer,第一个observer监视的RunLoop的Entry事件,在这个事件的回调内会调用autoreleasePoolPush()创建自动释放池,它的优先级最高,保证创建的动作在其他所有回调之前;第二个observer监视的RunLoop的BeforWaiting和Exit两个事件,在BeforWaiting时调用autoreleasePoolPop()释放旧池和autoreleasePoolPush()创建新池,在Exit时调用autoreleasePoolPop()释放旧池,这个observer的优先级最低,保证释放发生在其他回调之后。