ObjC RunLoop简析

RunLoop,顾名思义就是运行循环的意思,是指程序在运行过程当中循环作一些事情。api

RunLoop 简介

当咱们建立一个terminal项目的时候,此时的main函数中并无一个RunLoop。因此程序运行完main函数以后就退出了。数组

没有RunLoop

而一个iOS的application程序,默认在主线程开启了一个RunLoop,这样一个App就能够处理一些计时器事件,滑动事件等,不会立刻退出。markdown

有RunLoop

在iOS项目中每一条线程都对应着一个RunLoop对象,RunLoop存放在一个以线程做为key的散列表中。app

主线程在建立的时候默认开启RunLoop,而其余子线程默认不开启,可是会在第一次获取RunLoop([NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent())的时候建立。框架

通常状况下RunLoop的生命周期跟随线程,线程结束的时候RunLoop也会被销毁。函数

iOS中提供了一套Foundation框架的NSRunLoop api和一套基于Core Foundation的CFRunLoopRef api来使用RunLoop。其中NSRunLoop是基于CFRunLoopRef作了一层OC的封装。oop

RunLoop 结构

在CFRunLoop的源码中RunLoop的基本结构以下:atom

CFRunLoopRef

CFRunLoopRef是一个__CFRunLoop的结构体,结构体中存放了许多mode相关的成员。spa

其中_currentModeCFRunLoopModeRef类型的,它是一个__CFRunLoopMode类型的结构体指针。RunLoop经过它来表征RunLoop的运行状态。线程

一个RunLoop中包含有许多Mode。_commonModes是一个可变的集合,集合中存放了许多mode。RunLoop在运行的时候只能选择一个Mode做为当前RunLoop执行的状态,也就是_currentMode

mode是CFRunLoopMode类型的。而CFRunLoopMode是经过typedf__CFRunLoopMode获得的。__CFRunLoopMode中存放了处理触摸事件的source0、系统时间捕捉的source1、处理计时器的timers、监听RunLoop状态的observer等。

另外,若是RunLoop须要切换运行状态的时候必须先退出当前的Mode,才能进入新的Mode。若是当前Mode中全部的sourcetimerobserver的时候RunLoop就会马上退出。

常见的RunLoopMode有默认的modekCFRunLoopDefaultMode、跟踪界面(好比:保证滑动不受其余mode影响)的UITrackingRunLoopMode。另外在api中还有一个kCFRunLoopCommonModes可是这并非一个真正的mode,它不存在于_commonModes中,它只是一个标记。

RunLoop 的监听器会监听RunLoop的一些状态:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即将进入runloop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理计时器
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒
    kCFRunLoopExit = (1UL << 7), // 即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有状态
};
复制代码

探究RunLoop的执行流程

咱们能够经过Xcode自带的lldb 经过bt命令查看函数调用栈找到RunLoop的入口函数

RunLoop的入口函数

CFRunLoop.c文件中找到该函数,咱们发现它经过__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry)监听进入RunLoop。接着有一个__CFRunLoopRun的函数调用,该函数中封装了RunLoop处理事件的逻辑。

咱们只关注__CFRunLoopRun主要代码:咱们发现该函数中存在着一个do-while()循环,当retVal==0的时候循环持续进行,当retVal != 0的时候,回返回给函数CFRunLoopRunSpecific,它调用__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);退出RunLoop。

__CFRunLoopRun中主要的流程都在下图中进行了描述。总结来讲:

  1. 首先会通知监听者Observers:即将处理Timers
  2. 通知监听者Observers:即将处理Sources
  3. 处理blocks
  4. 处理source0,若是处理完了会再次处理blocks
  5. 若是存在source1,则跳到handle_msg处理,若是没有则通知监听器即将进入休眠
  6. 休眠时期等待消息来唤醒当前线程
  7. 若是有消息唤醒则进入handle_msg处理计时器,gcd,source1这些信息
  8. 再次处理blocks
  9. 获取返回值retVal
  10. 进入do-while(),若是retVal == 0 则循环持续进行。不然返回给CFRunLoopRunSpecific函数,退出RunLoop

RunLoop的执行流程

RunLoop的应用

Timer

当咱们使用+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;建立一个计时器,而且页面存在scrollView的时候。滑动scrollView计时器就会中止运行。这是由于一开始runloop存在于NSDefaultRunLoopMode,当滑动事件响应的时候runloop会进入UITrackingRunLoopMode模式处理滑动事件,全部timer就会失去处理。

咱们能够使用+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;建立timer,而后将它放在一个NSRunLoopCommonModes标记的模式下进行工做,timer是能够t在_commonModes数组中存放的模式下工做的。这样就解决了滑动事件和timer计时器事件冲突的问题。

线程保活

当咱们建立一条线程的时候,这条线程并无一个RunLoop。当咱们第一次获取RunLoop的时候这条线程中才会建立RunLoop。

因此建立一条一直存在的线程,咱们须要在线程中加入一个不会被回收的RunLoop,也就是让do-while()一直存在,也就是RunLoop一直有事情在处理,而retVal不会为不是0的其余值。

实例代码:

#import "SoCPermanentThread.h"
@interface SoCPermanentThread ()

@property (nonatomic, strong) NSThread *thread;

@end

@implementation SoCPermanentThread

- (instancetype)init {
    if (self = [super init]) {
        self.thread = [[NSThread alloc] initWithBlock:^{
            CFRunLoopSourceContext context = {0};
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            CFRelease(source);
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
        }];
        [self.thread start];
    }
    return self;
}

- (void)executeTask:(SoCPermenantThreadTask)task {
    if (!_thread || !task) return;
    [self performSelector:@selector(__task:) onThread:_thread withObject:task waitUntilDone:NO];
}

- (void)stop {
    if (!_thread) return;
    [self performSelector:@selector(__stop) onThread:_thread withObject:nil waitUntilDone:YES];
}

- (void)__task:(SoCPermenantThreadTask)task {
    task();
}

- (void)__stop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    _thread = nil;
}

- (void)dealloc {
    [self stop];
}

@end
复制代码

上述代码使用Core Foundation实现的线程保活,其中重要的就是首先往RunLoop中添加Source保证RunLoop有事情能够作,另外就是CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);这个方法的最后一个参数BOOL参数returnAfterSourceHandled,其值为flase表明执行完函数(处理完source)不会返回,而true则相反,表示执行完函数(处理完source)会当即返回。

总结

本篇主要以Core Foundation api 为基础(Core Foundation开源)简述了RunLoop的基本概念,和调用流程,因为NSRunLoop是基于CFRunLoop作的OC封装,其原理和流程都是同样的。另外介绍了两个使用RunLoop的案例。