RunLoop入门

什么是RunLoop

Run Loop是一让线程能随时处理事件但不退出的机制。RunLoop 实际上是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。(有个印象就行,暂时不用过于深入)

iOS中的RunLoop

iOS供了两种RunLoop:CFRunLoopRef 和NSRunLoop。

  • CFRunLoopRef: 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。(CFRunLoopRef 的代码是开源的)
  • NSRunLoop:NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

使用场景

保持线程的存活,而不是线性的执行完任务就退出了

不开启Runloop的线程

在遇到一些耗时操作时,为了避免主线程阻塞导致界面卡顿,影响用户体验,往往我们会把这些耗时操作放在一个临时开辟的子线程中。操作完成了,子线程线性的执行了代码也就退出了,就像下面一样。

DSThread重写了dealloc的NSThread的子类

#import <Foundation/Foundation.h>

@interface DSThread : NSThread
@end

#import "DSThread.h"

@implementation DSThread

-(void)dealloc {
  NSLog(@"%@线程被释放了", self.name);
}

@end
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    NSThread subThread = [[DSThread alloc] initWithTarget:self
                                                 selector:@selector(subThread2Do)
                                                   object:nil];;
    subThread.name = @"subThread";
    [subThread start];
}

- (void)subThread2Do {
    NSLog(@"%@----执行子线程任务",[NSThread currentThread]);
}

//运行结果
<NSThread: 0x60000006e080>{number = 1, name = main}----开辟子线程
<DSThread: 0x60400027f680>{number = 3, name = subThread}----执行子线程任务
subThread线程被释放了

运行结果就像一开始所说的一样,子线程执行完操作就自动退出了。

开始RunLoop的线程

1. 实验用self来持有子线程

如果子线程的操作是偶尔或者干脆只需要执行一次的话,像上面那样就没什么问题。但是如果这个操作需要频繁执行,那么按照上面那样的逻辑,我们就需要频繁创建子线程,这是很消耗资源的。就像平时我们在设计类的时候会把需要频繁使用的对象保持起来,而不是频繁创建一样。我们试试把线程“保持”起来,让它在需要的时候执行任务,不需要的时候就啥都不干。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    self.subThread = [[DSThread alloc] initWithTarget:self
                                             selector:@selector(subThread2Do)
                                               object:nil];
    self.subThread.name = @"subThread";
    [self.subThread start];
}

运行结果:
<NSThread: 0x60000006e140>{number = 1, name = main}----开辟子线程
<DSThread: 0x600000277180>{number = 3, name = subThread}----执行子线程任务

从运行结果上看,子线程内部操作完成后并没有被释放,看样子我们成功持有了子线程。那么按照刚才的设想,我们就可以在任何需要的时候开启子线程完成线程里面的操作。

我们在[self.subThread start];后面再添加上一句[self.subThread start];再运行试试看结果。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    self.subThread = [[DSThread alloc] initWithTarget:self
                                             selector:@selector(subThread2Do)
                                               object:nil];
    self.subThread.name = @"subThread";
    [self.subThread start];
    [self.subThread start];
}
//运行结果
<NSThread: 0x600000260480>{number = 1, name = main}----开辟子线程
<DSThread: 0x604000476b40>{number = 3, name = subThread}----执行子线程任务
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: '*** -
...

对,你没看错,它崩溃了。原因如下:

因为执行完任务后,虽然Thread没有被释放,还处于内存中,但是它处于死亡状态(当线程的任务结束后就会进入这种状态)。打个比方,人死不能复生,线程死了也不能复生(重新开启),苹果不允许在线程死亡后再次开启。所以会报错attempt to start the thread again(尝试重新开启线程)

2. 实验让线程不结束任务导致进入死亡状态
既然是线程的任务结束导致了线程进入死亡状态,那么我们不让线程结束任务就行了呗。所以我们用while循环让线程的任务无法结束就行了呗。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    self.subThread = [[DSThread alloc] initWithTarget:self
                                             selector:@selector(subThread2Do) 
                                               object:nil];
    self.subThread.name = @"subThread";
    [self.subThread start];
}

- (void)subThread2Do {
    do {
        NSLog(@"%@----执行子线程任务",[NSThread currentThread]);
    } while (1);
}

//运行结果
<NSThread: 0x604000071c40>{number = 1, name = main}----开辟子线程
<DSThread: 0x60000026d840>{number = 3, name = subThread}----执行子线程任务
<DSThread: 0x60000026d840>{number = 3, name = subThread}----执行子线程任务
<DSThread: 0x60000026d840>{number = 3, name = subThread}----执行子线程任务
<DSThread: 0x60000026d840>{number = 3, name = subThread}----执行子线程任务
<DSThread: 0x60000026d840>{number = 3, name = subThread}----执行子线程任务
<DSThread: 0x60000026d840>{number = 3, name = subThread}----执行子线程任务
...
...
...

一通操作过后代码变成这样。但是写完仔细一想,确实子线程不会进入死亡状态了,但是子线程却在不分时间地点场合的疯狂执行任务。这根我们一开始想象的,需要的时候执行任务,不需要的时候就啥都不干差远了。看起来似乎又是一次失败的尝试,但是别灰心,我们已经越来越接近答案了。

3. Event Loop模式
我们在开发中应该听说过或者看到过这下面一系列方法

performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
 
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
 
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

概括来讲它们的作用都是在某个线程内响应某个方法。既然有这种方法的存在,那么我们是不是可以更改一下思路,让子线程的任务由执行任务变为接收别人发给它的消息,去执行对应的任务,没人给他发消息就休息呢?现在我们来修改一下subThread2Do

- (void)subThreadTodo {
    do {
        1.接收消息
        2.如果没有消息就休息
        3.休息直到接收到了消息,执行消息对应的任务
    } while (消息 != 退出);
}

这样一来就达到了最初的目的,线程的任务直到我们主动让线程退出为止永远不会结束(不会进入死亡状态),可以在需要的时候让线程做对应的事情。但是怎么实现呢?我们不需要考虑,因为苹果已经帮我们做好了。事实上,这是一种模型,它被称为Event Loop。很多平台都有这种模型,而iOS/OSX中的体现就是RunLoop,可以说RunLoop的本质就是do while循环。当然,实际上它的逻辑不会像刚才写的那么简单,还涉及了很多其他东西,为了刚接触的时候不发懵,我们就这么简单的理解它。

4. 初步尝试使用RunLoop
绕了一大圈,终于讲到了RunLoop,现在我们来初步了解下RunLoop如何使用,顺便做个小测试。(这个例子中并没有对线程用self进行引用)

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    NSThread *subThread = [[DSThread alloc] initWithTarget:self
                                                  selector:@selector(subThread2Do)
                                                    object:nil];
    subThread.name = @"subThread";
    [subThread start];
}

- (void)subThread2Do {
    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
    //获取当前子线程的RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //下面这一行必须加,否则RunLoop无法正常启用。我们暂时先不管这一行的意思,稍后再讲。
    [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    //让RunLoop跑起来
    [runLoop run];
    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
}

//运行结果
<NSThread: 0x604000068840>{number = 1, name = main}----开辟子线程
<DSThread: 0x60000026c140>{number = 3, name = subThread}----开始执行子线程任务

这里没有对线程进行引用,也没有让线程内部的任务进行显式的循环。为什么子线程的里面的任务没有执行到输出任务结束这一步,为什么子线程没有销毁?就是因为[runLoop run];这一行的存在。

前面讲了,RunLoop本质就是个Event Loop的do while循环,所以运行到这一行以后子线程就一直在进行接受消息->等待->处理的循环。所以不会运行[runLoop run];之后的代码(这点需要注意,在使用RunLoop的时候如果要进行一些数据处理之类的要放在这个函数之前否则写的代码不会被执行),也就不会因为任务结束导致线程死亡进而销毁。这也就是我们最常使用RunLoop的场景之一,就如小节标题保持线程的存活,而不是线性的执行完任务就退出了。

5. 初体验产生的疑问

通过初步使用RunLoop的小实验可以引发一些与概念有关的疑问。如果嫌太长不想看的可以直接看下面的结论。

  • 为什么总是要把RunLoop和线程放在一起来讲?

总的来讲就是:RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。它是寄生于线程的,所以提到RunLoop必然会涉及到线程。

  • 如何创建RunLoop?
    苹果不允许直接创建 RunLoop,它只提供了四个自动获取的函数
[NSRunLoop currentRunLoop];//获取当前线程的RunLoop
[NSRunLoop mainRunLoop];
CFRunLoopGetMain();
CFRunLoopGetCurrent();

这些函数内部的逻辑大概是下面这样:

/// 全局的Dictionary,key 是 线程, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
     
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
  
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

注:这并不是源码,而是大神为了方便我们理解,对源码进行了一些可读性优化后的结果。但是其大致还是与源码一直的。

通过看这部分代码我们可以产生一个概念,那就是,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

  • 线程默认不开启RunLoop,为什么我们的App或者说主线程却可以一直运行而不会结束?
    主线程是唯一一个例外,当App启动以后主线程会自动开启一个RunLoop来保证主线程的存活并处理各种事件。而且从上面的源代码来看,任意一个子线程的RunLoop都会保证主线程的RunLoop的存在。

  • RunLoop能正常运行的条件是什么?
    看到刚才代码中注释说暂时不管的代码,第一次接触肯定会想[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];这一句是什么意思?为什么必须加这一句RunLoop才能正常运行?

我们仍然通过实验看现象来理解

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    NSThread *subThread = [[DSThread alloc] initWithTarget:self
                                                  selector:@selector(subThread2Do)
                                                    object:nil];
    subThread.name = @"subThread";
    [subThread start];
}

- (void)subThread2Do {
    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
    //获取当前子线程的RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //注释掉下面这行和不注释掉下面这行分别运行一次
    [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    NSLog(@"RunLoop:%@",runLoop);
    //让RunLoop跑起来
    [runLoop run];
    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
    
}

注释掉的结果:

<NSThread: 0x60400006e680>{number = 1, name = main}----开辟子线程
<DSThread: 0x600000274740>{number = 3, name = subThread}----开始执行子线程任务
RunLoop:<CFRunLoop 0x6000001e7d00 [0x107bbac80]>{wakeup port = 0x9c03, 
stopped = false, ignoreWakeUps = true, current mode = (none),
common modes = <CFBasicHash 0x60000024a260 [0x107bbac80]>
{type = mutable set, count = 1,
entries =>
    2 : <CFString 0x107b90818 [0x107bbac80]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = (null),
modes = <CFBasicHash 0x60000024a080 [0x107bbac80]>{type = mutable set, count = 1,
entries =>
    2 : <CFRunLoopMode 0x600000190a80 [0x107bbac80]>{name = kCFRunLoopDefaultMode, 
port set = 0x5c03, queue = 0x6000001416b0, source = 0x604000387290 (not fired),
 timer port = 0x5e03, 
    sources0 = (null),
    sources1 = (null),
    observers = (null),
    timers = (null),
    currently 557491190 (6036886480604) / soft 
deadline in: 1.8446738e+10 sec (@ -1) / hard deadline in: 1.8446738e+10 sec (@ -1)
},

}
}
<DSThread: 0x600000274740>{number = 3, name = subThread}----执行子线程任务结束
subThread线程被释放了

不注释得到的结果

<NSThread: 0x604000070180>{number = 1, name = main}----开辟子线程
<DSThread: 0x600000270000>{number = 3, name = subThread}----开始执行子线程任务
RunLoop:<CFRunLoop 0x6000001fb000 [0x1038d7c80]>{wakeup port = 0x5a03, 
stopped = false, ignoreWakeUps = true, current mode = (none),
common modes = <CFBasicHash 0x600000246db0 [0x1038d7c80]>
{type = mutable set, count = 1,
entries =>
    2 : <CFString 0x1038ad818 [0x1038d7c80]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = <CFBasicHash 0x604000255fc0 [0x1038d7c80]>
{type = mutable set, count = 1,
entries =>
    0 : <CFRunLoopSource 0x604000165dc0 [0x1038d7c80]>
{signalled = No, valid = Yes, order = 200,
 context = <CFMachPort 0x604000145ac0 [0x1038d7c80]>
{valid = Yes, port = 5d03, source = 0x604000165dc0, 
callout = __NSFireMachPort (0x102706eac), 
context = <CFMachPort context 0x604000256080>}}
}
,
modes = <CFBasicHash 0x600000246de0 [0x1038d7c80]>{type = mutable set,
 count = 1,
entries =>
    2 : <CFRunLoopMode 0x600000199a50 [0x1038d7c80]>
{name = kCFRunLoopDefaultMode, port set = 0x5b03, queue = 0x6000001463b0, 
source = 0x600000199bf0 (not fired), timer port = 0x5c03, 
    sources0 = <CFBasicHash 0x6040002561a0 [0x1038d7c80]>
{type = mutable set, count = 0,
entries =>
}
,
    sources1 = <CFBasicHash 0x604000255ff0 [0x1038d7c80]>{type = mutable set,
 count = 1,entries =>
    0 : <CFRunLoopSource 0x604000165dc0 [0x1038d7c80]>
{signalled = No, valid = Yes, order = 200,
context = <CFMachPort 0x604000145ac0 [0x1038d7c80]>{valid = Yes, port = 5d03, 
source = 0x604000165dc0, 
callout = __NSFireMachPort (0x102706eac), 
context = <CFMachPort context 0x604000256080>}}
}
,
    observers = (null),
    timers = (null),
    currently 557491348 (6195471875547) / soft deadline in: 1.84467379e+10 
sec (@ -1) / hard deadline in: 1.84467379e+10 sec (@ -1)
},

}
}

注释掉以后我们看似run了RunLoop但是最后线程还是结束了任务,然后销毁了。与没注释得到的结果比较,造成这一切的原因就在上面两张图片中标注部分的区别上。要解释这一部分就又要开始讲到让我们抓耳挠腮的概念部分,我们先来看一张眼熟到不行的RunLoop结构图。


runloop 结构图

一开始接触RunLoop我看到这张图的时候也是懵逼的,现在我们结合刚才的打印结果来理解。

  • 图中RunLoop蓝色部分就对应我们打印结果中,整个RunLoop部分的打印结果
  • 多个绿色部分共同被包含在RunLoop内就对应,打印结果中modes中同时包含多个Mode(这里可是看打印结果中标注出来的第一行往上再数两行。modes = ... count = 1。一个RunLoop可以包含多个Mode,每个Mode的Name不一样,只是在这个打印结果当中目前刚好Mode个数为1)
  • 每一个绿色部分Mode整体就对应,打印结果中被标注出来的整体。
  • 黄色部分Source对应标注部分source0+source1
  • 黄色部分Observer对应标注部分observer部分
  • 黄色部分Timer对应标注部分timers部分

讲完了结构我们继续来讲Mode是什么。

我对Mode的理解就是”行为模式“,就像我们说到上学这个行为模式,它就应该包含起床,出门,去学校,上课,午休等等。但是,如果上学这个行为模式什么都不包含,那么即使我们进行上学这个行为,我们也一直睡在床上什么都不会做。就像刚才注释掉addPort那一行代码得到的结果一样,RunLoop在kCFRunLoopDefaultMode下run了,但是因为该Mode下所有东西都为null(不包含任何内容),所以RunLoop什么都没做又退出来了,然后线程就结束任务最后销毁。之所以要有Mode的存在是为了让RunLoop在不同的”行为模式“之下执行不同的”动作“互不影响。比如执行上学这个行为模式就不能进行娱乐这个行为模式下的游戏这个动作。RunLoop同一时间只能运行在一种Mode下,当前运行的这个Mode叫currentMode。(这里也许比较抽象,在下面timer部分会有实例结合实例分析。)

一般我们常用的Mode有三种

1.kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。一般情况下App都是运行在这个mode下的

2.(CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
一般作用于ScrollView滚动的时候的模式,保证滑动的时候不受其他事件影响。

3.kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
这个并不是某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程中只包含NSDefaultRunLoopMode。

注意:
①在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功。
②在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。
③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合。

讲完了”行为模式“,现在我们来讲”行为模式“包含的具体”动作“

Source是什么?
source就是输入源事件,分为source0和source1这两种。

1.source0:诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作。
2.source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。

一般来说日常开发中我们需要关注的是source0,source1只需要了解。
之所以说source0更重要是因为日常开发中,我们需要对常驻线程进行操作的事件大多都是source0,稍后的实验会讲到。

Timer是什么?
Timer即为定时源事件。通俗来讲就是我们很熟悉的NSTimer,其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。

Observer是什么?
它相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建

// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
CFAllocatorGetDefault(), 
kCFRunLoopAllActivities, YES, 0,
 ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
 
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

由于它与这一问的关系并不大所以暂时不做过多阐述
重点:它不能作为让RunLoop正常运行的条件,只有Observer的RunLoop也是无法正常运行的。

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

对比刚才的打印日志,再结合刚才讲到的RunLoop结构内容,我们不妨做个猜测。RunLoop能正常运行的条件就是,至少要包含一个Mode(RunLoop默认就包含DefaultMode),并且该Mode下需要有至少一个的事件源(Timer/Source)。事实上经过NSRunLoop封装后,只可以往mode中添加两类事件源:NSPort(对应的是source1)和NSTimer(Timer源放在后面讲)。接下来我们还是用实验来加强理解。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    NSThread *subThread = [[DSThread alloc] initWithTarget:self
                                                  selector:@selector(subThread2Do)
                                               object:nil];
    subThread.name = @"subThread";
    [subThread start];
}

- (void)subThread2Do {
    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
    //获取当前子线程的RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //给RunLoop添加一个事件源,注意添加的Mode,关于这里的[NSMachPort port]我的理解是,
    //给RunLoop添加了一个占位事件源,告诉RunLoop有事可做,让RunLoop运行起来。
    //但是暂时这个事件源不会有具体的动作,而是要等RunLoop跑起来过后等有消息传递了才会有具体动作。
    [runLoop addPort:[NSMachPort port] forMode:UITrackingRunLoopMode];
    
    [runLoop run];
    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
}

//运行结果
<NSThread: 0x60400007f180>{number = 1, name = main}----开辟子线程
<DSThread: 0x60400026ea40>{number = 3, name = subThread}----开始执行子线程任务
<DSThread: 0x60400026ea40>{number = 3, name = subThread}----执行子线程任务结束
subThread线程被释放了

最后跟我们想的不一样,线程释放了,RunLoop没有成功启用。原因就出在[runLoop run];上面。
这句的意思是,在NSDefaultRunLoopMode下运行RunLoop。而我们添加的事件源是在另外一个Mode下,NSDefaultRunLoopMode仍然空空如也,所以RunLoop也就直接退出了。所以我们还要加一个条件,RunLoop正常运行的条件是:1.有Mode。2.Mode有事件源。3.运行在有事件源的Mode下。

除了[runLoop run]还有那些方法启动RunLoop?
NSRunLoop中总共包装了3个方法供我们使用

/**
* 除非希望子线程永远存在,否则不建议使用,因为这个接口会导致
Run Loop永久性的运行NSDefaultRunLoopMode模式, 即使使用 CFRunLoopStop(runloopRef);
*也无法停止RunLoop的运行,那么这个子线程也就无法停止,只能永久运行下去。
*/
- (void)run;

/**
* 比上面的接口好点,有个超时时间,可以控制每次RunLoop的运行时间也是运行在
NSDefaultRunLoopMode模式。这个方法运行RunLoop一段时间会退出给你检查运行条件的机会
* 如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef);
* 仍然无法停止RunLoop的运行,因此最好自己设置一个合理的RunLoop运行时间。比如
* while (!Stop){
*    [[NSRunLoop currentRunLoop] runUntilDate:
           [NSDate dateWithTimeIntervalSinceNow:10]];
* }
*/
- (void)runUntilDate:(NSDate *)limitDate;

/**
* 有一个超时时间限制,而且可以设置运行模式,这个接口在非Timer事件触发
* 、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出返回。
* 如果仅是Timer事件触发并不会让RunLoop退出返回,但是如果是PerfromSelector事件或者其他
Input Source事件触发处理后,
* RunLoop会退出返回YES。同样可以像上面那样用while包起来使用。
*/
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

初体验结论

  • RunLoop是寄生于线程的消息循环机制,它能保证线程存活,而不是线性执行完任务就消亡。
  • RunLoop与线程是一一对应的,每个线程只有唯一与之对应的一个RunLoop。我们不能创建RunLoop,只能在当前线程当中获取线程对应的RunLoop(主线程RunLoop除外)。
  • 子线程默认没有RunLoop,需要我们去主动开启,但是主线程是自动开启了RunLoop的。
  • RunLoop想要正常启用需要运行在添加了事件源的Mode下。
  • RunLoop有三种启动方式run、runUntilDate:(NSDate *)limitDate、runMode:(NSString *)mode beforeDate:(NSDate *)limitDate。第一种无条件永远运行RunLoop并且无法停止,线程永远存在。第二种会在时间到后退出RunLoop,同样无法主动停止RunLoop。前两种都是在NSDefaultRunLoopMode模式下运行。第三种可以选定运行模式,并且在时间到后或者触发了非Timer的事件后退出。

保持线程的存活后,让线程在我们需要的时候响应消息。

前面讲到了几个在某个线程内响应某方法的方法,现在我们就来讲讲这几个方法的具体含义

/**
* 在主线程中响应指定Selector。这两个方法给你提供了选项来阻断当前线程
(不是执行Selector的线程而是调用上述方法的线程)
* 直到selector被执行完毕。
*/
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

/**
* 在某个子线程(NSThread对像)中响应指定Selector。这两个方法同样给你提供了选项来阻断
当前线程直到Selector被执行完毕。
*/
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

/**
在当前线程中执行Selector,并附加了延迟选项。多个排队的Selector会按照顺序一个一个的执行。
*/ 
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

其实,这几个方法都是向线程中的RunLoop发送了消息,然后RunLoop接收到了消息就唤醒线程,去做对应的事情。所以想要正常使用这几个方法,响应selector的线程必须开启了RunLoop。惯例用例子来感受。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    DSThread *tmpThread = [[DSThread alloc] initWithTarget:self 
                                   selector:@selector(subThread2Do) object:nil];
    //subThread用weak声明,用weak声明,用weak声明
    self.subThread = tmpThread;
    self.subThread.name = @"subThread";
    [self.subThread start];
}

- (void)subThread2Do {
    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
    
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    
    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
}


- (void)want2Do {
    //断点2
    NSLog(@"当前线程:%@执行任务处理数据", [NSThread currentThread]);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //断点2
    [self performSelector:@selector(want2Do) onThread:self.subThread 
                withObject:nil waitUntilDone:NO];
}

运行之前我们先把Xcode左边侧栏中选到显示CPU、Memory使用情况那一页,把最下面的按钮中第一个按钮选中状态取消掉。(否则看不见RunLoop的堆栈信息。)


然后我们运行程序,先暂时不做任何操作。

<NSThread: 0x60000006e600>{number = 1, name = main}----开辟子线程
<DSThread: 0x60000027c6c0>{number = 3, name = subThread}----开始执行子线程任务

子线程开启,RunLoop正常运行,似乎与刚才没有任何不同。然后我们点击屏幕任何一个地方。断点1触发,我们来查看左侧的堆栈。


前面提到过UIEvent事件属于source0,从这里的堆栈就可以得到印证。我们在主线程中触发了touchesBegan,然后主线程的RunLoop就开始响应source0事件源,然后去调用对应的方法。我们放过断点继续查看。

同样是前面提到的,performSelector也是source0依然可以从堆栈得到印证。放过断点1后调用了performSelector,然后subThread的RunLoop开始响应source0事件源,然后去调用对应的方法,所以来到了断点2。放过断点2查看结果,整个流程结束,打印日志如下。

<DSThread: 0x60000027c6c0>{number = 3, name = subThread}执行任务处理数据
<DSThread: 0x60000027c6c0>{number = 3, name = subThread}----执行子线程任务结束

最后子线程任务结束然后被释放是因为之前提到的,runMode:(NSString *)mode beforeDate:(NSDate *)limitDate这种启动RunLoop的方式有一个特性,那就是这个接口在非Timer事件触发(此处是达成了这个条件)、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出。而例子当中也没有用while把RunLoop包围起来,所以RunLoop退出后子线程完成了任务最后退出了。

前面两种方法的使用大概就如同这个例子,大同小异。而第三种afterDelay的与前两种不同,并不是属于source0的,而是属于Timer源放在后面来讲。

看了刚才的堆栈信息可能会有疑问,标注出来的部分中,最长的那一串是什么,是干嘛的?为啥在执行发送给RunLoop的消息对应的事件之前,总要调用这么一长串?

其实RunLoop进行回调时,一般都是通过一个很长的函数(call out)调用出去(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的)

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

上面几个函数之所以那么长,我估计官方是想让我们观名知意,看名字就可以猜出作用。但是同样的,刚接触RunLoop的时候过多的接触这些深层次的东西反而会觉得找不到方向。我觉得入门还是先从表面一些的东西入手比较好。等有一些比较全面的了解以后想要深入理解了再来看这部分,现在有个印象知道这个概念就好。想要深入的话可以查看文末的文档或者RunLoop入门学习补充资料(4.RunLoop回调函数触发逻辑)。

让线程定时执行某任务(Timer)

 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法
 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法

初识NSTimer遇到的坑

刚接触Timer的时候,很多人(包括我)肯定都踩过一个坑,那就是创建了Timer却没有启动,百思不得其解,然后才知道还要把Timer加到一个叫RunLoop的东西里面才能正常运行。就像下面一样:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self 
selector:@selector(wantTodo) userInfo:nil repeats:YES];
    //timerWith开头的方法创建的Timer如果不加下面一句无法运行。
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

当时我们可能不理解为什么,只知道必须要这么做才能正常启动Timer。但是现在我们可以知道原因了。其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop。同时我们也应该知道Timer并不是严格的按照设定的时间点来触发的,RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行。(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)

注意:GCD的timer与NStimer不是一个东西。他俩中只有NSTimer是与RunLoop相关的。
但是凡事都有例外,似乎scheduedTimerWith开头的方法创建的NSTimer就不需要添加到RunLoop中就可以运行。事实上,这一系列方法的真实逻辑是,创建一个定时器并自动添加到当前线程RunLoop的NSDefaultRunLoopMode中。在声明一次,不添加到RunLoop中的NSTimer是无法正常工作的

使用NSTimer遇到的坑

不管是跟着网上说的[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];后发现Timer能正常使用,还是自己摸索发现直接用scheduedTimerWith创建的Timer直接就生效。用Timer的时间长了总有一天突然发现,为啥我的Timer运行的好好的突然就时好时坏了。于是找半天原因,发现是在进行Scrollview的滚动操作时Timer不进行响应,滑动结束后timer又恢复正常了。发现现象了但是,为啥啊?抓半天头发然后网上搜资料,然后我们就发现又回到了RunLoop的Mode这个点上。以前我们不懂为什么,现在对RunLoop有一定了解了,我们不妨来分析一下以便加深理解。

  • 在之前讲Mode的时候提到过,RunLoop每次只能运行在一个Mode下,其意义是让不同Mode中的item互不影响。
  • NSTimer是一个Timer源(item),在上面哪个例子中不管是[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];还是scheduedTimerWith我们都是把Timer加到了主线程RunLoop的NSDefaultRunLoopMode中。一般情况下主线程RunLoop就运行在NSDefaultRunLoopMode下,所以定时器正常运行。
  • 当Scrollview开始滑动时,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了UITrackingRunLoopMode。所以现在RunLoop要处理的就是UITrackingRunLoopMode中item。
  • 我们的timer是添加在NSDefaultRunLoopMode中的,并没有添加到UITrackingRunLoopMode中。即我们的timer不是UITrackingRunLoopMode中的item。
  • 本着不同Mode中的item互不影响的原则,RunLoop也就不会处理非当前Mode的item,所以定时器就不会响应。
  • 当Scrollview滑动结束,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了NSDefaultRunLoopMode。我们的Timer是NSDefaultRunLoopMode的item,所以RunLoop会处理它,所以又正常响应了。
  • 如果想Timer在两种Mode中都得到响应怎么办?前面提到过,一个item可以被同时加入多个mode。让Timer同时成为两种Mode的item就可以了(分别添加或者直接加到commonMode中),这样不管RunLoop处于什么Mode,timer都是当前Mode的item,都会得到处理。

更详细的内容请看 iOS RunLoop入门小结
上一篇 Block 实现高阶函数(map filter)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336

推荐阅读更多精彩内容