[iOS] 初识Runloop

作者: 木小易Ying | 来源:发表于2019-09-20 21:35 被阅读0次

    最近想写的topic太多了,但好多是和view相关的,所以吧就先谈一下Runloop了~
    源码链接:https://opensource.apple.com/tarballs/CF/

    1. RunLoop是什么?

    RunLoop顾名思义就是一个不停止的循环,不断地重复:
    休眠->有事件需要处理->处理事件->休眠

    我们的app就是基于RunLoop来生存的,当app启动会自动创建主线程以及它所对应的RunLoop,然后RunLoop会一直跑着,适时响应我们的事件,例如触摸、timer之类的。

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            NSLog(@"app start");
            int code = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
            NSLog(@"app stop");
            return code;
        }
    }
    
    输出:
    app start
    

    如果我们把main函数改成上面那个样子,它并不会执行到app stop,因为UIApplicationMain会开启一个RunLoop并且一直死循环,不会结束,所以后面的就都不会执行了。

    源码里面的CFRunLoopRun可看出当RunLoop不是stop或finish状态的时候,就会一直执行CFRunLoopRunSpecific和CHECK_FOR_FORK,的确是一个死循环。

    void CFRunLoopRun(void) {   /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    

    2. 处理哪些事件

    • 定时器(Timer)、PerformSelector
    • GCD Async Mian Queue (注意只有main queue哦
    • 事件响应、手势识别、界面刷新
    • 网络请求
    • AutoreleasePool

    3. RunLoop结构

    我们先从RunLoop是神马开始看起:

    struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;          /* locked for accessing mode list */
        __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread;
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    

    它里面的Mode非常重要:

    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    

    每个RunLoop可以有很多个Mode,然后在各个mode间转换,每个mode有:

    struct __CFRunLoopMode {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
        CFStringRef _name;
        Boolean _stopped;
        char _padding[3];
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        CFMutableDictionaryRef _portToV1SourceMap;
        __CFPortSet _portSet;
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        mach_port_t _timerPort;
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR */
        uint64_t _timerHardDeadline; /* TSR */
    };
    

    其中比较重要的是Mode包含和很多个set,

    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    

    RunLoop只处理当前Mode里面的timer、source、observer触发的事件,其他Mode注册的timer之类的会暂停不响应。

    RunLoop的Mode示意图

    Mode组成:

    • Source1 : 基于Port的线程间通信
    • Source0 : 触摸事件,PerformSelectors
    • Timers : 定时器,NSTimer
    • Observer : 监听器,用于监听RunLoop的状态
    ①Source1: 基于Port的线程通信
    @interface TestRunLoop()<NSPortDelegate>
    
    @end
    
    @implementation TestRunLoop
    
    - (instancetype)init {
        self = [super init];
        if (self) {
            [self setup];
        }
        return self;
    }
    
    - (void)setup {
        [self test1];
    }
    
    - (void)test1 {
        //声明两个端口
        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
            NSLog(@"Thread: %@", [NSThread currentThread]);
            [[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];
            
        });
        
    }
    
    #pragma mark - NSPortDelegate
    //这个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);
        
        NSLog(@"Stack:%@", [NSThread callStackSymbols]);
    }
    
    @end
    
    输出:
    Thread: <NSThread: 0x600000c13b40>{number = 5, name = (null)}
    收到消息了,线程为:<NSThread: 0x600000c13b40>{number = 5, name = (null)}
    hello
    Stack:(
        0   Example1                            0x0000000109e46f53 -[TestRunLoop handlePortMessage:] + 275
        1   Foundation                          0x000000010a1f4390 __NSFireMachPort + 253
        2   CoreFoundation                      0x000000010b0c7096 __CFMachPortPerform + 150
        3   CoreFoundation                      0x000000010b0f3419 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 41
        4   CoreFoundation                      0x000000010b0f2a7b __CFRunLoopDoSource1 + 459
        5   CoreFoundation                      0x000000010b0ed00a __CFRunLoopRun + 2490
        6   CoreFoundation                      0x000000010b0ec302 CFRunLoopRunSpecific + 626
        7   Foundation                          0x000000010a1fa044 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 277
        8   Example1                            0x0000000109e46b37 __20-[TestRunLoop test1]_block_invoke + 295
        9   libdispatch.dylib                   0x000000010c9f4d7f _dispatch_call_block_and_release + 12
        10  libdispatch.dylib                   0x000000010c9f5db5 _dispatch_client_callout + 8
        11  libdispatch.dylib                   0x000000010c9f87b9 _dispatch_queue_override_invoke + 1022
        12  libdispatch.dylib                   0x000000010ca06632 _dispatch_root_queue_drain + 351
        13  libdispatch.dylib                   0x000000010ca06fca _dispatch_worker_thread2 + 130
        14  libsystem_pthread.dylib             0x000000010cdde6b3 _pthread_wqthread + 583
        15  libsystem_pthread.dylib             0x000000010cdde3fd start_wqthread + 13
    )
    

    通过栈可以看出来__CFRunLoopDoSource1会接收Port的消息并对他进行处理。

    ② Source0: 触摸事件,PerformSelectors

    重写页面的touchBegan,可以看到是由__CFRunLoopDoSources0来处理的。

    - (void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"Stack:%@", [NSThread callStackSymbols]);
    }
    
    Stack:(
        0   Example1                            0x00000001009601fd -[MainViewController touchesBegan:withEvent:] + 93
        1   UIKitCore                           0x0000000104b0ba09 forwardTouchMethod + 353
        2   UIKitCore                           0x0000000104b0b897 -[UIResponder touchesBegan:withEvent:] + 49
        3   UIKitCore                           0x0000000104b1ac48 -[UIWindow _sendTouchesForEvent:] + 1869
        4   UIKitCore                           0x0000000104b1c5d2 -[UIWindow sendEvent:] + 4079
        5   UIKitCore                           0x0000000104afad16 -[UIApplication sendEvent:] + 356
        6   UIKit                               0x000000011dd184af -[UIApplicationAccessibility sendEvent:] + 85
        7   UIKitCore                           0x0000000104bcb293 __dispatchPreprocessedEventFromEventQueue + 3232
        8   UIKitCore                           0x0000000104bcdbb9 __handleEventQueueInternal + 5911
        9   CoreFoundation                      0x00000001012d6be1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
        10  CoreFoundation                      0x00000001012d6463 __CFRunLoopDoSources0 + 243
        11  CoreFoundation                      0x00000001012d0b1f __CFRunLoopRun + 1231
        12  CoreFoundation                      0x00000001012d0302 CFRunLoopRunSpecific + 626
        13  GraphicsServices                    0x000000010a2622fe GSEventRunModal + 65
        14  UIKitCore                           0x0000000104ae0ba2 UIApplicationMain + 140
        15  Example1                            0x000000010095f9a0 main + 112
        16  libdyld.dylib                       0x0000000103658541 start + 1
        17  ???                                 0x0000000000000001 0x0 + 1
    )
    
    ③ Timer: 定时器

    用定时器看一下调用栈是由__CFRunLoopDoTimer调用的block~

    - (void)testTimer {
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
            NSLog(@"Stack:%@", [NSThread callStackSymbols]);
        }];
    }
    
    Stack:(
        0   Example1                            0x0000000105c4b3bc __24-[TestRunLoop testTimer]_block_invoke + 92
        1   Foundation                          0x0000000106627135 __NSFireTimer + 83
        2   CoreFoundation                      0x00000001074f93e4 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
        3   CoreFoundation                      0x00000001074f8ff2 __CFRunLoopDoTimer + 1026
        4   CoreFoundation                      0x00000001074f885a __CFRunLoopDoTimers + 266
        5   CoreFoundation                      0x00000001074f2efc __CFRunLoopRun + 2220
        6   CoreFoundation                      0x00000001074f2302 CFRunLoopRunSpecific + 626
        7   GraphicsServices                    0x000000010f54e2fe GSEventRunModal + 65
        8   UIKitCore                           0x0000000109dccba2 UIApplicationMain + 140
        9   Example1                            0x0000000105c4a690 main + 112
        10  libdyld.dylib                       0x0000000108944541 start + 1
        11  ???                                 0x0000000000000001 0x0 + 1
    )
    
    ④ Observer: 用于监听RunLoop的状态
    - (void)testObserver {
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"RunLoop进入");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"RunLoop要处理Timers了");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"RunLoop要处理Sources了");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"RunLoop要休息了");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"RunLoop醒来了");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"RunLoop退出了");
                    break;
                    
                default:
                    break;
            }
            
            NSLog(@"Stack:%@", [NSThread callStackSymbols]);
        });
        
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    }
    
    输出截取stack部分:
    Stack:(
        0   Example1                            0x0000000108a97144 __27-[TestRunLoop testObserver]_block_invoke + 1076
        1   CoreFoundation                      0x0000000109d430f7 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
        2   CoreFoundation                      0x0000000109d3d5be __CFRunLoopDoObservers + 430
        3   CoreFoundation                      0x0000000109d3de69 __CFRunLoopRun + 2073
        4   CoreFoundation                      0x0000000109d3d302 CFRunLoopRunSpecific + 626
        5   GraphicsServices                    0x000000011239c2fe GSEventRunModal + 65
        6   UIKitCore                           0x000000010e0e4ba2 UIApplicationMain + 140
        7   Example1                            0x0000000108a96040 main + 112
        8   libdyld.dylib                       0x000000010b6bb541 start + 1
        9   ???                                 0x0000000000000001 0x0 + 1
    )
    

    因为Observer回调太多啦所以就只截取一个Stack片段,但还是可以看出来是由__CFRunLoopDoObservers来处理Observer事件滴。

    题外话:
    上篇GCD源码分析的时候我们看dispatch_async的调用堆栈是没有RunLoop的身影,那个是因为用的自己创建的并行queue,如果是dispatch给main queue是不同的哦,这个是会出现RunLoop滴。

    dispatch_async(dispatch_get_main_queue(), ^{
      NSLog(@"Stack:%@", [NSThread callStackSymbols]);
    });
    
    输出:
    Stack:(
        0   Example1                            0x000000010b952a90 __20-[TestRunLoop setup]_block_invoke + 64
        1   libdispatch.dylib                   0x000000010e502d7f _dispatch_call_block_and_release + 12
        2   libdispatch.dylib                   0x000000010e503db5 _dispatch_client_callout + 8
        3   libdispatch.dylib                   0x000000010e511080 _dispatch_main_queue_callback_4CF + 1540
        4   CoreFoundation                      0x000000010cc008a9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
        5   CoreFoundation                      0x000000010cbfaf56 __CFRunLoopRun + 2310
        6   CoreFoundation                      0x000000010cbfa302 CFRunLoopRunSpecific + 626
        7   GraphicsServices                    0x00000001152652fe GSEventRunModal + 65
        8   UIKitCore                           0x000000010fa07ba2 UIApplicationMain + 140
        9   Example1                            0x000000010b951dd0 main + 112
        10  libdyld.dylib                       0x000000010e578541 start + 1
        11  ???                                 0x0000000000000001 0x0 + 1
    )
    

    4. RunLoop与线程

    每个线程都对应一个RunLoop,我们是不能创建的,只能通过下面的两种方法获取当前线程的RunLoop:
    CFRunLoopGetMain() //获取主线程的RunLoop
    CFRunLoopGetCurrent() //获取当前线程的RunLoop
    or
    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

    我们来看一下源码里面是怎么get滴~

    CFRunLoopRef CFRunLoopGetMain(void) {
        CHECK_FOR_FORK();
        static CFRunLoopRef __main = NULL; // no retain needed
        if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
        return __main;
    }
    
    CFRunLoopRef CFRunLoopGetCurrent(void) {
        CHECK_FOR_FORK();
        CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
        if (rl) return rl;
        return _CFRunLoopGet0(pthread_self());
    }
    
    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
        ……
        //从字典里找thread为key的value是否存在
        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        __CFUnlock(&loopsLock);
    
        //如果不存在则创建新的RunLoop并放入字典
        if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
            // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
            __CFUnlock(&loopsLock);
        CFRelease(newLoop);
        }
        ……
        return loop;
    }
    

    可以看出来每个线程所对应的的RunLoop是存在字典里面的,thread作为key,RunLoop作为value。

    主线程的RunLoop会自动创建,子线程不会自动创建RunLoop,除非主动调用CFRunLoopGetCurrent。

    我们先尝试run一下面的代码,会发现木有任何log,因为子线程不会自动创建RunLoop,所以没有用于处理Timer的RunLoop,导致timer不会执行(这也是GCD source和timer不太一样的地方)。

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
            NSLog(@"Thread:%@", [NSThread currentThread]);
            NSLog(@"Stack:%@", [NSThread callStackSymbols]);
        }];
    });
    

    我们自己主动在thread中创建一个timer,控制台就会有log输出啦~ (太多了Log就不粘过来了)

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
            NSLog(@"Thread:%@", [NSThread currentThread]);
            NSLog(@"Stack:%@", [NSThread callStackSymbols]);
        }];
        
        // 主动开启RunLoop
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    });
    

    线程如果没有RunLoop,那么当任务执行完毕以后他就会被销毁,但是如果有RunLoop,就会一直活着,直到RunLoop退出。

    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"test"];
        
    [thread1 start];
    
    __weak typeof(NSThread)*weakThread = thread1;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"thread1: %@", weakThread);
    });
    
    - (void)run:(NSString *)string {
        NSLog(@"task executed thread:%@", [NSThread currentThread]);
    }
    
    输出:
    task executed thread:<NSThread: 0x60000305d900>{number = 5, name = (null)}
    thread1: (null)
    

    如果没有强引用,一般thread在执行完任务以后就会被销毁,如果想让thread仍旧活着,可以通过增加RunLoop的方法,只要线程对应的RunLoop在跑着,thread就不会被回收。

    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"test"];
        
    [thread1 start];
    
    __weak typeof(NSThread)*weakThread = thread1;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"thread1: %@", weakThread);
    });
    
    - (void)run:(NSString *)string {
        NSLog(@"task executed thread:%@", [NSThread currentThread]);
        
        // 启动RunLoop并添加port监听防止RunLoop自己退出
        NSPort *threadPort = [NSMachPort port];
        [[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    
    输出:
    task executed thread:<NSThread: 0x6000035f9b00>{number = 5, name = (null)}
    thread1: <NSThread: 0x6000035f9b00>{number = 5, name = main}
    

    这一次的thread在即使没有强引用的情况下,由于RunLoop的运行也没有被销毁,如果想要销毁这个thread,必须停止RunLoop。

    那么要如何停止RunLoop呢?
    方式一共有3种:

    • 移除掉runloop中的所有事件源(timer和source)
    • 设置一个超时时间 (run起来RunLoop时设置)
    • 只要CFRunloop运行起来就可以用CFRunLoopStop()停止

    总结一下如果想让线程一直活着,就得让他的RunLoop一直跑着,并且Mode不能为空哦,否则就会自动退出RunLoop哦。(注意只有Observer是不能保活RunLoop滴,也会被判断为空

    5. RunLoop工作流程

    从stack我们可以看出来每次处理其实都是在__CFRunLoopRun方法内做的:

    void CFRunLoopRun(void) {   /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    
    SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
        ……
        // 如果mode是空就return,直接stop当前的RunLoop
        CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
        if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
        }
    
    ……
            // 通知Observers在entry和exit的时候
        if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
        if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    ……
    }
    
    // 精简后的 __CFRunLoopRun函数,保留了主要代码
    static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
        int32_t retVal = 0;
        do {
            // 通知Observers:即将处理Timers
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); 
            
            // 通知Observers:即将处理Sources
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
            
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            // 处理Sources0
            if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
                // 处理Blocks
                __CFRunLoopDoBlocks(rl, rlm);
            }
            
            // 如果有Sources1,就跳转到handle_msg标记处
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
            
            // 通知Observers:即将休眠
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            
            // 进入休眠,等待其他消息唤醒
            __CFRunLoopSetSleeping(rl);
            __CFPortSetInsert(dispatchPort, waitSet);
            do {
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            } while (1);
            
            // 醒来
            __CFPortSetRemove(dispatchPort, waitSet);
            __CFRunLoopUnsetSleeping(rl);
            
            // 通知Observers:已经唤醒
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
            
        handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
            if (被Timer唤醒的) {
                // 处理Timer
                __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
            }
            else if (被GCD唤醒的) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } else { // 被Sources1唤醒的
                __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
            }
            
            // 执行Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            // 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
            if (sourceHandledThisLoop && stopAfterHandle) {
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(rl)) {
                __CFRunLoopUnsetStopped(rl);
                retVal = kCFRunLoopRunStopped;
            } else if (rlm->_stopped) {
                rlm->_stopped = false;
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
                retVal = kCFRunLoopRunFinished;
            }
            
        } while (0 == retVal);
        
        return retVal;
    }
    

    根据源码很多的流程图都是下面酱紫的,但是在Reference里面关于误解的一篇有纠正“是否存在Source1”的判断其实是有问题的,以及唤醒的时候也是其实等待的是Source1,具体可以看作者的验证哈。

    RunLoop运行流程

    每个RunLoop只能运行一个Mode,系统提供5种Mode:

    • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
    • UITrackingRunLoopMode:界面跟踪Mode,所有UI交互事件,例如用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
    • UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到
    • kCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode,是一种模式集合。将一个input source关联到这个模式集合上,等于将input source关联到这个模式集合中的所有模式上。在iOS系统中NSRunLoopCommonMode包含NSDefaultRunLoopMode、UITrackingRunLoopMode。

    通过CFRunLoopAddCommonMode可以将某个mode加到CommonModes里面。
    其实这个Mode并不是一个真正的Mode,只是用于当你需要一个Timer/Source…在多个Mode下都可以被触发,就将它加入这个CommonMode,并确保你的多个Mode在CommonMode之中就OK啦。

    由于每个时间点RunLoop只能运行在一种Mode下,是不可能将它设置为这个CommonMode的,CommonMode存在的意义就是让RunLoop运行在其他Mode时都可以触发规定的Source,它自身并没有什么东西所以只是一个占位Mode,RunLoop不会真的运行在这个Mode里面。

    实现原理摘抄:
    一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的"commonModes"中)。RunLoop 会自动将 _commonModeItems 加入到具有 "Common" 标记的所有Mode里。
    例如,当将Timer加入到顶层的 RunLoop 的 "commonModeItems"中时,"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。

    • 自定义Mode:可以设置自定义的运行模式Mode,可以用CFRunLoopAddCommonMode添加到NSRunLoopCommonModes中

    其实在我们触发界面滑动的时候,会发现timer之类的是不工作的,就是因为RunLoop切换到了UITrackingRunLoopMode,于是kCFRunLoopDefaultMode里面的timer就不能被触发了。

    我们来测试一下UI交互过程中会切换mode这个事儿~

    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
      NSLog(@"timer arrive");
    }];
    
    然后在viewController里面加一个textView,如果滑动textView过程中会发现timer并不会打印log。
    

    如果我希望Timer不要被UI交互打断要怎么办呢?
    A: 可以将它加入到CommonMode而不是DefaultMode

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
      NSLog(@"timer arrive");
    }];
        
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    把代码改成上面酱紫即使在滑动TextView过程中,timer的日志也会不断地打印滴~


    如果需要切换Mode,只能退出RunLoop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source、Timer、Observer,让其互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
    (系统run起来一个RunLoop可以指定的Mode枚举值只有Default和Commons所以也木有好的切换案例。。。)

    我们只能看一下RunLoop的几种启动方法了:

    • (void)run; //进入处理事件循环,如果没有事件则立刻返回。
      注意:主线程上调用这个方法会导致无法返回(进入无限循环,虽然不会阻塞主线程),因为主线程一般总是会有事件处理。

    • (void)runUntilDate:(NSDate *)limitDate; //同run方法,增加超时参数limitDate,避免进入无限循环。
      使用在UI线程(亦即主线程)上,可以达到暂停的效果,因为UI线程始终有事件处理,所以在limitDate时会返回继续之后的代码执行。

    • (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; //等待消息处理,好比在PC终端窗口上等待键盘输入。一旦有合适事件被处理了,则立刻返回;类同run方法,如果没有事件处理也立刻返回;有否事件处理由返回布尔值判断。同样limitDate为超时参数。

    • (void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate; //似乎和runMode:差不多(测试过是这种结果,但确定是否有其它特殊情况下的不同),没有BOOL返回值。

    官网文档也提到run和runUntilDate:会以NSDefaultRunLoopMode参数调用runMode:来处理事件。

    这里尝试一下几种方式的区别:

    - (void)test1 {
        //声明两个端口
        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] runUntilDate:[NSDate distantFuture]];
    //        [[NSRunLoop currentRunLoop] run];
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            
            NSLog(@"continue");
        });
        
        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];
        });
    }
    
    #pragma mark - NSPortDelegate
    //这个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);
    }
    

    三种方式的输出是不一样滴:

    // runMode:
    收到消息了,线程为:<NSThread: 0x600003a16f40>{number = 5, name = (null)}
    hello
    continue
    
    // runUntil:
    收到消息了,线程为:<NSThread: 0x600002485800>{number = 5, name = (null)}
    hello
    
    // run:
    收到消息了,线程为:<NSThread: 0x60000042d300>{number = 5, name = (null)}
    hello
    

    也就是runUntil和run会循环处理事件,runUntil是有超时时间的,但是run是木有滴;而runMode只要处理一次事件就返回了,不会一直循环处理。

    6. RunLoop生命周期

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),
        kCFRunLoopBeforeTimers = (1UL << 1),
        kCFRunLoopBeforeSources = (1UL << 2),
        kCFRunLoopBeforeWaiting = (1UL << 5),
        kCFRunLoopAfterWaiting = (1UL << 6),
        kCFRunLoopExit = (1UL << 7),
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    
    • kCFRunLoopEntry:
      每次RunLoop重新进入时的activity,RunLoop每一次进入一个Mode,就通知一次外部 kCFRunLoopEntry,之后会一直以该Mode运行,如果切换到其他Mode,会再次通知 kCFRunLoopEntry。

    • kCFRunLoopBeforeTimers:
      这个在工作流程分析的时候已经看到过了,就是在处理timer和source之前发出的通知
      __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
      注意虽然可能没有timer,但是这个通知是肯定每次都会发的,相当于是准备处理的通知

    • kCFRunLoopBeforeSources:
      同上~ 在调用__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); 之后会马上调用的__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

    • kCFRunLoopBeforeWaiting:
      这个activity表示当前线程即将可能进入睡眠,如果能够从内核队列上读出msg则继续运行任务,如果当前队列上没多余消息,则进入睡眠状态。

    • kCFRunLoopAfterWaiting:
      这个activity是当前线程从睡眠状态中收到消息恢复执行任务的通知。每一次RunLoop从睡眠状态中恢复必调的一个activity。

    • kCFRunLoopExit:
      切换Mode的时候会调用到这个activity,即RunLoop退出了。

    • kCFRunLoopAllActivities:
      监听所有状态


    在加了前面章节的Observer以后,如果执行CFRunLoopStop(CFRunLoopGetCurrent()),则会看到下面的log,如果不执行就Stop就不会看到前面的两个退出和进入的log:

    RunLoop退出了
    RunLoop进入
    RunLoop要处理Timers了
    RunLoop要处理Sources了
    RunLoop要处理Timers了
    RunLoop要处理Sources了
    ……
    RunLoop要处理Timers了
    RunLoop要处理Sources了
    RunLoop要休息了
    RunLoop醒来了
    RunLoop要处理Timers了
    RunLoop要处理Sources了
    RunLoop要处理Timers了
    RunLoop要处理Sources了
    RunLoop要休息了
    

    最后,其实还有很多东西木有提及,比如自动释放池、应用之类的,留一个小小的问题吧~ 也是看reference的时候看到的一个小考题:

     (void)test{
        NSLog(@"任务B");
    }
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSThread *thread = [[NSThread alloc] initWithBlock:^{
            NSLog(@"任务A");
        }];
        [thread start];
        [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
    }
    

    这个的打印结果会是什么嘞?

    答案:
    任务A
    *** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform'
    

    因为start thread以后那个线程执行完任务就被销毁了,所以再给nil对象执行performSelector会crash。

    Reference:

    1. 底层原理:https://www.jianshu.com/p/de752066d0ad
    2. 浅谈:https://www.jianshu.com/p/41d037f535c5
    3. 误解:https://www.jianshu.com/p/ae0118f968bf
    4. 源码及应用:https://www.jianshu.com/p/d6c5c0bf97fd
    5. port通信:https://blog.csdn.net/sharpyl/article/details/61016634
    6. 应用:https://www.jianshu.com/p/e29f846d8a97

    相关文章

      网友评论

        本文标题:[iOS] 初识Runloop

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