RunLoop

作者: ychen3022 | 来源:发表于2019-03-02 21:27 被阅读0次
    1、什么是Runloop

    runloop : 运行循环。
    一般来说,一个线程一次只能执行一个任务,当执行完任务后线程就会退出,所以需要有个机制保持程序可以随时处理事。
    RunLoop可以管理事件/消息,让线程在没有消息处理时休眠以免占用资源,在消息到来时立刻被唤醒。
    所以,Runloop实际上是一个对象,管理了其需要处理的事件/消息。
    它的作用就是:
    保持程序的持续运行
    处理App中的各种事件(比如触摸事件、定时器事件等)
    节省CPU资源,提高程序性能:该做事时做事,该休息时休息

    在iOS中有2套API来访问和使用RunLoop:

    • Foundation:NSRunLoop
    • Core Foundation:CFRunLoopRef
      NSRunLoop和CFRunLoopRef都代表着RunLoop对象
      NSRunLoop是基于CFRunLoopRef的一层OC包装
      CFRunLoopRef是开源的,源码下载地址:https://opensource.apple.com/tarballs/CF/
    2、RunLoop和线程的关系

    下面是根据当前线程pthread获取对应runloop的源码,从中我们可以看出:

    • 每条线程都有唯一的一个与之对应的RunLoop对象
    • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
    • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
    • RunLoop会在线程结束时销毁
    • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
    //用于存放RunLoops的全局Dictionary,线程作为key,RunLoop作为value
    static CFMutableDictionaryRef __CFRunLoops = NULL;
    // 访问__CFRunLoops(全局Dictionary)时的锁
    static CFLock_t loopsLock = CFLockInit;
    
    
    //获取一个pthread对应的RunLoop
    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {//传入了一个线程作为参数,获取该线程对应的runloop
        if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
        }
        __CFLock(&loopsLock);
        if (!__CFRunLoops) { //第一次进入时,初始化全局Dic,并先为主线程创建一个RunLoop
            __CFUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //为主线程创建runloop ---> mainLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
            __CFLock(&loopsLock);
        }
        //__CFRunLoops是一个字典,pthreadPointer(t)为一个key值,会根据传入的参数去获取该线程对应的runloop
        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        __CFUnlock(&loopsLock);
        if (!loop) {
        //如果不存在的话,会调用__CFRunLoopCreate创建一个runloop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
            __CFUnlock(&loopsLock);
        CFRelease(newLoop);
        }
        if (pthread_equal(t, pthread_self())) {
            //注册一个回调,当线程销毁时,顺便也销毁其对应的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
            if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
            }
        }
        return loop;
    }
    
    3、RunLoop相关的类

    因为NSRunLoop是基于CFRunLoopRef进行包装的,且CFRunLoopRef是开源的,所以研究RunLoop内部的时候,我们看的是CFRunLoopRef。

    Core Foundation中关于RunLoop的5个类:

    • CFRunLoopRef
    • CFRunLoopModeRef
    • CFRunLoopSourceRef
    • CFRunLoopTimerRef
    • CFRunLoopObserverRef

    CFRunLoopRef 的内部结构

    typedef struct __CFRunLoop * CFRunLoopRef;
    
    struct __CFRunLoop {
        // ... more ...
        pthread_t _pthread;               //线程
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;    //当前模式
        CFMutableSetRef _modes;           //模式集合,在模式集合中有且只有一个模式为currentMode
        // ... more ...
    };
    

    CFRunLoopModeRef 的内部结构

    struct __CFRunLoopMode {
        // ... more ...
        CFStringRef _name;       // Mode Name, 例如 @"kCFRunLoopDefaultMode"  
        CFMutableSetRef _sources0;   // Set集合
        CFMutableSetRef _sources1;   // Set集合
        CFMutableArrayRef _observers;  //Array 数组
        CFMutableArrayRef _timers;     //Array 数组
        // ... more ...
    };
    

    其中CFRunLoopModeRef 和 CFRunLoopRef 的关系:

    • CFRunLoopModeRef代表RunLoop的运行模式;
    • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer;
    • RunLoop启动时只能选择其中一个Mode,作为currentMode;
    • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入;
    • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响;
    • 如果Mode里没有任何Source0/Source1/Timer/Observer, RunLoop会立马退出;

      图示如下: RunLoop和Mode的关系

    CFRunLoopModeRef的模式:
    苹果公开提供两个Mode:
    kCFRunLoopDefaultMode (NSDefaultRunLoopMode)和 UITrackingRunLoopMode。
    其中,kCFRunLoopDefaultMode是App的默认Mode,通常主线程是在该模式下运行的。UITrackingRunLoopMode为页面跟踪Mode,在页面滚动时,该模式可以保证页面滚动不受影响,不会造成卡顿。
    另外,苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。这样,当加入到commonModes中时,实际上系统是找出commonModes代表的所有Mode,如defaultMode和trackingMode,然后分别将其加入了这些mode中。例如我们希望定时器在页面滚动的时候不会收到影响,我们会选择kCFRunLoopCommonModes。

    CFRunLoopSourceRef
    CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
    Source0主要管理触摸事件、子线程事件处理等。
    Source1 被用于管理系统事件捕获、基于Port的线程间通信等;

    CFRunLoopTimerRef
    CFRunLoopTimerRef 是基于时间的触发器。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

    CFRunLoopObserverRef
    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
    };
    
    4、RunLoop的运行逻辑
    RunLoop的运行逻辑大致如下图: 屏幕快照 2019-02-15 下午5.33.43.png

    下面是CFRunLoopRef中的部分源码,从中我们可以看到一个大致的执行流程,因为代码太杂乱,所以经过删除简化:

    函数从CFRunLoopRunSpecific中进入,但核心流程在CFRunLoopRun中:

    SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
        CHECK_FOR_FORK();
        if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
        __CFRunLoopLock(rl);
        //根据传入的modeName找到对应mode
        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;
        }
        volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
        CFRunLoopModeRef previousMode = rl->_currentMode;
        rl->_currentMode = currentMode;
        int32_t result = kCFRunLoopRunFinished;
    
        //通知 Observers : 进入loop循环,而且指定了模式
        if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        
        //具体要做的事情,并返回结果
        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
        
        //通知 Observers : 退出loop循环
        if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
            __CFRunLoopModeUnlock(currentMode);
            __CFRunLoopPopPerRunData(rl, previousPerRun);
        rl->_currentMode = previousMode;
        __CFRunLoopUnlock(rl);
        return result;
    }
    

    CFRunLoopRun函数的代码(简化过):主要是一个do-while循环

    static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
        
        Boolean didDispatchPortLastTime = true;
        int32_t retVal = 0;
        //这里的do-while循环,当retVal != 0时才会退出循环
        do {
            //通知Observers:即将处理timers
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
            
            //通知Observers: 即将处理sources
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
    
            //执行被加入的blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            //处理Sources0(非port) 回调
            Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
            if (sourceHandledThisLoop) {
                //返回结果YES的话,再一次处理blocks
                __CFRunLoopDoBlocks(rl, rlm);
            }
    
            Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
    
            if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
                msg = (mach_msg_header_t *)msg_buffer;
                // 判断有无Source1
                if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                    // 有 Source1 (基于port) 处于 ready 状态的话,跳转至handle_msg
                    goto handle_msg;
                }
            }
    
            didDispatchPortLastTime = false;
    
            // 通知 Observers: 即将进入休眠
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            
            //开始休眠
            __CFRunLoopSetSleeping(rl);
            __CFPortSetInsert(dispatchPort, waitSet);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
    
            CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
    
            
            do {
                // 内部还有一个do-while循环,线程进入休眠,直到被下列事件唤醒,被唤醒后才继续往下执行:
                // 1>一个基于 port 的Source 的事件
                // 2> 一个 Timer 到时间了
                // 3> RunLoop 自身的超时时间到了
                // 4> 被其他什么调用者手动唤醒
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
                
                if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                    // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                    while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                    if (rlm->_timerFired) {
                        // Leave livePort as the queue port, and service timers below
                        rlm->_timerFired = false;
                        break;
                    } else {
                        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                    }
                } else {
                    // Go ahead and leave the inner loop.
                    break;
                }
            } while (1);
            
            __CFRunLoopUnsetSleeping(rl);
            // 通知 Observers: 结束休眠
            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
        // 收到消息,处理消息
        handle_msg:;
            if (被timer唤醒) {
                __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
            } else if (被gcd唤醒) {//livePort == dispatchPort:看到dispatchPort,多和gcd相关
                //处理gcd相关的事情
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } else { //被sources1唤醒
                //处理sources1
                __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            }
    
            //再次处理blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            //设置返回值,根据返回值决定下一步要做什么,是否退出loop
            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;
    }
    

    需要注意的是:
    当RunLoop进入休眠的时候,是内核层面的休眠,与我们平时所说的代码循环阻塞不一样。
    runloop休眠实现的原理:
    当RunLoo将要进入休眠时,会从用户态切入到内核态,进入内核层面的休眠并等待消息。直到有消息唤醒时,才又从内核态切入到用户态去处理消息。RunLoop在不同状态之间的切换是mach_msg()去发送消息,从而完成状态切换的。

    5、RunLoop的应用
    • <1>定时器的使用
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"定时器的使用");
        }];
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
       [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        //定时器加到NSRunLoopCommonModes,这样页面scrollview在滑动的时候,定时器依然会执行
    
    

    NSRunLoopCommonModes并不是一个真实存在的模式,他只是一个标记,传入NSRunLoopCommonModes意味着这个timer在设置了common标记的模式下都可以运行。
    如果从结构上来说的话:
    __CFRunLoop结构体中有一项叫做CommonModes,该集合中装的是标记了common的模式,默认情况下NSDefaultRunLoopMode、UITrackingRunLoopMode都装在CommonModes中。
    当将timer加入到NSRunLoopCommonModes时,timer被加入到__CFRunLoop结构体的_commonModeItems这一集合,这个timer就可以在多个mode中都能执行了。

    • <2>控制线程生命周期
      看下面的代码:
    @interface ViewController ()
    @property(nonatomic,strong)NSThread *thread;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
        [self.thread start];
    }
    
    -(void)startThread{
        NSLog(@"开启子线程");
        
        NSLog(@"子线程结束");
    }
    
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        [self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
    
    -(void)run{
        NSLog(@"当前线程 %@",[NSThread currentThread]);
    }
    
    @end
    

    上面的代码,在启动的时候创建了子线程self.thread,每次点击屏幕的时候在touchesBegan中调用run方法。但是子线程在执行startThread方法后便结束了,所以后续在self. thread上调用run方法会报错,因为self.thread已经销毁了。
    所以我们需要用RunLoop来对线程进行保活操作:

    @interface ViewController ()
    
    @property(nonatomic,strong)NSThread *thread;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
        [self.thread start];
    }
    
    -(void)startThread{
        NSLog(@"开启子线程");
        //线程保活:往RunLoop里面添加Source\Timer\Observer
        //加上runloop之后,不会打印“子线程结束”,而是一直在runloop里面循环、休眠,当点击屏幕会唤醒runloop去处理
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
        
        NSLog(@"子线程结束");
    }
    
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        [self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
    
    -(void)run{
        NSLog(@"当前线程 %@",[NSThread currentThread]);
    }
    @end
    

    但是这样还是有不完善的地方,当页面销毁的时候,由于子线程的runLoop在循环运行,倒是self.thread不会销毁,从而导致ViewController也不会被销毁。
    下面这种写法更加完善:

    @interface ViewController ()
    
    @property(nonatomic,strong)MJThread *thread;
    @property (assign, nonatomic, getter=isStoped) BOOL stopped;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //但是这样会有一个强引用,线程self.thread强引用ViewController,ViewController强引用self.thread
        //导致页面销毁时,self.thread和ViewController不会被释放掉
        //self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
        //[self.thread start];
        
        
        //使用block,ViewController能够被释放掉,但是线程self.thread不会被释放
        //因为runloop一直循环运行,没有被销毁,所以self.thread没有被释放
        //所以要设置标记,在合适的时候停止runloop
        __weak typeof(self) weakSelf = self;
        self.stopped = NO;
        self.thread = [[MJThread alloc] initWithBlock:^{
            NSLog(@"begin---thread");
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            while (weakSelf && !weakSelf.isStoped ) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
            NSLog(@"end---thread");
        }];
        [self.thread start];
    }
    
    
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
         if (!self.thread) return;
        [self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    -(void)run{
        NSLog(@"当前线程 %@",[NSThread currentThread]);
    }
    
    
    //停止子线程self.thread的runloop
    -(void)stopThread{
        self.stopped = YES;// 设置标记为YES
        CFRunLoopStop(CFRunLoopGetCurrent()); // 停止RunLoop
        NSLog(@"stopThread---%@",[NSThread currentThread]);
        self.thread = nil;
    }
    
    -(void)dealloc{
        NSLog(@"%s",__func__);
        //waitUntilDone设置成NO,这样可以保证stopThread里面的方法执行完毕,才会这句代码下面的内容,k也就是彻底销毁ViewController
        [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
    
    @end
    
    6、总结
    • <1>什么是RunLoop,它是怎么做到有事做事,没事休息的?
      Runloop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
      当调用CFRunLoopRun()后,处理完毕事件要进入休眠时,系统会调用mach_msg()从用户态进入内核态,从而减少内存开支。当有事情唤醒runloop时,又从内核态切换到用户态。

    • <2>runloop与线程是什么样的关系?
      runloop与线程是一一对应的关系
      一个线程默认是没有runloop的(主线程默认创建了runloop),我们需要为它手动创建runloop。

    • <3>如何实现一个常驻线程?
      (1)创建一个线程对应的runloop
      (2)在runloop中添加一个timer、observer、port等内容
      (3)调用runloop的run方法

    • <4>当一个处于休眠状态的runloop,有哪些方式可以唤醒它?
      source1事件
      timer到时间了
      runloop自身的超时时间到了
      被外部手动操作事件唤醒

    相关文章

      网友评论

        本文标题:RunLoop

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