运行循环- Runloop

一、RunLoop 介绍

程序启动的原理和过程

运行循环- Runloop_第1张图片
程序启动原理图

start -> (加载framework,动态静态链接库,启动图片,Info.plist等) -> main函数 -> UIApplicationMain函数

  - 初始化`UIApplication`单例对象
  - 初始化`AppDelegate`对象,并设为`UIApplication`对象的代理
  - 检查`Info.plist`设置的`xib`文件是否有效,如果有则解冻`Nib`文件并设置`outlets`,创建显示`key window`、`rootViewController`、与`rootViewController`关联的`根view`(没有关联则看`rootViewController`同名的`xib`),否则`launch`之后由程序员手动加载。
  - 建立一个主事件循环,其中包含UIApplication的Runloop来开始处理事件。

Runloop的概念

当有持续的异步任务需求时,我们会创建一个独立的生命周期可控的线程。RunLoop就是控制线程生命周期并接收事件进行处理的机制。RunLoopiOS事件响应与任务处理最核心的机制,它贯穿iOS整个系统。使系统更加流畅、省电、响应快,用户体验好。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoopCFRunLoopRef
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的API,所有这些API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些API 不是线程安全的。

Runloop比喻

进程是一家工厂,线程是一个流水线,Run Loop就是流水线上的主管;当工厂接到商家的订单分配给这个流水线时,Run Loop就启动这个流水线,让流水线动起来,生产产品;当产品生产完毕时,RunLoop就会暂时停下流水线,节约资源。
RunLoop管理流水线,流水线(线程)才不会因为无所事事被工厂(进程)销毁;而不需要流水线时,就会辞退RunLoop这个主管,即退出线程,把所有资源释放。

RunLoop并不是iOS平台的专属概念,在任何平台的多线程编程中,为控制线程的生命周期,接收处理异步消息都需要类似RunLoop的循环机制实现,AndroidLooper就是类似的机制。

Runloop特性

  • 主线程的RunLoop在应用启动的时候就会自动创建
  • 其他线程则需要在该线程下自己启动
  • 不能自己创建RunLoop
  • RunLoop并不是线程安全的,所以需要避免在其他线程上调用当前线程的RunLoop
  • RunLoop负责管理autorelease pools
  • RunLoop负责处理消息事件,即输入源事件和计时器事件

Runloop的目的

  • 使程序一直运行接受用户输入
  • 决定程序在何时应该处理哪些Event
  • 调用解耦(对于编程经验为0的完全没搞懂这个意思,解释为Message Queue)
  • 节省CPU时间

二、RunLoop 相关接口

CoreFoundation 里面关于 RunLoop 有5个类:

  • CFRunLoopRefRunLoop对象
  • CFRunLoopModeRef :运行模式
  • CFRunLoopSourceRef :输入源/事件源
  • CFRunLoopTimerRef :定时源
  • CFRunLoopObserverRef :观察者

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:

运行循环- Runloop_第2张图片
RunLoop结构

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source / Timer / Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode (_currentMode) ,如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

Mode

CFRunLoopModeCFRunLoop 的结构大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

五种 Mode 模式 :

kCFRunLoopDefaultMode默认模式,主线程是在这个运行模式下运行。

UITrackingRunLoopMode跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)。

UIInitializationRunLoopMode在刚启动App时第进入的第一个 Mode,启动完成后就不再使用。

GSEventReceiveRunLoopMode接受系统内部事件,通常用不到。

kCFRunLoopCommonModes伪模式,不是一种真正的运行模式,实际是 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 的结合 ==> NSRunLoopCommonModes。

NSRunLoopCommonModes

kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode 是苹果公开的两个模式,可以通过mode name 来操作对应的 mode。 同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name

kCFRunLoopCommonModes 对应的 _commonModes :一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source / Observer / Timer 同步到具有 “Common” 标记的所有Mode里。

NSRunLoopCommonModes 实例解析:(广告图轮播或者TableView滑动时Timer):
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时默认所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时手动滑动一个轮播图时或者TableView,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

CFRunLoop对外暴露的管理 Mode 接口

CFRunLoop对外暴露的管理 Mode 接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

Mode 暴露的管理 mode item

Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

CFRunLoopSourceRef

CFRunLoopSourceRef是事件产生的地方。Source有两个版本:Source0Source1

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop)来唤醒 RunLoop,让其处理这个事件。

  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

  • 事件是在Sources0中处理的。至于 Source1 主要是用来接收、分发系统事件,然后再分发到Sources0中处理。

CFRunLoopTimerRef

CFRunLoopTimerRef 是时间的触发器,它和 NSTimertoll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行那个回调。

CFRunLoopObserverRef

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
};

上面的 Source / Timer / Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个item 都没有,则 RunLoop 会直接退出,不进入循环。

    可以通过以下代码验证RunLoop的几种状态:
    // 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"监听到RunLoop发生改变---%zd",activity);
    });
    // 添加观察者到当前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    // 释放observer
    CFRelease(observer);

RunLoop执行顺序

运行循环- Runloop_第3张图片
RunLoop执行顺序
1. Notify observers that the run loop has been entered.// 通知观察者 RunLoop 已经启动。
2. Notify observers that any ready timers are about to fire. // 通知观察者即将要开始定时器。
3. Notify observers that any input sources that are not port based are about to fire. // 通知观察者任何即将启动的非基于端口的源。
4. Fire any non-port-based input sources that are ready to fire. // 启动任何准备好的非基于端口的源(Source0)。
5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9. //如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
6. Notify observers that the thread is about to sleep. // 通知观察者线程进入休眠状态。
7. Put the thread to sleep until one of the following events occurs: //将线程置于休眠状态,知道下面的任一事件发生才唤醒线程。
 * An event arrives for a port-based input source. // 某一事件到达基于端口的源
 * A timer fires. // 定时器启动。
 * The timeout value set for the run loop expires. // RunLoop 设置的时间已经超时。
 * The run loop is explicitly woken up. // RunLoop 被唤醒。
8. Notify observers that the thread just woke up. // 通知观察者线程将被唤醒。
9. Process the pending event.  // 处理未处理的事件。
 * If a user-defined timer fired, process the timer event and restart the loop. Go to step 2. // 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
 * If an input source fired, deliver the event. // 如果输入源启动,传递相应的消息。
 * If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2. // 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
10. Notify observers that the run loop has exited. //  通知观察者RunLoop结束。
{
    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        // 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        // 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        // 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        // 6. 通知Observers,即将进入休眠
        // 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        // 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
        // 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        // 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        // 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        // 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);

    } while (...);
    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

三、RunLoop的应用

Runloop与线程和自动释放池相关:

  • Runloop的寄生于线程:一个线程只能有唯一对应的runloop;但这个根runloop里可以嵌套子runloops
  • 自动释放池寄生于Runloop:程序启动后,主线程注册了两个Observer监听runloop的pop、push与sleep。一个最高优先级OB监测Entry状态;一个最低优先级OB监听BeforeWaiting状态和Exit状态。
  • 线程(创建) --> runloop将进入 --> 最高优先级OB创建释放池 --> runloop将睡 --> 最低优先级OB销毁旧池创建新池 --> runloop将退出 --> 最低优先级OB销毁新池 --> 线程(销毁)

Runloop与线程

  • 苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()CFRunLoopGetCurrent()
  • 线程和 RunLoop 是一一对应的,其关系是保存在一个全局的 Dictionary 里。
  • 只能在当前线程中操作当前线程的 RunLoop,而不能去操作其他线程的 RunLoop
  • RunLoop 对象在第一次获取 RunLoop 时创建,销毁则是在线程结束的时候。
  • 主线程的 RunLoop 对象系统自动帮助我们创建好了,而子线程的 RunLoop 对象需要我们主动获取,因为子线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。

Runtime相关的NSTimer

//NSTimer:
  // 创建一个定时器(需要手动加到runloop的mode中)
 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

  // 默认已经添加到主线程的runLoop的DefaultMode中 
 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

// performSEL方法
// 内部会创建一个Timer到当前线程的runloop中(如果当前线程没runloop则方法无效;performSelector:onThread: 方法放到指定线程runloop中)
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay

相关类型(GCD的timer与CADisplayLink)

  • GCD的timer
    dispatch_source_t 类型,可以精确的参数,不用以来runloopmode,性能消耗更小。
dispatch_source_set_timer(dispatch_source_t source, // 定时器对象
                              dispatch_time_t start, // 定时器开始执行的时间
                              uint64_t interval, // 定时器的间隔时间
                              uint64_t leeway // 定时器的精度
                              );
  • CADisplayLink
    Timertolerance表示最大延期时间,如果因为阻塞错过了这个时间精度,这个时间点的回调也会跳过去,不会延后执行。
    CADisplayLink 是一个和屏幕刷新率一致的定时器,如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似,只是没有tolerance容忍时间),造成界面卡顿的感觉。

RunLoop处理事件

  • 界面刷新:
    UI改变 ——> 标记UI控件处于待处理状态 ——> 注册监听 ——> 遍历待处理对象进行处理 ——> 更新UI
    当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

  • 事件响应:

    运行循环- Runloop_第4张图片

    程序启动 ——> 用户点击屏幕 ——> 创建事件——>创建自动释放池 ——> Application响应事件 ——> 事件处理完毕销毁自动释放池。
    需要特别注意的是:如果没有使用alloc new copy retain 方法而创建了对象,则内部全是使用了autorelease方法。所以使用自动释放池能对这些对象进行及时释放

  • 手势识别
    如果上一步的_UIApplicationHandleEventQueue()识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
    苹果注册了一个Observer 监测 BeforeWaiting(Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
    当有UIGestureRecognizer的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

  • GCD任务
    当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 blockRunloop只处理主线程的blockdispatch 到其他线程仍然是由 libDispatch处理的。

  • NStime(略)

  • 网络请求
    关于网络请求的接口:最底层是CFSocket层,然后是CFNetwork将其封装,然后是NSURLConnectionCFNetwork进行面向对象的封装,NSURLSessioniOS7 中新增的接口,也用到NSURLConnectionloader线程。所以还是以NSURLConnection为例。
    当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket线程是处理底层socket连接的。NSURLConnectionLoader这个线程内部会使用 RunLoop 来接收底层 socket的事件,并通过之前添加的 Source0通知到上层的 Delegate

    运行循环- Runloop_第5张图片

滑动与图片刷新;

tableviewcell上有需要从网络获取的图片的时候,滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。

-(void)viewDidLoad {
  [super viewDidLoad];
  // 只在NSDefaultRunLoopMode下执行(刷新图片)
  [self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]];    
}

常驻子线程,保持子线程一直处理事件。

为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runOne) object:nil];
    [self.thread start];
}
- (void) runOne{
    NSLog(@"----任务1-----");
    // 下面两句代码可以实现线程保活
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"未开启RunLoop");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(runTwo) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) runTwo{
    NSLog(@"----任务2------");
}

监测卡顿

所谓的卡顿一般是在主线程做了耗时操作,卡顿监测的主要原理是在主线程的 RunLoop 中添加一个 observer,检测从 即将处理Source(kCFRunLoopBeforeSources) 到 即将进入休眠(kCFRunLoopBeforeWaiting)花费的时间是否过长。如果花费的时间大于某一个阙值,则认为卡顿,此时可以输出对应的堆栈调用信息。App卡顿监测

你可能感兴趣的:(运行循环- Runloop)