iOS中的RunLoop(内部逻辑及对外接口)

671 阅读6分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

RunLoop 对外的接口

主要针对 Core Foundation中 的 RunLoop 的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:

这里写图片描述

CFRunLoopModeRef

  • CFRunLoopModeRef 代表 RunLoop 的运行模式,正是因为RunLoop 里有SourceTimerObserver,RunLoop 才能一直保持运行,如果没有这些,RunLoop 会直接退出循环

  • 一个 RunLoop 包含了若干个 Mode,每个 Mode 又包含了若干个SourceTimerObserver

  • 每次 RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode

  • 如果需要切换 Mode,只能退出 RunLoop,再重新指定一个 Mode 进入循环

  • 每个 Mode 可以设置自己的 SourceTimerObserver,让其互不影响

  • 系统默认注册了5个 Mode(苹果只开放了前俩个)

    • kCFRunLoopDefaultMode:App的默认 Mode,通常主线程是在这个 Mode 下运行
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollerView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    • UIInitalizaationRunLoopMode:在刚启动App时进入的第一个 Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode:接受系统事件内部的 Mode(绘图渲染等等),通常用不到
    • kCFRunLoopCommonModes:这是占位的 Mode,不是真正的 Mode

CFRunLoopTimerRef

  • CFRunLoopTimerRef 是一个基于时间触发器(简单说就是设置一个时间,时间到了就去执行一个事件)

    eg:在UI上添加一个TextView,滑动TextView,观察timer是否执行

    • 方式一:
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
        //在当前RunLoop中添加timer
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    }
    
    - (void)run{
        NSLog(@"run");
    }
    

    调用scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:方法,返回定时器,系统已自动将 Timer 添加到 RunLoop 中,而且默认 NSDefaultRunLoopMode 模式,这样每隔1s都会执行 run。但是当你滑动 textView 时,定时器将失效,run 方法停止执行,当你停止滑动时,run 方法继续执行。

    当把这个 Timer 添加到当前的 RunLoop 中时,不管你滑不滑动 textView,run 方法都会执行。

    • 方式二:
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; 
    
    //1、只运行在 NSDefaultRunLoopMode 模式中
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    //2、只运行在 UITrackingRunLoopMode 模式中
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    //3、只运行在 NSRunLoopCommonModes 模式中
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    调用timerWithTimeInterval:target:selector:userInfo:repeats:方法,返回定时器,这个定时器只是完成初始化,不会执行定时任务,需要手动将 timer 加入到 Runloop 中。

    1. 只运行在 NSDefaultRunLoopMode 模式中,当你滑动 textView 时,定时器就不会执行定时任务了( run 方法不执行),停止滑动,timer 开始执行定时任务。

    2. 只运行在 UITrackingRunLoopMode 模式中,当你滑动textView时,定时器会执行定时任务( run 方法执行),当你不滑动textView,定时器就不会执行定时任务了( run 方法不执行)

    3. 只运行在 NSRunLoopCommonModes 模式中,不管你滑不滑动 textView,定时器都会执行定时任务( run 方法执行)

CFRunLoopSourceRef

CFRunLoopSourceRef 代表事件的输入源。按照函数调用栈,Source 的有俩个类型

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

比如一个点击事件,首先它会先包装成一个 event,之后传到 source1,source1 再分发到 source0,由 source0 处理

CFRunLoopObserverRef

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

可以监听的时间点有以下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          //即将进入Runloop
    kCFRunLoopBeforeTimers = (1UL << 1),   //即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2),  //即将处理Source事件
    kCFRunLoopBeforeWaiting = (1UL << 5),  //即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),   //从刚才休眠中唤醒
    kCFRunLoopExit = (1UL << 7),           //即将退出runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU  //监听所有状态
};

如果在 RunLoop 中添加Observer ,只能用 CF 的函数,一般用于拦截系统事件等

/*
添加Observer
参数1:默认值
参数2:监听活动(这里监听了kCFRunLoopAllActivities)
参数3:重复
参数4:0
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"---监听Runloop状态改变--%lu",activity);
});


/*
添加观察者,监听Runloop的在kCFRunLoopDefaultMode模式下的状态
参数1:当前runloop
参数2:监听者
参数3:模式
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
//释放Observer
CFRelease(observer);
  • CoreFoundation的内存管理
    • 凡是带有Createcopyretain等字眼的函数,创建出来的对象,都需要在最后做一次release
    • release函数:CFRelease(observer)

RunLoop 内部逻辑

RunLoop处理逻辑

//用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); 
            }
            
            //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() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

关于 RunLoop 的内存,在 RunLoop 中,一个 RunLoop 会对应一条线程,而自动释放池是针对当前线程的一些对象,它会在kCFRunLoopBeforeWaiting之前会被释放一次,在下一次启动的时候会重新创建