Runloop的应用与深入理解

作者: Tamp_ | 来源:发表于2017-09-15 22:47 被阅读266次

    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的建议花点时间仔细看看,另外本人的表述能力也确实有限,有什么表述不清或者错误的地方希望大家多多指点。

    相关文章

      网友评论

        本文标题:Runloop的应用与深入理解

        本文链接:https://www.haomeiwen.com/subject/tbzssxtx.html