RunLoop运行循环

RunLoop
——-一次偶然的机会和以前同事群里面聊天,他们要让我写一篇关于RunLoop的文章,作为内部分享所用,于是我就开始准备看了很多大量的资料,官方文档连接为:

https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

。其中有一篇在CocoaChina上写的最为实用,不难看出该作者功底还是很深,连接

http://www.cocoachina.com/ios/20150601/11970.html


当我把该文章分享给同事时,他们大部分人都表示看的不太懂,让我给一篇更加浅显的,我只有抱着死磕自己的态度,继续前行。

那我们就直接进入主题聊聊RunLoop
一:首先我们看看RunLoop到底干什么用的

a. 一个程序之所以能够持续不断的运行就是因为RunLoop
RunLoop运行循环_第1张图片

b.事件循环(Event Loop),能够处理各种事件,比如:定时器事件,PerformSelecter事件,手势识别,触摸事件等;

c. 他就像你的保姆一样,让她干活的时候她就干活,不让她干活的时候她就会主动休息,时刻准备着为你服务(里面相当于一个do–while循环,后面将详细介绍)。

二:OSX/iOS 系统中,有两种方式来访问和使用RunLoop

第一种.CFRunLoopRef代表着RunLoop对象,隶属于CoreFoundation 框架内的,纯C语言的IPA,这些IPA都是线程安全的。

第二种.NSRunLoop是基于CFRunLoopRef的OC包装,而且不是线程安全的。

三.RunLoop与线程的关系

a. 每一条线程都有对应的RunLoop对象,就像主线程一样,它只所以不死就是因为对应的RunLoop对象不死。所以如果你想让一个子线程不死,只要保证对应的RunLoop对象不死即可。线程和RunLoop对象是保存在全局的字典中的,以线程作为key,以RunLoop作为值。

b. RunLoop在第一次或取时被创建(苹果不允许直接创建,只能被获取,后面将会详细介绍),线程执行任务完将会被销毁。而且程序一起动就会默认创建了主线程,这时主线程对应的RunLoop对象也已经创建完毕,子线程的RunLoop对象需要你手动创建(说创建,其实就是直接获取,后面将详细介绍,这个东西不能急)。

四.由于RunLoop不断处理用户的交互事件,那么我们来探讨一下它是怎样处理的,首先,我们先了解一下牵扯到的类有哪些:

在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

看到这些类之后,你可能就蒙逼了,这个很正常。下面我们将详细介绍这些类是干什么的。先看一幅图:
RunLoop运行循环_第2张图片

到这里你只需要知道各个类的对应关系。

  1. CFRunLoopRef 可以理解就是RunLoop
  2. 我们看看CFRunLoopModeRef 是干什么用的
    a. CFRunLoopModeRef 代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer.
    b. 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode.
    c. 如果需要切换Mode,只能先退出Loop,再重新指定一个Mode进入,每一个Mode都是相互独立的。
    d. [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
//    [[NSRunLoop currentRunLoop] runMode:<#(NSString *)#> beforeDate:<#(NSDate *)#>];

当使用RunLoop时,需要传人一个Mode.这时系统为我们注册了5个Mode:

一. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响.

二. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行.

三. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常不用.

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

五.kCFRunLoopCommonModes: 这是一个占位用的Mode,你可以理解它就是一个标记或标签,用于标记上面的某些模式,不是一种真正的Mode,(使用时基本代表默认模式和跟踪模式,在主线程中,后面会讲到).
现在你已经对Mode有一定的了解,先别管会不会用,后面将详细讲解用法。

五:我们看看Source是干什么的

先看看苹果官方在文档里面给的一幅图片:

RunLoop运行循环_第3张图片

这时看这幅图也许有些蒙蒙的。先看右上角Input Sources 这部分就展示了Source的作用:我们称Source为事件源或输入源。现在你还是只需要知道Source就是一些事件,比如按钮点击事件,触摸事件,performSelector事件等。
我们通常把Source分为:Source0和Source1(这里我们是以函数调用栈来划分的,还有以其他方式划分我们先不予介绍)
Source0:非基于Port的
Source1:基于Port的,通过内核和其他线程通信,把接收和分发的系统事件交给Source0处理。
到这个时候,你只需要知道Source就是代表着一些事件,当有事件就会通知RunLoop这时RunLoop就会跑一遍,用来处理这些事件。当没有事件的时候他就会制动进入休息状态(后面将详细介绍处理过程)。

六:既然说到事件,那我们继续聊聊CFRunLoopTimerRef是干什么的

CFRunLoopTimerRef是基于时间的触发器。你可以认为他就是NSTimer。
说到NSTimer大家肯定就比较熟悉了,那我们就从熟悉的来。上代码

//创建定时器对象
    NSTimer *timer = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(goOn) userInfo:nil repeats:YES];
      //把定时器添加RunLoop中,给个默认模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

原理:当定时器加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行定时器要执行的方法。

前面我们就已经提到,RunLoop在某一时刻只能在一种模式下。而该模式又包括相应的NSTimer对象,当把NSTimer添加到默认模式下时,这时定时器就会在默认模式下工作,当RunLoop由于某种原因要改变模式时,该定时器就会失效。
比如当有定时器时,用户又在拖拽tableview,此时RunLoop会从NSDefaultRunLoopMode切换到UITrackingRunLoopMode模式,这时定时器就会失效,如果不想让定时器失效,可以把定时器添加在跟踪模式下。当然你也可以这样干比如:

//创建定时器对象
    NSTimer *timer = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(goOn) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    //在前面我们讲到有五种模式,其中NSRunLoopCommonModes不是一种真正的模式,它就是一种标记,表示如果其它四种之一被标记为CommonModes,就会在该模式下工作。在主线程RunLoop中kCFRunLoopDefaultMode 和 UITrackingRunLoopMode这两个 Mode 都已经被标记为"Common"属性,所以该代码在这两种模式下都能正常工作。此时你无论怎样拖拽tableview,定时器都会正常工作。

读到这里你已经对NSTimer有了一个基本的认知,最起码要知道定时器的事件也是交给RunLoop处理的。

七. 接下来谈谈CFRunLoopObserverRef是干什么用的

作用:CFRunLoopObserverRef能够监听RunLoop的状态的改变,我们称之为观察者。
既然它是监听RunLoop状态的,那么我们看看RunLoop到底都有什么状态:

kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit          = (1UL << 7), // 即将退出Loop

也就是说当RunLoop的这些状态发生改变的时候,Observer就会立即知道。

八.你已经对RunLoop有了基本的了解,下面我们看看它的整个循环过程,
先看一幅图,下面将一步一步解析:
RunLoop运行循环_第4张图片

1.当RunLoop即将启动或已经启动时会通知观察者。

// 1. 通知 Observers: RunLoop 即将进入 loop。
  __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

 // 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {}

2.当即将要处理定时器事件时会通知观察者。

// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
  __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

3.当即将启动非基于端口的源时会通知观察者。

// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
  __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
 // 执行被加入的block
   __CFRunLoopDoBlocks(runloop, currentMode);

4.启动准备好的非基于端口的源。

 // 4. RunLoop 触发 Source0 (非port) 回调。
   sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
 // 执行被加入的block
  __CFRunLoopDoBlocks(runloop, currentMode);

5.当基于端口的源已准备好时,就会立即启动,然后处理一些未处理的事件(跳到第九步)。

// 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.线程休眠直到下面任何事件发生:
a. 定时器启动
b. RunLoop设置的时间已经超时
b. 外部手动唤醒
d. 某一事件到达基于端口的源

// 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.通知观察者线程被唤醒。

 // 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

9.处理事件:
a. 如果用户定义的定时器启动,就处理定时器事件并重启RunLoop.跳到第二步。
b. 如果RunLoop被显示唤醒并且还没有超时,重启RunLoop.跳到第二步。
c. 如果输入源启动,传递相应的消息。

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

  // 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }

  // 执行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);

10.RunLoop结束,通知观察者。

// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

上面基本上是RunLoop的整个运作过程,其实在第一步之前,也就是当RunLoop即将启动或已经启动通知观察者之前,RunLoop还要找到对应的Mode,以及判断Mode里面有没有source/timer/observer,如果没有就会直接返回。

 // 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
 // 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

九:RunLoop常见获取方式

a. Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

b. Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

十:当需要有些子线程不死,并且在子线程中频繁的做一些耗时操作时,我们可以让一个子线程不死。子线程的特性:默认情况下当子线程把耗时操作做完之后就会销毁。现在先明确一下需求,我们想当一个子线程执行一个耗时操作之后不让它销毁,也就是还可以在该线程中继续执行其他的耗时操作。在上面我们讲RunLoop与线程的关系时,要想让子线程不死,只需让对应的RunLoop对象不死即可。我们模拟一下该情况:

@interface ViewController ()

/**线程对象为了更好的拿到该线程来使用它,不要误认为把线程搞一个强引用,该线程就不会销毁,如果不信你可以自己尝试一下,我已经尝试过啦,会立即崩溃*/

@property (nonatomic,strong)NSThread *thread ;


@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //创建一个线程,并让它执行love方法
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(love) object:nil];
    //启动线程,这时就会执行对应的love方法
     [self.thread start];
}

- (void)love
{
     NSLog(@"%s",__func__);

    //1.给RunLoop对象增加一个port,相当于增加一个Source,看到这如果你对AFNetworking有一定的了解的话,你会发现该作者就是这样干的
    [[NSRunLoop currentRunLoop]addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

    //2.让RunLoop跑起来
    [[NSRunLoop currentRunLoop]run];

    //注意:如果没有1,该RunLoop就会立即退出,因为RunLoop首先会判断Mode里面有没有source/timer/observer,如果没有就会直接退出,在主线程的RunLoop中系统已经默认加了。
    //如果没有上面的1和2,该线程执行完love方法之后,就会立即销毁,当点击屏幕下面的touchesBegan:也就没有反应。

}
//就是因为在love里面相当于创建了RunLoop对象,所以每次点击屏幕时就会执行go方法。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //在onThread:这个线程里面执行performSelector:这个方法,withObject:参数,waitUntilDone:是否等待
    [self performSelector:@selector(go) onThread:self.thread withObject:nil waitUntilDone:NO];

}
- (void)go
{
    NSLog(@"%s",__func__);
}

你可能感兴趣的:(ios,loop,runloop,运行循环)