iOS中的RunLoop详解(附面试题) - 底层原理总结

开胃面试题

1.讲讲 RunLoop,项目中有用到吗?
2.RunLoop内部实现逻辑?
2.Runloop和线程的关系?
3.timer 与 Runloop 的关系?
4.程序中添加每隔几秒就响应一次的NSTimer,当拖动tableview时,timer可能无法响应要怎么解决?
6.Runloop 是怎么响应用户操作的, 具体流程是什么样的?
7.说说RunLoop的几种状态?
8.Runloop的mode作用是什么?
看这篇文章之前可以先回答一下这几个面试题,然后带着问题耐心看完这篇文章,再来回答一下看看

一、什么是RunLoop?

顾名思义, RunLoop就是运行循环, 它在程序运行过程中交替循环做一些事情,如果没有RunLoop,程序执行完毕就会立即退出,如果有RunLoop,程序会一直运行,并且随时响应用户的操作。在没有用户操作的时候就睡觉,充分节省CPU资源,提高程序性能。

RunLoop

二、RunLoop有什么用?

1.保持程序持续运行,iOSApp一启动就会开一个主线程,主线程会开启RunLoop,保证主线程不会被销毁,也就保持了程序持续运行(命令行项目没有开启RunLoop,所以程序执行完就退出了)
2.处理App中各种事件,如触摸事件,定时器事件,Selector事件,网络请求, 线程间的通信,界面刷新,AutoreleasePool释放对象等。
3.节省CPU资源,提高程序性能,iOSApp启动后,当没有事情要做的时候,RunLoop就会睡觉,节省CPU资源。等到有事要做的时候,就会马上去做事。

如果没有RunLoop, 像下面这样, 这是一个macOS Command Line Tool程序, 打印完"Hello, World!, 程序就会退出, 表现在App上就是App一打开就闪退了.

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}
2019-06-21 20:00:06.877983+0800 NoRunLoop[44082:4739231] Hello, World!
Program ended with exit code: 0

我们可以通过一张图来看一下RunLoop内部运行逻辑

RunLoop内部运行逻辑

三、RunLoop对象

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

  1. Core Foundation : CFRunLoopRef
    它是开源的: https://opensource.apple.com/tarballs/CF/

  2. Foundation : NSRunLoop (基于CFRunLoopRef的OC封装)

NSRunLoop和CFRunLoopRef都代表着RunLoop对象

获取RunLoop对象

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

Core Foundation
CFRunLoopGetCurrent();   //获取当前线程的RunLoop对象
CFRunLoopGetMain();      //获取主线程的RunLoop对象

四、RunLoop在哪里开启

应用要保持运行状态,必须开启一个RunLoop,主线程一开起来,RunLoop就在程序的入口main函数中开启了。

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

RunLoop在UIApplicationMain函数内启动,进入UIApplicationMain

UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);

可以看到,它返回的是一个int类型的数据,我们对它做一些修改看看

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"开始");
        int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"结束");
        return re;
    }
}

执行程序,可以看到只会打印开始,不会打印结束,这说明在UIApplicationMain函数中,开启了RunLoop,这个RunLoop保活了主线程,也就保持了程序持续运行。

RunLoop中的源码

// 用DefaultMode启动
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

可以看到,RunLoop是使用do while判断result的值是否退出程序实现的。所以,可以将RunLoop看成一个死循环,如果没有这个死循环,UIApplicationMain函数执行完将直接返回,也就没有程序持续运行了。

五、RunLoop与线程的关系

  1. 每条线程都有唯一的一个与之对应的RunLoop对象
  2. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  3. 线程刚创建时并没有RunLoop对象, RunLoop会在第一次获取它时创建,RunLoop会在线程结束时销毁
  4. 主线程的RunLoop程序会自动获取(创建), 子线程默认没有开启RunLoop

查看源码

// 拿到当前Runloop 调用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法内部
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);
    // 根据传入的主线程获取主线程对应的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    // 保存主线程 将主线程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 从字典里面拿,将线程作为key从字典里获取一个loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    // 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
    if (!loop) {  
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    // 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
    if (!loop) { 
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        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;
}

可以看到
1. 线程和RunLoop是一一对应的,它们保存在一个字典里。我们创建子线程的RunLoop时,在子线程中获取当前线程的RunLoop对象即可。如果不获取,子线程是不会创建对应的RunLoop的,并且RunLoop只能在一个线程的内部获取。
2. 方法[NSRunLoop currentRunLoop]调用时,会先看一下字典里有没有子线程对应的RunLoop,如果有则直接返回,如果没有则会创建一个,并与子线程一起存入字典。当线程结束时,RunLoop也会被销毁。

六、RunLoop结构体

上源码__CFRunLoop结构体

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

除一些记录属性外,主要来看一下一下两个成员变量

CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

CFRunLoopModeRef其实是指向__CFRunLoopMode结构体的指针,__CFRunLoop结构体的源码如下

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

源码里面很多东西,我们主要查看以下成员变量

CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

上面的代码中,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0、Source1、Timer、Observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。

名词解析

Source0:触摸事件,PerformSelectors
Source1:基于Port的线程间通信
Timer:定时器,NSTimer
Observer:监听器,用于监听RunLoop的状态

我们可以通过打断点,使用bt打印堆栈信息稍微看一下

触摸事件

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"点击了屏幕");
}
touchsBegan堆栈信息

可以看到,触摸事件是会触发Source0

performSelector

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});
performSelector堆栈信息

NSTimer

[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
    NSLog(@"NSTimer ---- timer调用了");
}];
Timer堆栈信息

七、RunLoop相关的类和作用

Core Foundation中关于RunLoop的5个类

  • CFRunLoopRef - 获得当前RunLoop的主线程RunLoop
  • CFRunLoopModeRef - RunLoop运行模式,只能选择一种,在不同模式中做不同的操作
  • CFRunLoopSourceRef - 事件源,输入源
  • CFRunLoopTimerRef - 定时器事件
  • CFRunLoopObserverRef - 观察者

我们可以在开源的CFRunLoopRef看到它们之间的关系,源代码里面不止这些, 为了方便这里只拿了部分比较有用的.

我们可以看到CFRunLoopRef里面有CFRunLoopModeRef

// CFRunLoopRef里面有CFRunLoopModeRef
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
   pthread_t _pthread;
   CFMutableSetRef _commonModes;
   CFMutableSetRef _commonModeItems;
   CFRunLoopModeRef _currentMode;
   CFMutableSetRef _modes;
};

CFRunLoopModeRef里有CFRunLoopSourceRef,CFRunLoopTimerRef、CFRunLoopObserverRef

// CFRunLoopModeRef里有CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef
typedef struct __CFRunLoopMode * CFRunLoopModeRef;
struct __CFRunLoopMode {
   CFStringRef _name;
   CFMutableSetRef _sources0;
   CFMutableSetRef _sources1;
   CFMutableArrayRef _observers;
   CFMutableArrayRef _timers;
}

1.CFRunLoopModeRef

它代表RunLoop的运行模式

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

系统默认注册的5个Mode

RunLoop有如下5种运行模式,其中常见的有2种,分别是KCFRunLoopDefaultMode和UITrackingRunLoopMode

  • KCFRunLoopDefaultMode(NSDefaultRunLoopMode), 这是App的默认Mode, 通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode, 这是界面跟踪Mode, 用于ScrollView追踪触摸滑动, 保证界面滑动时不受其它Mode影响
  • UIInitializationRunLoopMode,在刚启动App时进入的第一个Mode,启动完成后就不再使用,会切换到KCFRunLoopDefaultMode
  • KCFRunLoopCommonModes,这是一个占位用的Mode,作为标记KCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

Mode间的切换

我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动后,NSTimer又会重新恢复,我们通过一段代码来看一下

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
    // 加入到RunLoop中才可以运行
    // 1. 把定时器添加到RunLoop中,并且选择默认运行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
    // [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 当textFiled滑动的时候,timer失效,停止滑动时,timer恢复
    // 原因:当textFiled滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了
    
    // 2. 当我们将timer添加到UITrackingRunLoopMode模式中,此时只有我们在滑动textField时timer才会运行
    // [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // 3. 那个如何让timer在两个模式下都可以运行呢?
    // 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer
    // 3.2 使用站位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,下面两种模式被打上标签
    //0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
    //2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
    // 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
-(void)show
{
    NSLog(@"-------");
}

上述代码中,NSTimer在我们滑动ScrollView时,就不再管用了,调用的方法也就不再执行了。因为我们在主线程使用定时器,此时RunLoop的Mode为KCFRunLoopDefaultMode,即这个定时器属于KCFRunLoopDefaultMode。但是我们滑动ScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,所以此时的定时器就失效了。当我们停止滑动时,RunLoop的Mode会切换回KCFRunLoopDefaultMode,所以NSTimer就又管用了。

同样道理的还有ImageView的显示,我们直接来看代码,不在赘述了

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
    // performSelector默认是在default模式下运行,因此在滑动ScrollView时,图片不会加载
    // [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 ];
    // inModes: 传入Mode数组
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];

我们使用GCD也可以创建定时器,而且更为精确

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //创建队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //1.创建一个GCD定时器
    /*
     第一个参数:表明创建的是一个定时器
     第四个参数:队列
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
    // 局部变量,让指针强引用
    self.timer = timer;
    //2.设置定时器的开始时间,间隔时间,精准度
    /*
     第1个参数:要给哪个定时器设置
     第2个参数:开始时间
     第3个参数:间隔时间
     第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
     GCD的单位是纳秒 所以要*NSEC_PER_SEC
     */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    
    //3.设置定时器要执行的事情
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"---%@--",[NSThread currentThread]);
    });
    // 启动
    dispatch_resume(timer);
}

2.CFRunLoopSourceRef事件源(输入源)

Source分为两种

Source0:非基于Port的,用于用户主动触发的事件(点击Button或点击屏幕等)
Source1:基于Port的,通过内核和其他线程相互发送消息(与内核相关)
触摸事件和PerformSelector会触发Source0事件,我们在前面介绍过

3.CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变。下面我们通过给RunLoop添加监听者,监听其运行状态

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
     //创建监听者
     /*
     第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
     第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
     第三个参数 Boolean repeats:YES:持续监听 NO:不持续
     第四个参数 CFIndex order:优先级,一般填0即可
     第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
     */
     /*
     所有事件
     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
     };
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop进入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要处理Timers了");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"RunLoop要处理Sources了");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"RunLoop要休息了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"RunLoop醒来了");
                break;
            case kCFRunLoopExit:
                NSLog(@"RunLoop退出了");
                break;
                
            default:
                break;
        }
    });
    
    // 给RunLoop添加监听者
    /*
     第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
     第二个参数 CFRunLoopObserverRef observer 监听者
     第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     /*
     CF的内存管理(Core Foundation)
     凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
     GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
     */
    CFRelease(observer);
}

输出


监听者监听RunLoop运行状态

可以看到,Observer确实用来监听RunLoop的状态,包括唤醒,休息,以及处理各种事件。

八、RunLoop的执行流程(处理逻辑)

我们先来看一下官方文档RunLoop处理逻辑

官方文档RunLoop处理逻辑
源码解析

下面源码仅保留了主流程代码

// 共外部调用的公开的CFRunLoopRun方法,其内部会调用CFRunLoopRunSpecific
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    // 通知Observers : 进入Loop
    // __CFRunLoopDoObservers内部会调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函数
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 核心的Loop逻辑
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 通知Observers : 退出Loop
    if (currentMode->_observerMask & kCFRunLoopExit ) __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);
        
        // 处理Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        // 如果有Sources1,就跳转到handle_msg标记处
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            goto handle_msg;
        }
        
        // 通知Observers:即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        
        // 进入休眠,等待其他消息唤醒
        __CFRunLoopSetSleeping(rl);
        __CFPortSetInsert(dispatchPort, waitSet);
        do {
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        } while (1);
        
        // 醒来
        __CFPortSetRemove(dispatchPort, waitSet);
        __CFRunLoopUnsetSleeping(rl);
        
        // 通知Observers:已经唤醒
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
        if (被Timer唤醒的) {
            // 处理Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        }
        else if (被GCD唤醒的) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { // 被Sources1唤醒的
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
        }
        
        // 执行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;
        }
        
    } while (0 == retVal);
    
    return retVal;
}

上述源码中,相应处理事件函数内部还会调用更底层的函数,内部调动才是真正处理事件的函数。

RunLoop的执行流程可以用下面这幅图来表示,RunLoop重复做着这些事情,图中的序号就是RunLoop的执行步骤。

RunLoop的执行流程

相关解释:

  • Source0: 触摸事件处理, performSelector:onThread:
  • Source1: 基于Port的线程间通信, 系统事件捕捉
  • Timers: NSTimers, performSelector:withObject:afterDelay:
  • Observers: 用于监听RunLoop的状态, UI刷新(BeforWaiting), Autorelease pool(BeforeWaiting)

九、RunLoop的退出

  1. 主线程销毁RunLoop退出。
  2. Mode中有一些Timer、Source、Observer,这些保证Mode不为空时RunLoop没有空转并且是在运行的,当Mode为空时,RunLoop会立即退出。
  3. 我们在启动RunLoop的时候可以设置什么时候停止。

十、RunLoop的应用

1、使用 RunLoop 控制线程生命周期(线程保活)

在我们的iOS程序中,开启一个子线程执行任务的时候,它执行完任务就会自行销毁,等到再要执行这样的任务的时候,又要重新开启子线程,如果经常需要用到子线程,老是这样开启和销毁子线程是比较耗费资源的,这个时候为了节约资源,我们可以使用RunLoop来保活子线程,提高性能,比如我们经常用到的网络请求框架AFNetworking就是通过RunLoop保活线程来提高性能的。

下面是一个点击屏幕就开启一个子线程进行打印的程序,为了监听线程是否挂了,我们创建一个继承自NSThread的类,里面只做一件事情,就是dealloc方法里面打印它是否挂了。接着,我们在点击事件中开启子线程打印。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    QLThread *thread = [[QLThread alloc] initWithTarget:self selector:@selector(printThread) object:nil];
    [thread start];
}

- (void)printThread {
    NSLog(@"%s thread:%@",__func__, [NSThread currentThread]);
}
2019-06-21 22:28:07.166746+0800 RunLoop[64726:7390313] -[ViewController printThread] thread:<QLThread: 0x6000008df100>{number = 3, name = (null)}
2019-06-21 22:28:07.167628+0800 RunLoop[64726:7390313] -[QLThread dealloc]
2019-06-21 22:28:10.077604+0800 RunLoop[64726:7390336] -[ViewController printThread] thread:<QLThread: 0x6000008d10c0>{number = 4, name = (null)}
2019-06-21 22:28:10.078640+0800 RunLoop[64726:7390336] -[QLThread dealloc]

可以看到,点了两次创建的两个子线程打印完就挂了。接下来,我们就使用RunLoop来保住它的命看看。

#import "ViewController.h"
#import "QLThread.h"

@interface ViewController ()

@property (nonatomic, strong) QLThread *thread;
@property (nonatomic, assign, getter=isStoped) BOOL stoped;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    //开启一个子线程, 并运行RunLoop
    self.thread = [[QLThread alloc] initWithBlock:^{
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (weakSelf && !weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if (self.thread) {
        [self performSelector:@selector(printThread) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
}

// 子线程需要执行的任务
- (void)printThread {
    NSLog(@"%s thread:%@",__func__, [NSThread currentThread]);
}

// 在子线程停止当前线程的RunLoop
- (void)stop {
    if (!self.thread) return;
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 停止RunLoop
- (void)stopRunLoop {
    self.stoped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
}

// 保活线程
- (void)hold {
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

- (void)dealloc {
    [self stop];
}

@end
2019-06-21 23:53:36.232546+0800 RunLoop[71963:7473281] -[ViewController printThread] thread:<QLThread: 0x60000365d7c0>{number = 3, name = (null)}
2019-06-21 23:53:38.163183+0800 RunLoop[71963:7473281] -[ViewController printThread] thread:<QLThread: 0x60000365d7c0>{number = 3, name = (null)}
2019-06-21 23:53:39.309686+0800 RunLoop[71963:7473281] -[ViewController printThread] thread:<QLThread: 0x60000365d7c0>{number = 3, name = (null)}

可以看到,每次点击屏幕执行打印任务的子线程都是那个我们保住的那个子线程,而不是重新创建的子线程。

需要注意的是:创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中至少要有一个Timer或一个Source保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃。

2、使用 RunLoop 解决NSTimer在滑动时停止工作的问题

RunLoop有几种模式, iOS中的定时器是在KCFRunLoopDefaultMode(默认模式)下工作的, 当我们在UIScrollView上滑动时, RunLoop就会切换成UITrackingRunLoopMode(界面跟踪模式), 这个时候定时器就会停止工作.

解决这个问题, 我们只要把定时器添加到这两种模式就可以了, 这样定时器在这两种模式下都可以工作了.

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];

3、使用 RunLoop 监控卡顿

导致卡顿的几种原因:

  • 复杂 UI、图文混排的绘制量过大
  • 在主线程上做网络同步请求
  • 在主线程做大量的 IO 操作
  • 运算量过大, CPU 持续高占用
  • 死锁和主子线程抢锁

对于 iOS 开发来说, 监控卡顿就是要去找到主线程上都做了哪些事. 线程的消息事件是依赖 RunLoop 的, 所以从 RunLoop 入手, 就可以知道主线程上都调用了哪些方法. 我们通过监听 RunLoop 的状态, 就能够发现调用方法是否执行时间过长, 从而判断出是否会出现卡顿.

如果 RunLoop 的线程, 进入睡眠前方法的执行时间过长而导致无法进入睡眠, 或者线程唤醒后接收消息时间过长而导致无法进入下一步的话, 就可以认为是线程受阻了. 如果这个线程是主线程的话, 表现出来的就是出现了卡顿.

我们利用 RunLoop 监控卡顿, 就是要关注这两个阶段. RunLoop在进入睡眠前和唤醒后的两个 loop 状态定义的值, 分别是 kCFRunLoopBeforeSource 和 kCFRunLoopAfterWaiting, 也就是触发 Source0 回调和接收 mach_port 消息两个状态.

4、自动释放池

Timer和Souce也是一些变量,需要占用一部分内存,所以要释放掉,如果不释放掉,就会一直积累,占用的内存就会越来越大。那么这些什么时候释放,怎么释放呢?

RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池。

最后记得再回顾一下开胃面试题

持续优化中...

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

推荐阅读更多精彩内容

  • iOS刨根问底-深入理解RunLoop 概述 RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时...
    reallychao阅读 820评论 0 6
  • 面试题 讲讲 RunLoop,项目中有用到吗? RunLoop内部实现逻辑? Runloop和线程的关系? tim...
    xx_cc阅读 35,731评论 17 236
  • 概述 RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多...
    sumrain_cloud阅读 943评论 0 5
  • 概述RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多数...
    飞天猪Pony阅读 514评论 0 7
  • 1. CFRunloopRef CFRunloopRef 是纯 C 的函数,而 NSRunloop 仅仅是 CFR...
    和风细羽阅读 600评论 0 1