RunLoop机制

一直想写关于RunLoop的文章,但是发现要完全搞明白实在是太难了。RunLoop好多设计到了内核。现在只能说一说RunLoop的表层了。
首先RunLoop是跟线程相关的。可以理解为辅助线程执行任务的工具。
如果没有RunLoop线程的执行顺序就是任务执行完就销毁了。如果有了RunLoop就可以调用内核 可以让线程挂起 休眠。节省CPU资源,下次有任务的时候再唤醒,节省线程开辟的消耗。如果你是单一的任务 那么你就不用关系RunLoop了。但是如果你是重复 并且是间断执行的时候,就可以使用RunLoop了。
先说下大概的使用场景

RunLoop应用

1.NSTimer CADisplayLink 定时器

//在主线程创建的定时器
 NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(calculate) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//当控制器里有scrollView滚动的时候 主线程的runloop会切换到UITrackingRunLoopMode 模式下执行runLoop 所以在原来NSDefaultRunLoopMode事件下的定时器就不走了

解决办法就是
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

//还有如果是在子线程添加的控制器 子线程的runLoop是默认没有的。
只有调用[NSRunLoop currentRunLoop] 系统才会去创建对应子线程的runLoop ,只创建了还是没有用的 还得调用runLoop的 run发法让子线程的runLoop跑起来
这样加进来的定时器才能够执行

2.performSelector 的使用

大家在调用performSelector方法的时候 可以点击option看看苹果的注释。
这个方法是跟RunLoop有关的
默认是执行在当前线程的NSDefaultRunLoopMode 模式下

 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(creatThread) object:nil];
    _thread = thread;
    [thread start];

- (void)creatThread {
    //创建线程的时候最好加上 自动释放池
    @autoreleasepool{
        NSPort *port = [[NSPort alloc] init];
        //为了保证runLoop不退出 需要有事件源  这个port就是占位的
        [[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
        //开启运行循环  如果不开启的话 后期再这个线程是没法执行任务
        [[NSRunLoop currentRunLoop] run];
    }
}

 [self performSelector:@selector(calculate) onThread:self.thread withObject:nil waitUntilDone:NO];

3.常驻线程

跟第2条一样 可以常驻线程 用来做一些频繁的任务  比如日志的收集上传  耗时操作的计算 等
运用的经典案例 AFNetworking2.0中的 
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (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)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

希望我们以后也可以用到常驻线程

4.autoreleaspool 相关

自动释放池跟runloop也有关系
在第一次创建runloop的时候会创建自动释放池
在runloop休眠的时候会销毁上次创建的自动释放池
在被唤醒的时候会创建新的自动释放池
已达到无用对象的快速释放 减少内存

说了这么多应用 我们看看他它的源码吧

RunLoop大概构造
NSRunLoop 和 CFRunLoopRef是一一对应的 只不过是不同框架下的
NSRunLoop是基于CFRunLoopRef的的一层OC封装,所以了解RunLoop的内部结构,就需要研究CFRunLoopRef(core Foundation框架)

RunLoop的相关类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

CFRunLoopModeRef代表RunLoop的运行模式
每个runLoop跟线程是一对一的
1.一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
2.每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode.
3. 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
4. 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响

CFRunLoopSourceRef 代表事件 Source有两个版本:Source0 和 Source1。
Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};

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

RunLoop的Mode
UIInitializationRunLoopMode 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
GSEventReceiveRunLoopMode接受系统事件的内部 Mode,通常用不到。
kCFRunLoopDefaultModeApp的默认 Mode,通常主线程是在这个 Mode 下运行的。
UITrackingRunLoopMode界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
kCFRunLoopCommonModes这是一个占位的 Mode,没有实际作用


关系图如下


image.png

image.png

苹果官方文档
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-131281
CFRunLoop源码地址
http://opensource.apple.com/tarballs/CF/

#######接下来我们看下源码

/// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// 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;
            }
            
            /// 通知 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);
            
            /// 收到消息,处理消息。
            handle_msg:
 
            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.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);
    }
    
    /// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

相关逻辑图


image.png
image.png
demo

//demo中有 起死回升术
// 切记 崩溃的时候不支持xcode调试 要不你就会以为只能起死回生一次呢
//还可以用来检测主线程的卡顿等
https://github.com/rjb0514/RunLoop.git

参考blog
//深入理解RunLoop YYKit的作者 https://blog.ibireme.com/2015/05/18/runloop/
iOS—RunLoop深度剖析 https://www.jianshu.com/p/de2716807570
//sunnyxx线下分享RunLoop
https://v.youku.com/v_show/id_XODgxODkzODI0.html

你可能感兴趣的:(RunLoop机制)