面试遇到RunLoop的第一天-原理

2,114 阅读8分钟

RunLoop是什么?你了解嘛。RunLoop也是作为一名iOS manager必须了解的一个知识点,开发中可能只有用到timer的时候,接触过runloop.其实,对于iOS App来说,runloop是一个非常重要的东西,可以说runloop是支持程序运行的不可缺少的一部分。

什么是RunLoop

RunLoop顾名思义,就是运行循环,一个如此抽象的描述,可以理解为在程序运行过程中,循环做一些事情。那么他的应用范畴有哪些呢?比如

  • Timer
  • performSelector
  • GCD
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • autoreleasePool

吃惊嘛?真的上面说到的这么多的事都是runloop去处理的嘛?你可能Timer,GCD都用的很溜,可是却没想过,是什么支撑他们可以实现他们本身的功能的。甚至没想过,App为何可以打开之后一直停留在App内,而一个命令行程序为什么执行完就退出了呢?

RunLoop的基本作用

没错,RunLoop的基本作用就是保持程序可以持续运行,处理App的各种事件,比如触摸,定时器,界面更新等。RunLoop 的目的是,当有事件要去处理时保持线程忙,当没有事件要处理时让线程进入休眠,所以RunLoop还可以帮助我们节省CPU资源,帮助程序提高性能

简单来说,RunLoop 是用来监听输入源,进行调度处理的。这里的输入源可以是输入设备、网络、周期性或者延迟时间、异步回调。RunLoop 会接收两种类型的输入源:一种是来自另一个线程或者来自不同应用的异步消息;另一种是来自预订时间或者重复间隔的同步事件。

但是RunLoop这块的知识,我们研究起来会感觉比较难,比较底层,而且源码都是C语言,理解起来也比较不容易,所以,下面我们是抱着了解的态度去学习吧,把重点的地方认真理解,其他比如runloop的处理流程等,作为一个了解就可以了。

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

  • Foundation: NSRunLoop
  • Core Foundation: CFRunLoopRef

NSRunLoopCFRunLoopRef都是RunLoop对象,NSRunLoop是基于CFRunLoopRef的一层Objective-C的封装, CFRunLoopRef是完全开源的,源码在官网,大家感兴趣可以下载源码研究一下。

RunLoop与线程

RunLoop与线程的关系,也是面试中常遇到的问题,下面先说一下结论:

  1. 每条线程都有唯一一个与之对应的RunLoop对象,不会对应多个runloop,但是runloop可以嵌套
  2. 线程刚创建的时候并没有RunLoop对象,RunLoop会在第一次获取他时创建
  3. RunLoop保存在全局的Dictionary中,线程为key,RunLoop是value
  4. RunLoop会在线程结束时销毁
  5. 主线程的RunLoop已经自动创建,子线程默认没有开启RunLoop

对于以上结论呢,在后面的源码分析中,会一步步证实。

先说一下主线程的RunLoop已经自动创建,但是上面有说了线程刚创建的时候并没有RunLoop对象,RunLoop会在第一次获取他时创建,那主线程的RunLoop是在什么时候获取的呢?

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

主线程的RunLoop就是在UIApplicationMain这个函数中获取的,所以main函数执行完,主线程就有了自己的RunLoop,所以程序有RunLoop的支持也就不会退出。

获取RunLoop

FoundationCore Foundation都分别提供了获取RunLoop的方法

 [NSRunLoop mainRunLoop];//主线程对应的runloop
 [NSRunLoop currentRunLoop];//当前线程对应的runloop

 CFRunLoopGetCurrent();

在源码中找到__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

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

这里的_sources0_sources1_observers_timers是不是开始变得熟悉了。CFMutableSetRef可以理解为一个set集合类,内部的元素无序且不重复。

RunLoop的运行逻辑

上面看到的_sources0_sources1_observers_timers都分别在RunLoop中处理什么逻辑呢?他们各司其职,分别处理文章开头说过的RunLoop应用范畴内的任务:

  • sources0:触摸事件处理、performSelector:onThread:
  • sources1: 基于port的线程间通信、系统事件捕捉
  • timers: NSTimer、performSelector:withObject:afterDelay:
  • observers: 用于监听RunLoop的状态、UI刷新、Autorelaese Pool

CFRunLoopModeRef

CFRunLoopModeRef 代表RunLoop的运行模式,一个RunLoop可以包含多个mode,每个mode又可以包含多个sources0,sources1,observers,timers

RunLoop启动时只能选择其中一个mode作为currentMode,如果需要切换mode,只能退出当前RunLoop,再重新选择一个mode。这里要注意,切换mode并不会导致程序退出,哪怕是主线程的RunLoop切换,也不会。

但是,mode中如果没有任何的sources0,sources1,observers,timers,RunLoop就会立刻退出。

常见的mode有两种:

  • kCFRunLoopDefaultMode :App的默认mode,通常主线程是在这个mode下运行
  • UITrackingRunLoopMode :界面跟踪mode,用于scrollview追踪滑动触摸,保证界面滑动时不受其他mode影响

CFRunLoopObserverRef

CFRunLoopObserverRef是用来监听RunLoop状态的,状态是一个CFRunLoopActivity类型的枚举,共有下面这几种:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),        //即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),         //即将退出Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

创建observer 有两种方法,一种是带着block的,另外一种需要一个监听的方法

CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
#if __BLOCKS__
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity)) API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
#endif

demo中我用了两种方法分别测试了监听,注意添加observer到runloop后,还需要调用CFRelease释放一下

    //kCFRunLoopCommonModes 默认包括kCFRunLoopDefaultMode UITrackingRunLoopMode
//   创建observer 的两种方法
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
//    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
//        switch (activity) {
//            case kCFRunLoopExit:{
//                CFRunLoopMode model = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
//                NSLog(@"kCFRunLoopExit - %@",model);
//                CFRelease(model);
//            }
//                break;
//            case kCFRunLoopEntry:{
//                CFRunLoopMode model = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
//                NSLog(@"kCFRunLoopEntry- %@",model);
//                CFRelease(model);
//            }
//                break;
//
//            default:
//                break;
//        }
//    });
    // 添加observer到runloop
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);

用上面这段demo的代码,我们就可以监听到,timer是可以唤醒RunLoop的,以及scrollview滑动前后,mode的切换是需要退出loop再进入的。

RunLoop处理逻辑

关于RunLoop处理逻辑,我们只做一个了解就可以了,可以看看下面这种图,是大体的处理步骤,在研究源码的时候,可以对照这张图,帮助理解。

源码分析

开始分析源码,第一步肯定是要找到RunLoop的入口,比如断点在touchesBegan中,控制台通过 bt命令,查看所有的调用栈,就可以找到CFRunLoopRunSpecific

然后可以在源码中通过搜索找到CFRunLoopRunSpecific函数的实现,源码确实晦涩难懂,我们找到关键代码主要看调用流程就可以了,核心是调用了__CFRunLoopRun函数,得到result最后返回。__CFRunLoopRun中的实现就更加复杂了,当然也是在__CFRunLoopRun中,就可以找到上面RunLoop处理逻辑的每一个步骤对应的源码。

为了便于阅读就不再直接贴源代码,放一段伪代码方便大家阅读:

int32_t __CFRunLoopRun()
{
    // 通知即将进入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知将要处理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 处理非延迟的主线程调用
        __CFRunLoopDoBlocks();
        // 处理Source0事件
        __CFRunLoopDoSource0();
        
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks();
	     }
        /// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort();
            if (hasMsg) goto handle_msg;
        }
            
        /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
            
        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即将进入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待内核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // 等待。。。
        
        // 从等待中醒来
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 处理因timer的唤醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();
        
        // 处理异步方法唤醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
        // 处理Source1
        else
            __CFRunLoopDoSource1();
        
        // 再次确保是否有同步的方法需要调用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即将退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

现在只要了解上面的伪代码知道核心的方法__CFRunLoopRun内部其实是一个_do while_循环,这也正是Runloop运行的本质。执行了这个函数以后就一直处于“等待-处理”的循环之中,直到循环结束。只是不同于我们自己写的循环它在休眠时几乎不会占用系统资源,当然这是由于系统内核负责实现的,也是Runloop精华所在。

RunLoop休眠

RunLoop休眠,就是线程阻塞和普通的线程阻塞是不一样的 ,他是真的会让线程休眠 ,不做任何事,CPU也不分配资源,一直等待线程被唤醒,要做到这样的休眠,只有在内核层面的API才能办到。

所以RunLoop的休眠这里还存在一个用户态和内核态的切换,从用户态切换到内核态进入休眠,当收到唤醒线程的消息后,又切换到用户态处理消息。

这里使用的是mach_msg()方法,监听唤醒runloop的端口,这里还没深入研究过,就不展开说了。感兴趣的可以留言一起讨论学习一下。