iOS-Runloop1-Runloop

一. RunLoop相关

什么是Runloop?
顾名思义,Runloop就是运行循环,就是在程序运行过程中循环做一些事情。

RunLoop.png

RunLoop的基本作用:

保持程序的持续运行
处理App中的各种事件(比如触摸事件、定时器事件等)
节省CPU资源,提高程序性能:该做事时做事,该休息时休息
......

1. UIApplicationMain里面的RunLoop

如果没有RunLoop

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

执行完打印,就会退出程序。

如果有了RunLoop

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

UIApplicationMain函数里面就是创建一个RunLoop对象,然后RunLoop对象一直循环等待和处理消息,伪代码如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            //睡眠中等待消息
            int message = sleep_and_wait();
            //处理消息
            retVal = process_message(message);
        } while (0 == retVal);
        return 0;
    }
}

这样就保证了程序并不会马上退出,一直保持运行状态。

2. RunLoop对象

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

Foundation:NSRunLoop
Core Foundation:CFRunLoopRef

获取RunLoop对象:

Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
  1. NSRunLoop和CFRunLoopRef都代表着RunLoop对象
  2. NSRunLoop是基于CFRunLoopRef的一层OC包装
  3. CFRunLoopRef是开源的:Core Foundation源码

他们两者之间的关系如下图:

NSRunLoop和CFRunLoopRef

NSRunLoop就是对CFRunLoopRef的一层包装,就好像NSArray是对CFArrayRef的封装,NSString是对CFStringRef的封装一样。

获取当前线程、主线程RunLoop方法如下:

NSLog(@"%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
NSLog(@"%p %p", CFRunLoopGetCurrent(), CFRunLoopGetMain());
NSLog(@"%@", [NSRunLoop mainRunLoop]);

打印:

0x600001141260 0x600001141260
0x600000940a00 0x600000940a00
<CFRunLoop 0x600000940a00 [0x1089e6ae8]>{wakeup port = 0x1d07, stopped = false, ignoreWakeUps = false, 
current mode = kCFRunLoopDefaultMode,
......

打印的地址不一样,可以发现使用%@打印mainRunLoop,里面装的的确是CFRunLoop,通过CFRunLoop的地址也可以验证:NSRunLoop的确是堆CFRunLoopRef的一层封装,所以地址不一样。

3. RunLoop与线程

  1. 每条线程都有唯一的一个与之对应的RunLoop对象,主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop,RunLoop会在线程结束时销毁。
  2. RunLoop是懒加载的,线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建。
  3. 主线程几乎所有的事情都是交给了runloop去做,比如UI界面的刷新、点击时间的处理、performSelector等等。
  4. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。

下载Core Foundation源码,拖到新创建的命令行项目中,查看CFRunLoopGetCurrent源码实现:

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

进入_CFRunLoopGet0:

static CFMutableDictionaryRef __CFRunLoops = NULL; //是个字典,线程作为key,取出对应的RunLoop
static CFLock_t loopsLock = CFLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    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);
    }
    //将字典和key(线程)传入CFDictionaryGetValue函数,获取RunLoop对象
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) { //如果RunLoop不存在
    CFRunLoopRef newLoop = __CFRunLoopCreate(t); //就创建RunLoop
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//并且将RunLoop保存到字典里面
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

其中__CFRunLoops是个字典,线程作为key,取出对应的RunLoop,如果没有,再创建,然后保存到字典中,验证了RunLoop对象的确是懒加载的。

4. RunLoop相关的类

Core Foundation中关于RunLoop的5个类

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

在源码中查看,CFRunLoopRef:

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

可以发现CFRunLoopRef是指向__CFRunLoop结构体的指针,找到__CFRunLoop结构体源码:

struct __CFRunLoop {
    ......
    pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;//当前模式
    CFMutableSetRef _modes; //是个集合,无序的,里面装的是CFRunLoopModeRef类型的对象
    ......
};

__CFRunLoop里面有个_modes,它是个集合,里面装的是一堆CFRunLoopModeRef类型的对象,当前的模式是_currentMode。

集合是无序的,数组是有序的,集合只能通过anyObject取值,数组可以通过索引取值,如下:

// 有序的
NSMutableArray *array;
[array addObject:@"123"];
array[0];

// 无序的
NSMutableSet *set;
[set addObject:@"123"];
[set anyObject];

进入CFRunLoopModeRef:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    ......
    CFStringRef _name;//名称
    CFMutableSetRef _sources0;//里面装的是CFRunLoopSourceRef类型的对象
    CFMutableSetRef _sources1;//里面装的是CFRunLoopSourceRef类型的对象
    CFMutableArrayRef _observers;//里面装的是CFRunLoopObserverRef类型的对象
    CFMutableArrayRef _timers;//里面装的是CFRunLoopTimerRef类型的对象
    ......
};

① 5个类之间的关系:

CFRunLoopRef里面有个_modes集合,里面装好多CFRunLoopModeRef类型的模式,_currentMode是当前模式。

模式里面有name,_sources0、_sources1集合(里面装的是CFRunLoopSourceRef类型的东西),_observers数组(里面装的是CFRunLoopObserverRef类型的东西),_timers数组(里面装的是CFRunLoopTimerRef类型的东西)。

如下图所示:

结构.png
  1. CFRunLoopModeRef代表RunLoop的运行模式
  2. 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
  3. RunLoop启动时只能选择其中一个Mode,作为currentMode,如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
  4. 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
  5. 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
  6. 其中Timer是定时器,平时创建的一些定时器都放在这里,Observer是监听器,source0/Source1是事件,比如点击事件、performSelector等等。

② 为什么多种模式要分开呢?

比如scrollView滚动的时候让它切换到滚动模式,那么在滚动模式下,scrollView就专心处理滚动相关的就可以了,以前模式下的事情就不处理了。如果不滚动,在正常模式下,就专心处理正常模式下的事情就好了,这样可以做到流畅不卡顿。

5. 常见的两种Mode

  1. kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行。

  2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

常用的就是默认模式和界面跟踪模式,其他模式基本用不到,都懒得说了。

6. Source0、Source1、Timers、Observers

RunLoop切换到某个模式就开始处理那个模式的Source0、Source1、Timers、Observers,那么这些东西分别代表什么呢?

Source0:触摸事件处理、performSelector:onThread:。
Source1:基于Port(端口)的线程间通信、系统事件捕捉 (比如点击事件,通过Source1捕捉,然后包装成Source0进行处理)。
Timers:NSTimer、performSelector:withObject:afterDelay:(底层就是NSTimer)。
Observers:用于监听RunLoop的状态、UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)。

比如,点击界面空白就是Source0事件,验证如下:

在touchesBegan:withEvent:方法打断点

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
 //打断点
}

查看函数调用栈:

函数调用栈.png

可以发现的确是通过source0处理事件,最后才调用到touchesBegan:withEvent:方法的。

关于Observers监听UI刷新(BeforeWaiting):
比如以下代码就属于UI刷新:

self.view.backgroundColor = [UIColor redColor];

这句代码并不是立马执行,Observers会先记下来,当Observers监听到RunLoop将要睡觉啦,就在RunLoop将要睡觉之前执行(刷新UI)。

同理Autorelease pool也是一样,当Observers监听到RunLoop将要睡觉啦,就在RunLoop睡觉之前释放对象。

7. RunLoop状态

上面说了,Observers可以监听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  //所有状态
};

8. 如何监听RunLoop的状态?

Observers数组里面有系统创建的一些Observer,用于监听RunLoop状态进行UI刷新、Autorelease pool等,如果我们自己想监听RunLoop状态肯定要自己创建Observer。

// 创建Observer
/*
参数一:一般传默认的:kCFAllocatorDefault
参数二:传入你想监听什么状态
参数三:是否重复监听
参数四:顺序0,不需要考虑顺序
参数五:监听函数名
参数六:context,会传入监听函数的info里面,一般传NULL
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式,默认包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

//监听RunLoop的函数,这个函数要求传入三个参数
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}

当然也可以通过block监听,和上面一样的:

// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
       ......
    }
});
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式,默认包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
① 监听点击空白事件

点击空白,打印如下:

kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources 即将处理source事件
-[ViewController touchesBegan:withEvent:]
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
......

可以看出先是kCFRunLoopBeforeSources状态,之后再执行点击事件方法,这也验证了点击事件是source0事件。

② 监听定时器事件

添加定时器:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"定时器-----------");
    }];
//    NSLog(@"%s",__func__);
}

点击空白,打印:

kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting 先睡觉
kCFRunLoopAfterWaiting 3秒后唤醒
定时器-----------
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources

可以发现,先睡觉,睡了3秒后再执行定时器事件。

③ 监听模式切换

主view里面添加一个textView,写如下代码监听RunLoop模式改变:

// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
        case kCFRunLoopEntry: {
            CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"kCFRunLoopEntry - %@", mode);
            CFRelease(mode);
            break;
        }
        case kCFRunLoopExit: {
            CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"kCFRunLoopExit - %@", mode);
            CFRelease(mode);
            break;
        }
        default:
            break;
    }
});
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式,默认包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

滑动textView,打印:

kCFRunLoopExit - kCFRunLoopDefaultMode
kCFRunLoopEntry - UITrackingRunLoopMode
kCFRunLoopExit - UITrackingRunLoopMode
kCFRunLoopEntry - kCFRunLoopDefaultMode

可以发现,先退出默认模式进入滑动模式,再退出滑动模式进入默认模式。

二. RunLoop的运行流程

1. 流程图

苹果官方的流程图:

RunLoop的运行流程.png

官方的图有点抽象,看MJ老师的图:

RunLoop的运行流程.png

对于第4步的block就是下面的block

CFRunLoopPerformBlock(<#CFRunLoopRef rl#>, <#CFTypeRef mode#>, ^{
    //传入RunLoop和模式,RunLoop就会执行这个block里面的代码
})

2. 源码验证

仔细分析上图流程之后,下面我们查看源码,看看到底是不是这样的,但是RunLoop的入口是哪个呢?打断点,查看函数调用栈,如下:

函数调用栈

可以发现第一个关于RunLoop的函数是CFRunLoopRunSpecific,是Core Foundation框架下的函数。在Core Foundation源码中搜索“ CFRunLoopRunSpecific”,由于原函数比较复杂,精简后如下:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    //通知Observers进入Loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //具体要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知Observers退出Loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    //返回事情做完后的结果
    return result;
}

进入__CFRunLoopRun函数,这个函数更复杂,精简后如下:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {
        // 通知Observers,即将处理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知Observers,即将处理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 处理Source0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 如果返回YES,处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        // 判断有无Source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 如果有Source1就跳转handle_msg
            goto handle_msg;
        }
        
        // 通知Observers,即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 开始休眠
        __CFRunLoopSetSleeping(rl);
        
        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
        
        // 等待别的消息来唤醒当前线程,往下走代表有人唤醒它了
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
        // 结束休眠
        __CFRunLoopUnsetSleeping(rl);
        // 通知Observers,结束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg:
        
       if (被Timer唤醒) {
           // 处理Timers
           __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        } else if (被GCD唤醒) {
           // 处理GCD相关
           __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { // 被Source1唤醒
           // 处理Source1
           __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        }
        
        // 再处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
    
        // 设置返回值retVal
        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;
        }
        
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
        
        // 如果retVal等于0,继续执行循环
    } while (0 == retVal);
    
    return retVal;
}

上面源代码对比流程图可知,流程的确如图所示。

3. 小细节

__CFRunLoopDoObservers函数中真正做通知Observers相关的是CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION函数

__CFRunLoopDoBlocks函数中真正做Block相关的是CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK函数

__CFRunLoopDoSources0函数中真正做Source0相关的是CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION函数

__CFRunLoopDoTimers函数中真正做定时器相关的是CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION函数

__CFRunLoopDoSource函数中真正做Source1相关的是CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION函数

我们也可以通过函数调用栈来验证:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"123"); // 断点
    }];
}

在断点处查看函数调用栈:

定时器函数调用栈

可以发现的确调用了CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION函数,之后Core Foundation框架的函数就调用完了。

一般情况下GCD的东西是GCD来处理的,不会交给RunLoop。GCD是GCD,RunLoop是RunLoop,他们互不干扰,但是有一种情况下GCD是交给RunLoop处理的,如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        // 子线程处理一些逻辑

        // 回到主线程去刷新UI界面
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"11111111111"); // 断点
        });
    });
}

函数调用栈:

GCD处理

当我们在子线程处理一些逻辑然后回到主线程去刷新UI界面,这种情况就会交给RunLoop去处理GCD相关的东西,然后再回到GCD。

③ 线程休眠

当RunLoop事情处理完发现没有事情干,就会进入休眠,这时候你可以认为是“线程阻塞”,这时候代码就不会往下走了,直到被唤醒,才会继续往下走。

这种休眠和while(1){};不一样,这种休眠是当前线程休息了,CPU不再给当前线程分配资源,但是while(1){};这种阻塞是一个死循环,这时候线程没有休息,还在一直执行while(1){}。

那么线程休眠是怎么做到的呢?
同样查看源码,进入__CFRunLoopServiceMachPort函数:

......
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
......

上面的mach_msg是内核层面的API,内核层面的API是不给我们程序员使用的,因为比较危险,给我们使用的都是应用层面的API。

RunLoop休眠的实现原理,如下图:

RunLoop休眠的实现原理.png

当用户态调用mach_msg()函数会自动切换到内核态,会真正调用内核态的mach_msg()函数,内核态mach_msg做的事就是:没有消息就让线程休眠,有消息就唤醒线程,唤醒线程后就又回到用户态。所以线程休眠就是因为用户态和内核态的切换

三. RunLoop在实际开发中的应用

  1. 解决NSTimer在滑动时停止工作的问题
  2. 控制线程生命周期(线程保活)
  3. 监控应用卡顿
  4. 性能优化

先讲前两种,后面两种讲到性能优化再说。

1. 解决NSTimer在滑动时停止工作的问题

定时器失效问题:
在view上添加一个scrollView,写如下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];
}

不拖动scrollView,定时器一直在打印,拖动scrollView发现定时器停止工作了。
这是因为RunLoop只会在一种模式下工作,默认情况下定时器被添加到NSDefaultRunLoopMode模式下,但是拖动的时候RunLoop切换成UITrackingRunLoopMode模式,但是定时器没有添加到这个模式下,所以定时器不会工作。

解决办法也很简单,就是在两种模式下都添加定时器,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];

    // NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
    // NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
    // timer能在_commonModes数组中存放的模式下工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

{
    //scheduledTimerWithTimeInterval默认会把定时器添加到默认模式下,所以用timerWithTimeInterval
    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];
}

注意,NSRunLoopCommonModes并不是一个真的模式,它只是一个标记,定时器能在_commonModes数组中存放的模式下工作。

如果timer被标记为commonModes,那么timer就能在_commonModes数组中存放的模式下工作,而_commonModes数组中存放的恰好是NSDefaultRunLoopMode和UITrackingRunLoopMode模式,当然这两种模式也在_modes数组里面。
能在commonModes“模式”下工作的东西都会被添加到_commonModeItems数组中,比如上面的timer。

再看一下__CFRunLoop结构体源码:

struct __CFRunLoop {
    ......
    pthread_t _pthread;
    CFMutableSetRef _commonModes; //就是这个_commonModes数组
    CFMutableSetRef _commonModeItems; //能在commonModes“模式”下工作的东西都会被添加到这个数组中
    CFRunLoopModeRef _currentMode;//当前模式
    CFMutableSetRef _modes; //是个集合,无序的,里面装的是CFRunLoopModeRef类型的对象
    ......
};

如果timer是NSDefaultRunLoopMode或者UITrackingRunLoopMode模式,那么它就会被添加到自己模式下的_timers数组中。
进入CFRunLoopModeRef:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    ......
    CFStringRef _name;//名称
    CFMutableSetRef _sources0;//里面装的是CFRunLoopSourceRef类型的对象
    CFMutableSetRef _sources1;//里面装的是CFRunLoopSourceRef类型的对象
    CFMutableArrayRef _observers;//里面装的是CFRunLoopObserverRef类型的对象
    CFMutableArrayRef _timers;//里面装的是CFRunLoopTimerRef类型的对象
    ......
};

关于线程保活请看下篇文章。

四. 面试题

  1. 讲讲 RunLoop,项目中有用到吗?

  2. runloop内部实现逻辑?
    看流程图

  3. runloop和线程的关系?
    一对一的关系

  4. timer 与 runloop 的关系?
    ① RunLoop对象里面有个_modes数组,里面放一堆模式,模式里面会放timer,如果timer被标记为commonModes,那么timer就能在_commonModes数组中存放的模式下工作,能在commonModes“模式”下工作的东西都会被添加到_commonModeItems数组里中。
    ② 如果线程休眠了,timer也可以唤醒休眠的RunLoop。

  5. 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
    将timer被标记为commonModes

  6. runloop 是怎么响应用户操作的,具体流程是什么样的?
    当用户有个点击事件,这个系统事件会先被Source1捕捉,Source1捕捉之后会包装成事件队列(EventQuene),再放到Source0里面进行处理,然后RunLoop循环再处理Source0里面的事件。

  7. 说说runLoop的几种状态

  8. runloop的mode作用是什么?
    不同模式的Source0/Source1/Timer/Observer能分隔开来,互不影响

Demo地址:RunLoop1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容