RunLoop

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_第1张图片
    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的运行逻辑大致如下图:
RunLoop_第2张图片
屏幕快照 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 *)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 *)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 *)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)