[iOS] 初识Runloop

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

推荐阅读更多精彩内容