RunLoop

作者: 意一ineyee | 来源:发表于2019-10-04 18:38 被阅读0次

    目录
    一、RunLoop是什么
    二、RunLoop的底层实现
     1、RunLoop和线程的关系
     2、RunLoop的运行模式
     3、RunLoop的“特殊运行模式”——CommonModes
    三、Source0/Source1/Observer/Timer分别代表什么
    四、RunLoop的运行流程
    五、RunLoop的实际应用
     1、处理NSTimer不工作的问题
     2、线程保活

    一、RunLoop是什么


    RunLoop即运行循环,它也是一个OC对象,主要作用有三个:

    • 它内部维护着一个do...while循环,正是由于这个do...while循环的存在,线程才得以保活——不会执行完任务就立马退出,比如说我们的App在启动后就会在main函数那里创建并启动一个主线程对应的RunLoop,这才保证了App能一直活着,否则App一启动执行完main函数就退出了;
    • 这个do...while循环里循环处理着App的各种事件,包括Source0事件、Source1事件和Timer事件;
    • 线程的休眠唤醒则是RunLoop区别于其它语言EventLoop的核心所在,线程没事做时就休眠,有事做时就唤醒做事,可以节省CPU资源。

    二、RunLoop的底层实现


    RunLoop是NSRunLoop类型的,它的C语言实现为CFRunLoop。因为CFRunLoop开源,所以接下来我们会从它的源码来看看RunLoop的底层实现。

    typedef struct __CFRunLoop *CFRunLoopRef;
    struct __CFRunLoop {
        pthread_t _pthread; // RunLoop对应的线程
    
        CFMutableSetRef _modes; // RunLoop所有的运行模式
        CFRunLoopModeRef _currentMode; // RunLoop当前的运行模式
    
        CFMutableSetRef _commonModes; // RunLoop的一个“特殊运行模式”
        CFMutableSetRef _commonModeItems; // “特殊运行模式“里的items
    };
    
    typedef struct __CFRunLoopMode *CFRunLoopModeRef;
    struct __CFRunLoopMode {
        CFStringRef _name; // 运行模式的名字,如@"NSDefaultRunLoopMode"、@"UITrackingRunLoopMode"
        CFMutableSetRef _sources0; // Set
        CFMutableSetRef _sources1; // Set
        CFMutableArrayRef _observers; // Array
        CFMutableArrayRef _timers; // Array
    };
    

    1、RunLoop和线程的关系

    苹果并没有为我们提供创建RunLoop的API,仅仅提供了获取RunLoop的API。

    // Core Foundation框架
    CFRunLoopGetMain(); // 获取主线程对应的RunLoop
    CFRunLoopGetCurrent(); // 获取当前线程对应的RunLoop
    
    // Foundation框架,对Core Foundation框架函数的封装
    [NSRunLoop mainRunLoop]; // 获取主线程对应的RunLoop
    [NSRunLoop currentRunLoop]; // 获取当前线程对应的RunLoop
    

    这两套API的底层实现大概如下(伪代码,详见CFRunLoop.c文件):

    CFRunLoopRef CFRunLoopGetMain() {
        return _CFRunLoopGet(pthread_main_thread_np());
    }
     
    CFRunLoopRef CFRunLoopGetCurrent() {
        return _CFRunLoopGet(pthread_self());
    }
    
    
    // 全局的字典,pthread_t是key,CFRunLoopRef是value
    static CFMutableDictionaryRef __CFRunLoops;
    // 访问__CFRunLoops时的锁
    static CFLock_t loopsLock;
    
    CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
        __CFLock(&loopsLock);
        
        if (!__CFRunLoops) { // 如果是第一次获取RunLoop(那肯定是获取主线程对应的RunLoop,因为App一启动系统就会自动去获取主线程对应的RunLoop,我们自己写的获取且早着呢)
    
            // 初始化全局的字典
            __CFRunLoops = CFDictionaryCreateMutable();
            
            // 创建主线程对应的RunLoop
            CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
            // 主线程为key,主线程对应的RunLoop为value,存入全局的字典里
            CFDictionarySetValue(dict, pthread_main_thread_np(), mainLoop);
        }
        
        // 从全局的字典里读取某个线程对应的RunLoop
        CFRunLoopRef loop = CFDictionaryGetValue(__CFRunLoops, thread);
        
        if (!loop) { // 如果读取不到
            
            // 创建该线程对应的RunLoop,
            loop = __CFRunLoopCreate(thread);
            // 该线程为key,该线程对应的RunLoop为value,存入全局的字典里
            CFDictionarySetValue(__CFRunLoops, thread, loop);
        }
    
        // 注册一个回调,当某个线程销毁时,也销毁该线程对应的RunLoop
        _CFSetTSD(thread, loop, __CFFinalizeRunLoop);
    
        __CFUnlock(&loopsLock);
        return loop;
    }
    

    可见:

    • RunLoop是基于线程来管理的,它们俩是一一对应的关系,共同存储在一个全局的字典里,线程是key,RunLoop是value
    • 但是线程在创建时,它对应的RunLoop并不会被创建,RunLoop的创建发生在它第一次被获取时。只不过App一启动系统就会自动去获取主线程对应的RunLoop并启动,而子线程对应的RunLoop,除非我们主动去获取,否则不会创建,我们获取也即创建子线程的RunLoop后,还需要手动启动它;
    • RunLoop的销毁发生在线程销毁时。

    2、RunLoop的运行模式

    可见:

    • 一个RunLoop可以有多个运行模式,而每个运行模式里又可以有多个Source0/Source1/Observer/Timer。
    • 但是RunLoop一次只能运行在一个运行模式下,这个运行模式被称为CurrentMode。如果要切换运行模式,就得退出RunLoop,重新选择一个运行模式运行,这样做的目的是为了把不同运行模式里的Source0/Source1/Observer/Timer给隔离开来,在这个模式下的时候就做这个模式里的事,在这个模式下的时候就做这个模式里的事,让它们互相不 影响。

    3、RunLoop的“特殊运行模式”——CommonModes

    系统为RunLoop提供了好几种运行模式,其中NSDefaultRunLoopModeUITrackingRunLoopMode是我们经常使用的。NSDefaultRunLoopMode是RunLoop默认的运行模式,UITrackingRunLoopMode是界面滑动时RunLoop的运行模式,系统会自动完成不同情况下这两种运行模式的切换。

    当然RunLoop还有另一种“特殊运行模式”,就是NSRunLoopCommonModes,严格来说它不是一种运行模式,而是一些运行模式的组合。比如说系统会默认把NSDefaultRunLoopModeUITrackingRunLoopMode添加到NSRunLoopCommonModes里,RunLoop运行在NSRunLoopCommonModes模式时,并不是说它就真得运行在NSRunLoopCommonModes下,而是说RunLoop在切换真正的运行模式时会自动把一个运行模式里面的Source0/Source1/Observer/Timer同步到另一个运行模式里。

    三、Source0/Source1/Observer/Timer分别代表什么


    Source0事件源
    • 手势事件
    • performSelector:onThread: 事件
    Source1事件源
    • 系统事件(比如原始指针事件、锁屏、静音、靠近传感器、加速等系统事件,其中原始指针事件在捕捉后可能会被包装成Source0的手势事件处理)
    • 基于Port的线程间通信事件
    Observer

    Observer不是RunLoop的事件源,而是RunLoop的观察者,它主要用来观察RunLoop状态的变化,从而触发回调做一些自定义的处理,比如系统的UI刷新和autoreleasepool创建、销毁就是通过Observer观察RunLoop的状态实现的。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
    };
    
    • UI刷新

    我们编写的UI代码,并不是执行到那一行就立马刷新生效的,而是RunLoop的线程即将进入休眠或刚刚退出RunLoop时才刷新生效的。

    App一启动,系统就会添加一个Observer,监听主线程对应的RunLoop,主要负责UI刷新。这个Observer监听是“线程即将进入休眠”和“刚刚退出RunLoop”两个状态,它的回调里才会真正刷新UI。

    • autoreleasepool的创建和销毁

    App一启动,系统就会添加两个Observer,监听主线程对应的RunLoop,主要负责autoreleasepool的创建和销毁。第一个Observer监听的是“即将进入RunLoop”状态,它的回调里会创建一个autoreleasepool,这个Observer的优先级最高,以此保证创建autoreleasepool发生在其它所有回调之前。第二个Observer监听的是“线程即将进入休眠”和“刚刚退出RunLoop”两个状态,“线程即将进入休眠”时它的回调里会销毁旧autoreleasepool并创建新autoreleasepool,“刚刚退出RunLoop”时它的回调里会销毁autoreleasepool,这个Observer的优先级最低,以此保证销毁autoreleasepool发生在其它所有回调之后。

    Timer事件源
    • NSTimer

    NSTimer是基于RunLoop实现的,我们必须得把NSTimer添加到RunLoop中它才会工作。NSTimer添加到RunLoop中之后,系统就会在相应的时间点(例如11:11、11:12、11:13、11:14)添加好事件,等到了某个时间点就唤醒线程处理事件,但是如果RunLoop某一次循环的任务量很大,到时间点了线程还在处理那个任务,这就会导致NSTimer事件不能准时触发,所以要想定时器非常准时可以使用GCD定时器。

    • CADisplayLink

    CADisplayLinkNSTimer的工作原理基本是一样的,只不过CADisplayLink的调用频率和屏幕的刷新频率一样,每1/60秒调用一次。

    • performSelector:afterDelay:

    performSelector:afterDelay:,内部其实就是创建了一个NSTimer并添加到当前线程的RunLoop中。

    四、RunLoop的运行流程


    RunLoop的运行流程大概如下图:

    RunLoop的运行流程大概如下伪代码:

    // 选择DefaultMode进入RunLoop(App一启动,会走main函数,main函数里面会调用UIApplicationMain函数,UIApplicationMain函数里面就调用该函数获取并启动了主线程对应的RunLoop)
    void CFRunLoopRun() {
        CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10);
    }
    
    // 选择指定的Mode进入RunLoop,也可以指定RunLoop的超时时间(切换Mode时,系统就会调用这个方法来重新进入RunLoop)
    int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds) {
        return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds);
    }
    
    
    int CFRunLoopRunSpecific(runloop, modeName, seconds) {
       
        // 先根据modeName去查找Mode
        CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName);
        // 如果Mode里没有Source0/Source1/Observer/Timer,则直接返回,不进入RunLoop
        if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
        
        // 1、通知Observers:即将进入RunLoop
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
        __CFRunLoopRun(runloop, currentMode, seconds) {
            
            int retVal = 0;
            do { // do...while循环
                // 2、通知Observers:线程即将处理Timer事件
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
                // 3、通知Observers:线程即将处理Source事件
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
                
                
                // 4、处理Source0事件
                __CFRunLoopDoSources0(runloop, currentMode);
                // 5、判断有没有Source1事件
                if (__CFRunLoopServiceMachPort(dispatchPort, &msg)) {
                    
                    // 如果有,就跳转到handle_msg去处理
                    goto handle_msg;
                }
                
                
                // 6、如果Source0事件处理完了、而且没有Source1事件,Timer事件的时间点还没到,则通知Observers:线程即将进入休眠
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
                // 7、线程休眠,等待被唤醒,这里是利用内核函数mach_msg实现的。线程进入休眠后,切换到内核态,会卡死在这个地方,因为线程不做任何事情,不占用任何CPU资源,仅仅是等待着被唤醒
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy) {
                    mach_msg(msg, MACH_RCV_MSG, port);
                }
        
                
                // 8、通知Observers:线程被唤醒,切换到用户态
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
    
                
            handle_msg: // 9、处理唤醒事件
                if (msg_is_timer) { // 如果是被Timer事件唤醒的
                    
                    // 则处理Timer事件
                    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
                } else if (msg_is_dispatch) { // 如果是被GCD dispatch到主线程的事件唤醒的
                    
                    // 则处理GCD dispatch到主线程的事件
                    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                } else { // 如果是被Source1事件唤醒的
                    
                    // 则处理Source1事件
                    __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                }
                
                // 10、根据前面的执行结果,决定如何操作
                if (timeout) { // 如果RunLoop对应线程的休眠时间超过了超时时间
                    
                    // 则退出RunLoop
                    retVal = kCFRunLoopRunTimedOut;
                } else if (__CFRunLoopIsStopped(runloop)) { // 如果RunLoop被强行终止了
                    
                    // 则退出RunLoop
                    retVal = kCFRunLoopRunStopped;
                } if (__CFRunLoopModeIsEmpty(runloop, currentMode, previousMode)) { // 如果RunLoop当前Mode里没有Source0/Source1/Observer/Timer了
                    
                    // 则退出RunLoop
                    retVal = kCFRunLoopRunFinished;
                }
    
                // 如果RunLoop没超时,也没被强行终止,当前Mode里也没空,则继续RunLoop
            } while (0 == retVal);
        }
        
        // 11、通知Observers:刚刚退出RunLoop
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopExit);
        
        return retVal;
    }
    

    比如说有这样一个问题:App启动、App运行过程中点击屏幕、App杀死,这个过程系统都发生了什么?

    App一启动,会走main函数,main函数里面会调用UIApplicationMain函数,UIApplicationMain函数里面会获取并启动主线程对应的RunLoop;主线程处理完一些事件后,没事做了,就会进入休眠状态,而一旦此时我们点击屏幕,系统就会捕捉到这个点击事件做为Source1事件来唤醒主线程,并把点击事件包装为Source0事件处理;之后App杀死,主线程销毁,主线程对应的RunLoop也就销毁了。(也可以结合事件传递和事件响应来做一番回答)

    五、RunLoop的实际应用


    1、处理NSTimer不工作的问题

    Timer有两种创建方式,一种是timerWithXXX,一种是scheduledWithXXX。它们的区别是:timerWithXXX只会创建一个Timer,不会把Timer添加到RunLoop中;scheduledWithXXX不仅会创建一个Timer,还会把Timer添加到RunLoop中,而且是添加到了DefaultMode下。

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    

    所以如果你发现Timer不工作,首先看看是不是用了timerWithXXX的创建方式,如果是,那么你可以手动把Timer添加到RunLoop中,或者换成scheduledWithXXX的创建方式。

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 不工作
        static int count = 0;
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"%d", count++);
        }];
    }
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 工作
        static int count = 0;
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"%d", count++);
        }];
        // 把Timer添加到RunLoop中
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:(NSDefaultRunLoopMode)];
    }
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 工作
        static int count = 0;
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"%d", count++);
        }];
    }
    

    如果你发现Timer仅仅是在界面滑动时不工作,那么你可以把Timer添加到CommonModes下,因为Timer默认是被添加到DefaultMode下,所以在TrackingMode下不工作。

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 界面滑动和不滑动时,都可以工作
        static int count = 0;
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            
            NSLog(@"%d", count++);
        }];
        // 添加到CommonModes下
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:(NSRunLoopCommonModes)];
    }
    

    如果你是在子线程中使用Timer,Timer默认是不工作的,因为子线程的RunLoop没有启用,创建倒是创建了(把Timer添加到RunLoop时系统会创建),因此我们需要手动启用一下RunLoop。

    // 不工作
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            // 用scheduledWithXXX创建定时器时,不仅会创建一个Timer,还会把Timer添加到RunLoop中,所以这个方法里获取了子线程的RunLoop了,也就是说子线程的RunLoop被创建了,就差启动
            [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
                
                NSLog(@"11");
            }];
        });
    }
    
    
    // 工作
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            // 用scheduledWithXXX创建定时器时,不仅会创建一个Timer,还会把Timer添加到RunLoop中,所以这个方法里获取了子线程的RunLoop了,也就是说子线程的RunLoop被创建了,就差启动
            [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
                
                NSLog(@"11");
            }];
            
            // 手动启用一下子线程的RunLoop
            [[NSRunLoop currentRunLoop] run];
        });
    }
    

    2、线程保活

    我们知道线程一执行完任务,它的生命周期就结束了,生命周期一结束,这个线程就无法再使用了,即便它还存在于内存中,但它已经不能做事情了。所以我们说的线程保活其实是指保住线程的生命周期,不让它结束,而不是保住线程一直存在于内存中,要想保住线程一直存在于内存中很简单啊,用强指针就可以了,而要想保住线程的生命周期就不能让线程执行完它的任务,那咱们任务里添加个while(1)死循环吧,可以是可以,但是这太占用CPU资源了吧,所以我们可以用RunLoop来实现线程保活,即:

    • 获取(即创建)子线程的RunLoop
    • 往RunLoop中添加一个Source或Observer或Timer(通常我们选择添加Source,其它两个太重了犯不着),以保证RunLoop不会因没有Source、Observer、Timer而退出
    • 启动RunLoop
    • 而如果想要结束常驻线程,则可以在适当的时机移除掉RunLoop里的Source
    @interface ViewController ()
    
    @property (nonatomic, strong) NSThread *thread;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 创建一个线程并启动
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
        [self.thread start];
    }
    
    - (void)threadAction {
        
        NSLog(@"threadAction:%@", [NSThread currentThread]);
    
        // 获取(即创建)子线程的RunLoop
        // 往RunLoop中添加一个Source或Observer或Timer(通常我们选择添加Source,其它两个太重了犯不着),以保证RunLoop不会因没有Source、Observer、Timer而退出
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 启动RunLoop
        [[NSRunLoop currentRunLoop] run];
    }
    
    @end
    
    // 适当的时机,移除掉RunLoop里的Source,RunLoop就可以顺利退出,线程就会结束生命周期,进而线程销毁时,对应的RunLoop也销毁
    [[NSRunLoop currentRunLoop] removePort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    

    相关文章

      网友评论

        本文标题:RunLoop

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