Runloop的应用与深入理解

RunLoop的概念

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,这就是runloop做的事。在iOS应用中,程序就是运行在一个主线程的runloop中,所以应用才会一直不断的运行。

一、RunLoop的基础知识

RunLoop的结构

RunLoop_0.png

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
CFRunLoopMode 和 CFRunLoop 的结构大致如下:

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<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
};

这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
如:
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。因为当你滑动时TrackingRunLoopMode追踪的事件加入到_commonModeItems,而RunLoop又会将该事件同步到具有 “Common” 标记的所有Mode(kCFRunLoopDefaultMode)里,所以此时响应了追踪事件而放弃了Timer的调用。

RunLoop与线程的关系

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:(不想看代码的话可以跳过,记住下面的结论就好)

/// 全局的Dictionary,key 是 pthread_t, 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的基本了解,我们再来具体看看runloop相关的函数

1、Runloop相关函数

RunLoop运行接口:

  • NSRunLoop的运行接口:
//运行 NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制
- (void)run;
//运行 NSRunLoop: 参数为运时间期限,运行模式为默认的NSDefaultRunLoopMode模式 
- (void)runUntilDate:(NSDate *)limitDate;
//运行 NSRunLoop: 参数为运行模式、时间期限,返回值为YES表示是处理事件后返回的,NO表示是超时或者停止运行导致返回的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
  • CFRunLoopRef的运行接口:
//运行 CFRunLoopRef
void CFRunLoopRun();
//运行 CFRunLoopRef: 参数为运行模式、时间和是否在处理Input Source后退出标志,返回值是exit原因
SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);
//停止运行 CFRunLoopRef
void CFRunLoopStop( CFRunLoopRef rl );
//唤醒CFRunLoopRef
void CFRunLoopWakeUp ( CFRunLoopRef rl );

先来讲讲NSRunLoop的这三个函数:

1、

- (void)run; 无条件运行

这个函数一般是不建议使用的,因为它会导致RunLoop永久的运行在NSDefaultRunLoopMode模式,即使CFRunLoopStop(runloopRef)也无法停止runloop的运行,除非能移除这个runloop上的所有事件源,包括定时器和source事件,不然这个子线程就无法停止,只能永久运行下去。
2、

- (void)runUntilDate:(NSDate *)limitDate;  有一个超时时间限制

有个超时时间,可以控制每次runloop的运行时间,也是运行在NSDefaultRunLoopMode模式。但是CFRunLoopStop(runloopRef)也无法停止runloop的运行。
3、

//有一个超时时间限制,而且设置运行模式
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

这里不仅可以设置超时时间,还有可以设置运行模式。这种运行方式可以被CFRunLoopStop(runloopRef)所停止。
查看 run 方法的文档还可以知道,它的本质就是无限调用 runMode:beforeDate: 方法,同样地,runUntilDate: 也会重复调用 runMode:beforeDate:,区别在于它超时后就不会再调用。
总结来说,runMode:beforeDate: 表示的是 runloop 的单次调用,另外两者则是循环调用。

这里有一点东西需要讲解(下面分割线里的内容),涉及到runloop的一些源码知识,如果没有精力的可以跳过不看,想深入探讨的可以看一看


这里第三种运行方式除了limitDate超时和CFRunLoopStop(runloopRef)能够使runloop退出外,其它事件也可以导致runloop退出,举个例子:

- (void)test
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"runloop线程开始");
        //获取到当前线程
        self.thread = [NSThread currentThread];
        NSRunLoop *runloop = [NSRunLoop currentRunLoop];
        
        //添加一个Port,为了防止runloop没事干直接退出
        [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
       
         //运行一个runloop
        [runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

        NSLog(@"runloop线程结束");

    });

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //在我们开启的异步线程调用方法,该方法在runloop的线程中执行
        [self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
    });
}

- (void)run
{
    NSLog(@"run in runloop thread");
}

输出如下:

2017-09-15 20:21:23.386 test[5829:32394503] runloop线程开始
2017-09-15 20:21:25.387 test[5829:32394503] run in runloop thread
2017-09-15 20:21:25.388 test[5829:32394503] runloop线程结束

可以看到,runloop收到一个“run”的消息,然后runloop处理完这个消息后就退出了,为什么会这样呢?看看runloop的源代码就知道了

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

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

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

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

        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {

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

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

            /// 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. 调用 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. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

            /// 9.收到消息,处理消息。
            handle_msg:

            /// 10.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 

            /// 10.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 

            /// 10.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);


            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }

            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
        } while (retVal == 0);
    }

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

RunLoop的运行逻辑:

RunLoop_1.png

上面的代码逻辑注释中已经解释得很清楚了,我就不再多说。可以知道我上面的例子中runloop就是收到了一个source1的消息,所以直接从5跳转到了9。然后10.3处理这个Source1处理完后会吧sourceHandledThisLoop这个变量赋值为true,注意后面判断是否退出runloopif (sourceHandledThisLoop && stopAfterHandle) { /// 进入loop时参数说处理完事件就返回。 retVal = kCFRunLoopRunHandledSource; }满足了sourceHandledThisLoop为true,还有一个变量stopAfterHandle就是在- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;运行runloop的时候已经将它设置为true了,所以到这里runloop就退出了。
OK,分析到这我们就明确知道runloop退出的条件了。


再看看CFRunLoopRef这里的几个函数:
1、

void CFRunLoopRun();

运行在默认的kCFRunLoopDefaultMode模式下,直到使用CFRunLoopStop接口停止这个Run Loop,或者Run Loop的所有事件源都被删除。
2、

SInt32 CFRunLoopRunInMode (mode, seconds, returnAfterSourceHandled);

根据前面的经验看参数名就知道这些参数的作用,这里说说最后一个参数和返回值。前面分割线的内容我们已经分析了runloop的源码,其实- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;这个函数就是根据这个函数实现的,这就是为什么它可以通过CFRunLoopStop来停止runloop的原因,第三个参数就是设置源码中returnAfterSourceHandled参数的,- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;这个函数默认就是把它设置成了YES。这个返回值也就是前面源码中retVal变量返回的值。有返回值就说明runloop已经结束运行。

OK,以上就是使runloop运行的全部方法。

二、RunLoop的应用

通过Runloop使线程保活:

以AF2.x的源代码为例:

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
} 

这是最runloop最基础的一个应用。这里创建了一个线程,取名为AFNetworking,然后添加了一个runloop,所以这个线程不会被销毁,直到runloop停止。

基于runloop的线程通信:

  • 1、Cocoa 执行 Selector 的源:
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray<NSString *> *)#>]

[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>]
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#> modes:<#(nullable NSArray<NSString *> *)#>]

大概讲一下 waitUntilDone 这个参数,顾名思义,就是是否等到结束。
1)如果这个值设为YES,那么就需要等到这个方法执行完,线程才能继续往下去执行。它会阻塞提交的线程。
2)如果为NO的话,这个调用的方法会异步的实行,不会阻塞提交线程。

  • 2、定时源:
[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>
[NSTimer timerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]

还有使用block的那几个方面。这里记住只有schedule开头的方法默认加到了NSDefaultRunLoopMode模式下,其它方法都需要调用[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]去给它指定一个mode

  • 3、基于端口的输入源:

举个例子

- (void)testDemo
{
    //声明两个端口   随便怎么写创建方法,返回的总是一个NSMachPort实例
    NSMachPort *mainPort = [[NSMachPort alloc]init];
    NSPort *threadPort = [NSMachPort port];
    //设置线程的端口的代理回调为自己
    threadPort.delegate = self;

    //给主线程runloop加一个端口
    [[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        //添加一个Port
        [[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

    });

    NSString *s1 = @"hello";

    NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
        //过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
        //components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
        [threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];

    });

}

//这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
- (void)handlePortMessage:(id)message
{

    NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);

    //只能用KVC的方式取值
    NSArray *array = [message valueForKeyPath:@"components"];

    NSData *data =  array[1];
    NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"%@",s1);

//    NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
//    NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];

}

输出如下:

2017-09-15 22:18:50.235 test[6345:32771911] 收到消息了,线程为:<NSThread: 0x60000007ba80>{number = 3, name = (null)}
2017-09-15 22:18:50.236 test[6345:32771911] hello

可以看到我们从主线程往另一个线程发送了消息。这里有一点要注意就是这里The components array consists of a series of instances
of some subclass of NSData(components array里面传值的类型只能是NSData或者NSPort的子类)。

  • 4、自定义输入源:
栗子

- (void)testDemo4
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSLog(@"starting thread.......");

        _runLoopRef = CFRunLoopGetCurrent();
        //初始化_source_context。
        bzero(&_source_context, sizeof(_source_context));
        //这里创建了一个基于事件的源,绑定了一个函数
        _source_context.perform = fire;
        //参数
        _source_context.info = "hello";
        //创建一个source
        _source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
        //将source添加到当前RunLoop中去
        CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);

        //开启runloop 第三个参数设置为YES,执行完一次事件后返回
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 9999999, YES);

        NSLog(@"end thread.......");
    });


    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        if (CFRunLoopIsWaiting(_runLoopRef)) {
            NSLog(@"RunLoop 正在等待事件输入");
            //添加输入事件
            CFRunLoopSourceSignal(_source);
            //唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
            CFRunLoopWakeUp(_runLoopRef);
        }else {
            NSLog(@"RunLoop 正在处理事件");
            //添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
            CFRunLoopSourceSignal(_source);
        }
    });

}

//此输入源需要处理的后台事件
static void fire(void* info){

    NSLog(@"我现在正在处理后台任务");

    printf("%s",info);
}

输出

2017-09-15 22:39:09.750 test[6384:32816434] starting thread.......
2017-09-15 22:39:11.750 test[6384:32816214] RunLoop 正在等待事件输入
2017-09-15 22:39:11.751 test[6384:32816434] 我现在正在处理后台任务
hello
2017-09-15 22:39:11.752 test[6384:32816434] end thread.......

简单讲解一下。CFRunLoopSourceRef _source这个是自定义输入源中最重要的一个参数。它用来连接runloop与CFRunLoopSourceContext中的一些配置项,注意我们自定义的输入源,必须由我们手动来触发。需要先CFRunLoopSourceSignal(_source);再看当前runloop是否在休眠中,来看是否需要调用CFRunLoopWakeUp(_runLoopRef);(一般都是要调用的)。

以上就是线程间通信的四个方法了。

总结一下知识要点

  • runloop的结构,它是由若干个Mode构成,而每个 Mode 又包含若干个 Source/Timer/Observer。
  • 除了主线程,其它线程的runloop默认是没有开启的,而runloop的创建其实就是发生在你第一次在线程获取runloop中。
  • 运行runloop的那几个函数的区别
  • runloop运行的逻辑
  • runloop线程保活
  • runloop线程通信的四种方式

最后
文章挺长的,如果想要好好了解一下runloop的建议花点时间仔细看看,另外本人的表述能力也确实有限,有什么表述不清或者错误的地方希望大家多多指点。

推荐阅读更多精彩内容