RunLoop

RunLoop

官方文档

  • NSRunLoop
  • Threading Programming Guide

A NSRunLoop object processes input for sources such as mouse and keyboard events from the window system, NSPort objects, and NSConnection objects. A NSRunLoop object also processes NSTimer events.
Your application neither creates or explicitly manages NSRunLoop objects. Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.
Note that from the perspective of NSRunLoop, NSTimer objects are not “input”—they are a special type, and one of the things that means is that they do not cause the run loop to return when they fire.
NSRunLoop对象处理输入源例如系统的鼠标和键盘的事件,NSPort对象,和NSConnection对象。NSRunLoop对象也处理NSTimer事件。每个NSThread对象——包括应用的主线程,都有一个按需自动创建的NSRunLoop对象。通过currentRunLoop方法,获取当前线程的runloop


内容来自Runloop

Runloop 基本概念

Runloop 是什么?Runloop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

盗一张苹果官方文档的图,也是几乎每个讲 Runloop 的文章都会引用的图,大体说明了 Runloop 的工作模式:

RunLoop_第1张图片

Runloop 与线程

Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。

主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。

Input Source 和 Timer Source

这两个都是 Runloop 事件的来源,其中 Input Source 又可以分为三类

  • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到
  • Custom Input Sources,用户手动创建的 Source
  • Cocoa Perform Selector SourcesCocoa 提供的 performSelector 系列方法,也是一种事件源

Timer Source 顾名思义就是指定时器事件了。

Runloop Mode

在监视与被监视中,Runloop 要处理的事情还挺复杂的。为了让 Runloop 能专心处理自己关心的那部分事情,引入了 Runloop Mode 概念。

RunLoop_第2张图片

如图所示,Runloop Mode 实际上是 SourceTimerObserver 的集合,不同的 Mode 把不同组的 SourceTimerObserver 隔绝开来。Runloop 在某个时刻只能跑在一个 Mode 下,处理这一个 Mode 当中的 SourceTimerObserver

苹果文档中提到的 Mode 有五个,分别是:
内容来自RunLoop详解

  • NSConnectionReplyMode处理NSConnection对象相关事件,系统内部使用,这个mode表明NSConnection对象等待reply,用户基本不会使用
  • NSModalPanelRunLoopMode处理modal panels事件,需要等待处理的input sourcemodal panel时设置,比如NSSavePanelNSOpenPanel
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes
  • NSDefaultRunLoopMode

iOS 中公开暴露出来的只有 NSDefaultRunLoopModeNSRunLoopCommonModesNSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode

与 Runloop 相关的坑

日常开发中,与 runLoop 接触得最近可能就是通过 NSTimer 了。一个 Timer 一次只能加入到一个 RunLoop 中。我们日常使用的时候,通常就是加入到当前的 runLoopdefault mode 中,而 ScrollView 在用户滑动时,主线程 RunLoop 会转到 UITrackingRunLoopMode 。而这个时候, Timer 就不会运行。

有如下两种解决方案:

第一种: 设置RunLoop Mode,例如NSTimer,我们指定它运行于 NSRunLoopCommonModes ,这是一个Mode的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。
第二种: 另一种解决Timer的方法是,我们在另外一个线程执行和处理 Timer 事件,然后在主线程更新UI
AFNetworking 3.0 中,就有相关的代码,如下:

- (void)startActivationDelayTimer {
    self.activationDelayTimer = [NSTimer
                                 timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO];
    [[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes];
}

这里就是添加了一个计时器,由于指定了 NSRunLoopCommonModes,所以不管 RunLoop 出于什么状态,都执行这个计时器任务。


一些测试
为了把NSTimer添加到一个子线程中,创建一个WZThread类,继承自NSThread,重写dealloc方法:

- (void)dealloc
{
    NSLog(@"WZWZThread销毁");
}

如下,创建子线程,并把NSTimer添加到其中,如下:

    WZThread *thread = [[WZThread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(timerMethod) userInfo: nil repeats: YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    }];
    [thread start];

启动后,发现NSTimer要执行的方法并没有运行,控制台输出:

2017-06-21 10:43:11.712 RunTimeTest[5690:294930] 当前线程:0x608000275900>{number = 5, name = (null)}
2017-06-21 10:43:11.713 RunTimeTest[5690:294930] WZWZThread销毁

表示WZWZThread已经销毁,这里是因为子线程创建出来后,执行完毕就异步销毁了。

怎么让子线程常驻?
每一个子线程都一个RunLoop,默认是不启动的,所以子线程执行完代码后就挂掉了

如下:

    WZThread *thread = [[WZThread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(timerMethod) userInfo: nil repeats: YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    }];
    [thread start];

- (void)timerMethod
{
    NSLog(@"timerMethod");
    NSLog(@"%@", [NSThread currentThread]);
}

[[NSRunLoop currentRunLoop] run]后,timer正常运行

2017-06-21 11:03:05.284 RunTimeTest[5785:304973] timerMethod
2017-06-21 11:03:05.285 RunTimeTest[5785:304973] 0x60800027c1c0>{number = 5, name = (null)}

但查看控制台输出,会发现NSLog(@"当前线程:%@", [NSThread currentThread]);并没有运行

原因是什么呢?
参考深入研究 Runloop 与线程保活

所以可以通过设置一个flag来退出循环,如下:

    WZThread *thread = [[WZThread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @selector(timerMethod) userInfo: nil repeats: YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        while(!self.isFinished) {
            [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
        }
        NSLog(@"当前线程:%@", [NSThread currentThread]);
    }];
    [thread start];

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.finished = YES;
}

控制台输出如下:

2017-06-21 11:39:23.990 RunTimeTest[5879:320368] timerMethod
2017-06-21 11:39:23.991 RunTimeTest[5879:320368] 0x608000275580>{number = 5, name = (null)}
2017-06-21 11:39:24.989 RunTimeTest[5879:320368] timerMethod
2017-06-21 11:39:24.990 RunTimeTest[5879:320368] 0x608000275580>{number = 5, name = (null)}
2017-06-21 11:39:25.512 RunTimeTest[5879:320368] 当前线程:0x608000275580>{number = 5, name = (null)}
2017-06-21 11:39:25.513 RunTimeTest[5879:320368] WZWZThread销毁

Runloop 的启动与退出

考验英文水平的时候到了,首先来看一段官方文档对于如何启动 runloop 的介绍,它的启动方式一共有三种:

  1. Unconditionally
  2. With a set time limit
  3. In a particular mode

这三种进入方式分别对应了三种方法,其中第一种就是我们目前使用的:

  1. run
  2. runUntilDate
  3. runMode:beforeDate:

接下来分别是对三种方式的介绍,文字比较啰嗦,这里我简单总结一下,有兴趣的读者可以直接看原文。

  • 无条件进入是最简单的做法,但也最不推荐。这会使线程进入死循环,从而不利于控制 runloop,结束 runloop 的唯一方式是 kill
  • 如果我们设置了超时时间,那么 runloop 会在处理完事件或超时后结束,此时我们可以选择重新开启 runloop。这种方式要优于前一种
  • 这是相对来说最优秀的方式,相比于第二种启动方式,我们可以指定 runloop 以哪种模式运行。

查看 run 方法的文档还可以知道,它的本质就是无限调用 runMode:beforeDate: 方法,同样地,runUntilDate: 也会重复调用 runMode:beforeDate:,区别在于它超时后就不会再调用。

总结来说,runMode:beforeDate: 表示的是 runloop 的单次调用,另外两者则是循环调用。

相比于 runloop 的启动,它的退出就比较简单了,只有两种方法:

  • 设置超时时间
  • 手动结束

如果你使用方法二或三来启动 runloop,那么在启动的时候就可以设置超时时间。然而考虑到目标是:“利用 runloop 进行线程保活”,所以我们希望对线程和它的 runloop 有最精确的控制,比如在完成任务后立刻结束,而不是依赖于超时机制。

好在根据文档的描述,我们还可以使用 CFRunLoopStop() 方法来手动结束一个 runloop。注意文档中在介绍利用 CFRunLoopStop() 手动退出时有下面这句话:

The difference is that you can use this technique on run loops you started unconditionally.

这里的解释非常容易产生误会,如果在阅读时没有注意到 exitterminate 的微小差异就很容易掉进坑里,因为在 run 方法的文档中还有这句话:

If you want the run loop to terminate, you shouldn’t use this method

总的来说,如果你还想从 runloop 里面退出来,就不能用 run 方法。根据实践结果和文档,另外两种启动方法也无法手动退出。

正确的做法
难道子线程中开启了 runloop 就无法结束并释放了么?这显然是一个不合理的结论,经过一番查找,终于在这篇文章里找到了答案,它给出了使用 CFRunLoopStop() 无效的原因:

CFRunLoopStop() 方法只会结束当前的 runMode:beforeDate: 调用,而不会结束后续的调用。

这也就是为什么 Runloop 的文档中说 CFRunLoopStop() 可以 exit(退出) 一个 runloop,而在 run 等方法的文档中又说这样会导致 runloop 无法 terminate(终结)。

文章中给出的方案是使用 CFRunLoopRun() 启动 runloop,这样就可以通过 CFRunLoopStop() 方法结束。而文档则推荐了另一种方法:

BOOL shouldKeepRunning = YES;        // global  
NSRunLoop *theRL = [NSRunLoop currentRunLoop];  
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);  

监听RunLoop

runLoop的终极大杀器

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 1
    kCFRunLoopBeforeTimers = (1UL << 1), // 2
    kCFRunLoopBeforeSources = (1UL << 2), // 4
    kCFRunLoopBeforeWaiting = (1UL << 5), // 32
    kCFRunLoopAfterWaiting = (1UL << 6), // 64
    kCFRunLoopExit = (1UL << 7), // 128
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Observer的使用步骤

添加Observer
添加观察者:监听RunLoop的状态
释放Observer

// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});

// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// 释放Observer
CFRelease(observer);
-(void)observer{

    //创建一个监听对象
    /*
     第一个参数:分配存储空间的
     第二个参数:要监听的状态 kCFRunLoopAllActivities 所有状态
     第三个参数:是否要持续监听
     第四个参数:优先级
     第五个参数:回调
     */
   CFRunLoopObserverRef observer =  CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"runloop进入");
                break;

            case kCFRunLoopBeforeTimers:
                NSLog(@"runloop要去处理timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"runloop要去处理Sources");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"runloop要睡觉了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"runloop醒来啦");
                break;

            case kCFRunLoopExit:
                NSLog(@"runloop退出");
                break;
            default:
                break;
        }
    });


    //给runloop添加监听者
    /*
     第一个参数:要监听哪个runloop
     第二个参数:监听者
     第三个参数:要监听runloop在哪种运行模式下的状态
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(run1) userInfo:nil repeats:YES];

    CFRelease(observer);

}

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

实践例子

  • iOS--利用RunLoop优化加载表格
  • CFRunLoop
  • iOS-Runloop常驻线程/性能优化

其它文章

  • 深入理解RunLoop

你可能感兴趣的:(Objective-C)