美文网首页
iOS:RunLoop详解

iOS:RunLoop详解

作者: G_Jayson | 来源:发表于2019-07-10 14:52 被阅读0次

RunLoop 本质:

RunLoop 本质上是一个运行循环,其作用是保持线程的生命,防止线程被销毁,日常开发中无处不在,为了感受到 RunLoop 的存在,举个简单的例子,我们都知道,APP程序的入口在main.m里面:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

接下来我们试着感受一下 RunLoop,点击查看 UIApplicationMain,其返回值是一个 int 值:

// If nil is specified for principalClassName, the value for NSPrincipalClass from the Info.plist is used. If there is no
// NSPrincipalClass key specified, the UIApplication class is used. The delegate class will be instantiated using init.
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);

我们在 main 函数里面用一个 int 变量去接收它,然后在返回值前后添加输出语句:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"1 %@", [NSThread currentThread]);
        
        int a = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        
        NSLog(@"2 %@", [NSThread currentThread]);
        
        return a;
    }
}

运行程序,看一下输出:


image.png

我们看到,只执行了第一个输出,并且当前线程是主线程,第二个输出语句并没有执行,没有执行的原因,有两种可能,第一种:UIApplicationMain 内部阻塞;第二种:UIApplicationMain 函数内部开启了一个死循环;但是一旦程序发生阻塞,APP 就无法使用了,那么说得通的可能性就是第二种,这个函数内部开启了死循环,就是 RunLoop 循环,它的作用,就是保持主线程的生命,并且帮我们监听一些事件,比如触摸、时钟、网络等;

Runloop 的两种形式:

OC: Foundation ——> NSRunLoop

// NSRunLoop获取RunLoop
[NSRunloop currentRunloop];  // 获得当前线程的RunLoop的方法
[NSRunloop  mainRunloop];   // 获得主线程的RunLoop的方法   

C: CoreFoudation ——> CFRunLoopRef

// CFRunLoopRef获取RunLoop
CFRunloopGetCurrent();  // 获得当前线程的RunLoop的方法  
CFRunloopGetMain();   // 获得主线程的RunLoop的方法  

NSRunLoop 是对 CFRunLoopRef 的进一步封装,并且 CFRunLoopRef 是线程安全的,而这一点 NSRunLoop 并不能保证。

RunLoop 与线程的关系:

每一条线程都有一个与之对应的 RunLoop 对象;
RunLoop 保存在全局的字典内,是以线程为 key,RunLoop 为 value;
RunLoop 的创建是发生在第一次获取时(懒加载, 不获取是不会创建的),RunLoop 的销毁是发生在线程结束时,只能在一个线程的内部获取其 RunLoop(主线程除外)。

RunLoop 对外的接口:

在 CoreFoundation 里面关于 RunLoop 有5个类:

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;    // CFRunLoopRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;   // CFRunLoopSourceRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;  // CFRunLoopObserverRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;   // CFRunLoopTimerRef

CFRunLoopModeRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装:


image.png

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source / Timer / Observer,每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode,如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 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 的状态发生变化时,观察者就能通过回调接受到这个变化,RunLoop 的状态如下:

/* Run Loop Observer Activities */
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
};

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

RunLoop 的 Mode:

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
    ...
};

常见的2种 Mode:

kCFRunloopDefaultMode  //程序的默认Mode
UITrackingRunloopMode  //界面追踪Mode

不同 Mode 对应的事件类型都不相同,例如我们平时使用的 Timer:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
    // timerWithTimeInterval:target:selector:userInfo:repeats: 创建的 Timer 需要手动添加到RunLoop中才能运行
    // 两个 Mode 同时添加,在两个模式下都会运行
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];  // 默认模式下运行,但当触摸事件发生时,就会停止
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];  // 有触摸事件时才会运行,但是时间结束后就会停止
    
    // 添加到 NSRunLoopCommonModes 中,在两个模式下都会运行
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}


- (void)timerMethod {
    NSLog(@"%@", [NSThread currentThread]);
}

通过这种方式创建的 Timer 本身是不会运行的,只有将 Timer 添加到 RunLoop 中才会运行,我们先将 Mode 设置为 NSDefaultRunLoopMode,Timer 会运行,并且打印输出, 这个时候我们往界面中添加交互控件,例如添加一个ScrollView,当我们滑动这个控件时, Timer 就会停止,响应完触摸事件又会恢复运行,这是因为 RunLoop 会优先响应 UITrackingRunloopMode 的事件,相反,如果我们将 Mode 设置为 UITrackingRunloopMode,那正常情况下 Timer 不会运行,当有 UI 事件的时候,才会运行;这和我们的初衷相背离,我们希望在任何情况下,Timer 都能运行,这个时候,我们就要将 Timer 同时添加到 NSDefaultRunLoopMod 和 UITrackingRunloopMode 两个模式下,NSRunLoopCommonModes 就是为我们解决这个问题。

注:
kCFRunloopCommonModes 默认包括了 kCFRunloopDefaultMode 和 UITrackingRunloopMode;
kCFRunloopCommonModes 并不是一个真正的模式,只是一个标记,而 kCFRunloopDefaultMode 和 UITrackingRunloopMode 的模式是真正意义的模式,timer 在设置了 kCFRunloopCommonModes 标记的模式下都能运行,而 kCFRunloopDefaultMode 和 UITrackingRunloopMode 这两个模式都是 kCFRunloopCommonModes 标记的。

RunLoop 运行逻辑:

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

相关文章

网友评论

      本文标题:iOS:RunLoop详解

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