iOS多线程——你要知道的RunLoop都在这里

你要知道的iOS多线程NSThread、GCD、NSOperation、RunLoop都在这里

转载请注明出处 http://www.jianshu.com/p/cfe5132e975f

本系列文章主要讲解iOS中多线程的使用,包括:NSThread、GCD、NSOperation以及RunLoop的使用方法详解,本系列文章不涉及基础的线程/进程、同步/异步、阻塞/非阻塞、串行/并行,这些基础概念,有不明白的读者还请自行查阅。本系列文章将分以下几篇文章进行讲解,读者可按需查阅。

RunLoop 基本概念

前面几篇文章详细讲解了创建多线程的方法和多线程编程的相关知识,当我们使用NSThread进行多线程编程时,只要任务结束,线程也就退出了,每次执行一个任务都需要创建一个线程非常浪费资源,所以需要一种能够使线程常驻内存不退出d,当有任务来临时能随时执行的方法,这就是RunLoop的作用。类似于javascriptEvent Loop模型,大致类似于如下代码:

int retVal = Running;
do {
     // 执行各种任务,处理各种事件
     // ......
} while (retVal != Stop && retVal != Timeout);

上述循环只有在特定条件才才会退出,否则就会一直在循环中处理各种任务或事件,诸如触摸屏幕事件、手势事件、定时器事件、用户提交的任务、各种方法的执行等。

RunLoop与线程关联的,是一种事件处理环,用来安排和协调到来的事件,目的就是让其关联的线程在有事件到达时时刻保持运行状态,而当没有事件需要处理时进入睡眠状态从而节约资源,每一个线程都可以有一个RunLoop对象与之对应,并且是在第一次获取它是系统自动创建的,比如主线程关联的RunLoop,我们都知道程序的入口函数是main函数,下面是创建工程后Xcode自动生成的main.m文件的main函数代码:

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

该方法执行体被autoreleasepool包围,所以程序可以使用ARC来管理内存,后面会讲解RunLoopautoreleasepool的关系,main函数直接返回了UIApplicationMain函数,该函数内部就会第一次获取RunLoop对象,所以系统就会创建这样一个RunLoop对象,因此在没有满足特定条件的时候该主线程不会退出,应用就可以持续运行而不会退出。

在官方文档中使用下图描述RunLoop模型:

官方RunLoop模型图

从上图可以看出一个线程会关联一个RunLoop对象,RunLoop对象会一直循环,直到超时或收到退出指令。在无限循环的过程中会一直处理到来的事件,右侧将事件分为了两类,一类是Input sources这部分包括基于端口的source1事件,开发者提交的各种source0事件,调用performSelector:onThread:方法事件,还有一类Timer sources这个就是常用的定时器事件,这些事件在程序运行期间会不断产生之后会由RunLoop对象检测并负责处理相关事件。

RunLoop 源码解析

RunLoop有两个对象,NSRunLoopCFRunLoopRef,区别在于由Core Foundation框架提供的CFRunLoopRef是纯C语言编写的,提供的也是C语言接口,这些接口都是线程安全的,由Foundation框架提供的NSRunLoop是面向对象的,它是基于CFRunLoopRef的封装,提供的都是面向对象的接口,但这些接口不是线程安全的,Core Foudation框架是开源的,可以在这个地址下载:Core Foundation开源代码,本文接下来的内容主要是针对该开源代码进行讲解。

首先,看一下在代码中如何获取RunLoop对象,在Foundation框架中的NSRunLoop类提供了如下两个类属性:

//获取当前线程关联的RunLoop对象
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
//获取主线程关联的RunLoop对象
@property (class, readonly, strong) NSRunLoop *mainRunLoop

对应的Core Foundation框架中提供了如下两个函数来获取RunLoop对象:

//获得当前线程关联的RunLoop对象
CFRunLoopGetCurrent(); 
// 获得主线程关联的RunLoop对象
CFRunLoopGetMain();

前面一直讲每一个线程都会关联一个RunLoop对象,并且不能通过手动创建该对象,只能在第一次获取时系统自动创建,看一下Core Foundation框架是如何实现的:

//CFRunLoopGetMain函数用于获取主线程关联的RunLoop对象
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    //静态变量保存主线程关联的RunLoop对象
    static CFRunLoopRef __main = NULL; // no retain needed
    //如果主线程关联的RunLoop对象为NULL就调用_CFRunLoopGet0函数获取一个
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main
}

//获取当前线程关联的RunLoop对象
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    //这一段没找到对应的函数...猜测是和上面的函数用意一样
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    //如果上面没找到就调用_CFRunLoopGet0函数去获取一个
    return _CFRunLoopGet0(pthread_self());
}


//全局的可变字典数据结构,key为thread_t即线程,value为RunLoop对象
static CFMutableDictionaryRef __CFRunLoops = NULL;
//全局的一个锁
static CFLock_t loopsLock = CFLockInit;

//_CFRunLoopGet0接收一个pthread_t对象,即线程对象,返回一个与之关联的RunLoop对象
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //判断是否为主线程
    if (pthread_equal(t, kNilPthreadT)) {
    //pthread_main_thread_np()函数用来获取主线程
    t = pthread_main_thread_np();
    }
    //加锁,防止产生竞争创建多个RunLoop对象
    __CFLock(&loopsLock);
    //如果全局的保存线程和runloop对象的字典为空
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    //创建一个字典
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    /*
    根据主线程创建RunLoop对象
    所以,当第一次获取RunLoop对象时就会自动创建主线程关联的RunLoop对象
    */
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    //设置全局的字典,key为主线程,value为主线程关联的RunLoop对象
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    //通过线程在字典中获取RunLoop对象
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    //如果没有获取到
    if (!loop) {
    //没有获取到就根据线程创建一个RunLoop对象
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    //再次获取
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        //字典中仍然没有线程关联的RunLoop对象就将刚才新创建加入到字典照中
        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);
        }
    }
    //返回线程关联的RunLoop对象
    return loop;
}

/*
真正的用于创建RunLoop对象的静态函数,形参为线程对象
该函数主要用于分配存储空间,并进行RunLoop对象相关初始化操作
*/
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
    CFRunLoopRef loop = NULL;
    CFRunLoopModeRef rlm;
    uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
    loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
    if (NULL == loop) {
    return NULL;
    }
    (void)__CFRunLoopPushPerRunData(loop);
    __CFRunLoopLockInit(&loop->_lock);
    loop->_wakeUpPort = __CFPortAllocate();
    if (CFPORT_NULL == loop->_wakeUpPort) HALT;
    __CFRunLoopSetIgnoreWakeUps(loop);
    loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
    loop->_commonModeItems = NULL;
    loop->_currentMode = NULL;
    loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
    loop->_blocks_head = NULL;
    loop->_blocks_tail = NULL;
    loop->_counterpart = NULL;
    loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
    loop->_winthread = GetCurrentThreadId();
#else
    loop->_winthread = 0;
#endif
    rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
    if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
    return loop;
}

通过上面源码不难发现,RunLoop对象保存在一个全局的字典中,该字典以线程对象pthread_tkey,以RunLoop对象为value,并且,在第一次获取RunLoop对象时总会先把主线程关联的RunLoop对象创建好,在获取其他线程关联的RunLoop对象时都从这个全局的字典中获取,如果没有获取到就创建一个并且添加进字典中,所以每一个线程有且仅有一个与之关联的RunLoop对象,重要的是,如果不获取线程关联的RunLoop对象,那么这个RunLoop对象就不会被创建。当线程退出时,也会将RunLoop对象销毁。

接下来查看一下CFRunLoopRef具体的数据结构如下:

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

typedef struct __CFRunLoop * CFRunLoopRef;

上述数据结构中比较重要的就是_commonModes_commonModeItems_currentMode以及_modes,具体关系如下图所示,该图取自文章深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/下面讲的内容也有参考该博客中的内容,建议读者阅读原文:

RunLoop数据结构

上图很好的描述了struct __CFRunLoop数据结构相关成员变量的关系,每一个__CFRunLoop对象可以包含数个不同的Mode,而每一个Mode又包含了数个SourceObserverTimer,当一个RunLoop运行时只能选择其中的某一个Mode来执行,如果要切换Mode则需要退出运行后指定一个新的Mode后重新执行运行。通过这样的方式,可以在不同Mode中设置不同的Source/Observer/Timer而不同的Mode中间的这三部分互不影响,也就是说,有些Source/Observer/Timer只能在某一个Mode中运行,当RunLoop运行在其他Mode中,该事件得不到处理。

Source CFRunLoopSourceRef

SourceCFRunLoopSourceRef类的对象,指代事件源,即前文官方结构图中的Input Source,在官方文档中该事件源Source分为三类:

  • Port-Based Sources 基于端口的,也称为source1事件,通过内核和其他线程通信,接收到事件后包装为source0事件后分发给其他线程处理。

  • Custom Input Sources 用户自定义

  • Cocoa Perform Selector Sources 调用诸如perfromSelector:onThread:这样的方法产生的事件

按照调用栈来说其实只分成两类,Source0不基于端口的和Source1基于端口的,分类方式并不是很重要,了解即可。

Timer CFRunLoopTimerRef

Timer可以理解为定时器即NSTimer,因为CFRunLoopTimerRefNSTimertoll-free bridged,所以可以互相转换,将其理解为NSTimer即可,RunLoop对象会在注册的定时器时间到达时唤醒关联的线程对象来执行定时器的回调。

Observer CFRunLoopObserverRef

Observer就是监听器,用来监听RunLoop的各种状态,在源码中有如下监听状态的枚举定义:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    //即将进入RunLoop的执行循环
    kCFRunLoopEntry = (1UL << 0),
    //即将处理Timer事件
    kCFRunLoopBeforeTimers = (1UL << 1),
    //即将处理Source事件
    kCFRunLoopBeforeSources = (1UL << 2),
    //RunLoop即将进入休眠状态
    kCFRunLoopBeforeWaiting = (1UL << 5),
    //RunLoop即将被唤醒
    kCFRunLoopAfterWaiting = (1UL << 6),
    //RunLoop即将退出
    kCFRunLoopExit = (1UL << 7),
    //监听RunLoop的全部状态
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Observer中定义了一系列的监听器,开发者也可以使用监听器来监听具体的状态改变,具体栗子后文会介绍。

Mode CFRunLoopModeRef

ModeRunLoop中比较重要的部分,系统默认为我们提供了五种Mode:

  • kCFRunLoopDefaultMode 即 NSDefaultRunLoopMode,默认运行模式

  • UITrackingRunLoopMode 跟踪UIScrollView滑动时使用的运行模式,保证滑动时不受其他事件处理的影响,保证丝滑

  • UIInitializationRunLoopMode 启动应用时的运行模式,应用启动完成后就不会再使用

  • GSEventReceiveRunLoopMode 事件接收运行模式

  • kCFRunLoopCommonModes 即 NSRunLoopCommonModes 是一种标记的模式,还需要上述四种模式的支持

UITrackingRunLoopMode只有当用户滑动屏幕时,即滑动UIScrollView时才会执行的模式,此时,不在该模式内的Source/Timer/Observer都不会得到执行,它仅仅专注于滑动时产生的各种事件,通过这样的方式就可以保证用户在滑动页面时的流畅性,这也是分不同Mode的优点。

具体数据结构如下:

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

从上述数据结构中可以看出,Mode内部管理了一个_source0的事件集合,一个_source1的事件集合,一个_observers的数组以及_timers的数组,这也印证了前文中关于Mode的图例,再结合之前讲的__CFRunLoop中比较重要的几个成员变量:

struct __CFRunLoop {
    ...
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
}

其中_currentMode即代表当前RunLoop对象正在执行的ModeCFRunLoopModeRef类的对象。

_mode是一个Set集合保存了所有该RunLoop对象可以执行的Mode

_commonModes保存的是具有Common属性的Mode的名称,前文__CFRunLoopMode的结构体定义中可以看到,每个Mode管理自己的Source/Timer/Observer,而被标记为Common属性的Mode还有一个特性就是当RunLoop对象在执Common属性的Mode时,会自动将_commonModeItems中保存的Source/Observer/Timer同步添加该Mode中,标识Common属性只需要将__CFRunLoopModeRef_name成员变量的值添加进_commonModes集合中即可。被标记为Common属性的Mode就是前文讲的kCFRunLoopCommonModes模式,可以看出这种模式不是一种真正的模式,仅仅是标识其他模式是否需要同步添加_commonModeItems中的Source/Timer/Observer

_commonModeItems中保存的就是那些需要同步添加到具有Common属性的Mode中的Source/Timer/Observer集合。

系统默认将kCFRunLoopDefaultModeUITrackingRunLoopMode添加到了_commonModes中,即标识为Common属性,所以当RunLoop运行在这两种模式中会自动同步添加_commonModeItems中的Source/Timer/Observer

举个常见的栗子:

- (void) viewWillAppear:(BOOL)animate
{
    [super viewWilAppear:YES];
    //创建一个NSTimer的对象,从当前时间开始每1s输出一次Hello,World
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"Hello, World");
    }];
    //将timer加入到当前线程关联的RunLoop对象的NSDefaultRunLoopMode中
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    //类方法,创建一个timer并添加到当前线程关联的RunLoop的NSDefaultRunLoopMode中
    [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"Hello, World222");
    }];
}

上面的栗子创建了两个NSTimer,这两个定时器执行效果相同,但如果页面中有一个UIScrollView或其子类的对象在滑动时,NSTimer就不会再有任何输出,当停下滑动时又会有输出,因为上述代码创建的两个NSTimer都加入到了RunLoop对象的NSDefaultRunLoopMode中,在滑动时RunLoop会切换到UITrackingRunLoopMode模式下执行,而UITrackingRunLoopMode中没有上述定时器,所以不会执行,当停止滑动时RunLoop对象又切换到了NSDefaultRunLoopMode模式,所以可以继续执行定时器的回调。

为了解决这个问题,可以将NSTimer即加入到NSDefaultRunLoopMode中,又加入到UITrackingRunLoopMode中,同一个Source/Timer/Observer可以添加到不同的Mode中,但同一个Source/Timer/Observer不能添加到同一个Mode中,这样不会有任何效果,但添加到两个Mode中并不是最好的解决方案,还有一个方案就是利用前面的Common属性,NSDefaultRunLoopModeUITrackingRunLoopMode都被添加进了_commonModes集合中被标识了具有Common属性,所以在运行时就会自动将_commonModeItems中的Source/Timer/Observer同步添加到其中,因此,只需要将创建的NSTimer加入到_commonModeItems中即可,此时只需要使用NSRunLoopCommonModes即可,代码如下:

- (void) viewWillAppear:(BOOL)animate
{
    [super viewWilAppear:YES];
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"Hello, World");
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

NSTimer加入到NSRunLoopCommonModes中就是把其加入到_commonModeItems集合中,这样在滑动时就会自动同步添加NSTimerUITrackingRunLoopMode模式下,所以定时器也可会得到执行。如果需要注意使用类方法scheduledTimerWithTimeInterval:repeats:block时要注意该方法默认是加入到NSDefaultRunLoopMode模式中的。

通过上述讲解,可以发现,NSTimer其实是不那么精确的,首先,在使用时需要加入到RunLoop中,如果加在CommonMode在普通情况或滑动时都可以执行回调方法,这个时候的误差就来自于RunLoop一次循环的执行延迟,最坏情况下,RunLoop一次循环需要执行的任务较多,NSTimer回调执行的延迟就会加大。如果加在其他模式下,当模式切换时就不会再执行NSTimer的回调方法了,所以,在使用时需要根据情况选择不同的定时器以满足项目需求。

在查看RunLoop运行机制前,做一个小实验,创建一个视图控制器,并添加一个按钮,在按钮点击事件的回调函数中打一个断点,然后运行程序点击按钮,之后查看调用栈如下图所示:

按钮点击的调用栈

从上图中可以看到程序在18处执行main函数,17执行UIApplicationMain函数,这就是程序启动过程,16是系统内部事件,15调用CFRunLoopRunSpecific后文会详细讲解该函数,14开始执行RunLoop进入循环,13开始处理source0这个source0就是点击按钮的事件,11是真正执行source0的函数,10-0就是点击事件的整个转发处理过程,最终交由我们自定义的回调方法进行处理。

RunLoop 执行逻辑

在官方文档中描述的RunLoop循环中的执行逻辑如下:

  1. 通知监听器RunLoop进入循环

  2. 通知监听器即将处理Timer事件

  3. 通知监听器即将处理source0(不是基于端口的)事件

  4. 执行source0事件

  5. 如果有source1(基于端口的)事件则立即执行跳转到第九步

  6. 通知监听器RunLoop即将进入休眠状态

  7. 将线程休眠,直到以下事件发生才会被唤醒:

    • 有source1事件到达
    • 定时器触发时间到达
    • RunLoop对象的超时时间过期
    • 被外部显示唤醒
  8. 通知监听器RunLoop对象即将被唤醒

  9. 处理添加进来的事件,包括:

    • 如果用户定义的定时器时间到达,执行定时器时间并重启循环,跳转到第二步
    • 如果有source1事件,传递这个事件
    • 如果RunLoop被显示唤醒并且没有超时则重启RunLoop,跳转到第二步
  10. 通知监听器RunLoop退出循环

为了验证上述执行顺序,可以自行编写监听器来监听RunLoop对象状态的改变,具体栗子如下:

- (void)viewWillAppear:(BOOL)animated
{
    // 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"Status has changed into: %zd", activity);
    });    
    
    /*
    将监听器添加到当前RunLoop对象中,在RunLoop循环中就会执行上述回调块
    监听的是kCFRunLoopDefaultMode即默认状态
    也可以使用kCFRunLoopCommonModes,同时监听默认状态以及滑动视图的状态
    */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    //CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);

    //Core Foundation需要手动释放observer
    CFRelease(observer);
    
    //添加一个textView,它是UIScrollView的子类
    UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 20, ScreenWidth, 300)];
    textView.text = @"Hello, World";
    [textView setBackgroundColor:[UIColor redColor]];
    [self.view addSubview:textView];
}

为了减少输出选择监听kCFRunLoopDefaultMode模式,启动程序后不做任何操作发现其输出如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    //即将进入RunLoop的执行循环 1
    kCFRunLoopEntry = (1UL << 0),
    //即将处理Timer事件 2
    kCFRunLoopBeforeTimers = (1UL << 1),
    //即将处理Source事件 4
    kCFRunLoopBeforeSources = (1UL << 2),
    //RunLoop即将进入休眠状态 32
    kCFRunLoopBeforeWaiting = (1UL << 5),
    //RunLoop即将被唤醒 64
    kCFRunLoopAfterWaiting = (1UL << 6),
    //RunLoop即将退出 128
    kCFRunLoopExit = (1UL << 7),
    //监听RunLoop的全部状态
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64

从输出中不难发现,不做任何操作时程序处于NSDefaultRunLoopMode模式下,一直在2-9步间循环,当没有事件要处理时就转入了休眠状态,之后又被唤醒继续处理,可能有读者疑惑为什么连续那么多次都是2 4的输出,状态2表示即将处理Timer,状态4表示即将处理Source,接着就会处理Source,但如果有source1的存在(基于端口的事件)就不会休眠直接跳转到第九步处理相关事件,处理完成之后又回到第二步,所以产生上述输出。

此时当我们将UITextView中添加多个换行符直到滚动条出现后,滑动UItextView会发现有如下输出:

Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 128

128代表RunLoop对象退出循环了,因为当我们滑动UItextView时,RunLoop对象切换到了UITrackingRunLoopMode,前文讲过,RunLoop对象每次执行时只能执行在一个模式下,如果要切换模式只能退出后重新进入循环,从上述输出就证明了这一点。

接下来看一下RunLoop的执行源码。

RunLoop执行的入口函数

RunLoop对外只提供了两个入口函数

/*
RunLoop对外提供的入口函数
用户可以显示调用后使当前线程关联的RunLoop对象以默认模式运行
*/
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    //返回值
    int32_t result;
    //循环体,直到RunLoop停止或者结束时才会终止循环
    do {
        /*
        调用CFRunLoopRunSpecific启动RunLoop
        执行的RunLoop就是当前线程关联的RunLoop对象
        超时时间100亿秒,317.098年,永不超时
        */
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

/*
RunLoop对外提供的入口函数
用户可以显示调用后使当前线程关联的RunLoop对象以指定模式、超时时间运行
*/
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRunSpecific

/*
按照指定的模式、超时时间以及条件运行RunLoop
rl: 要运行的RunLoop对象
modeName: RunLoop对象要执行的模式名称
seconds: RunLoop循环的超时时间
returnAfterSourceHandled: 是否在处理完source后就退出RunLoop循环
*/
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //通过Mode的名称查找CFRunLoopModeRef对象
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    /*
    如果没有获取到Mode
    或Mode的内容为空,内容为空即Mode的Source/Timer/Observer集合都没有数据
    为空就直接返回,并不真正执行RunLoop的循环
    */
    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;

    /*
    调用__CFRunLoopDoObservers触发监听器,RunLoop即将进入循环
    */
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //调用真正执行RunLoop循环的函数
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    /*
    调用__CFRunLoopDoObservers触发监听器,RunLoop退出循环
    */
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

上述代码做了一系列的处理,比如通过名称查找Mode,判断Mode是否为空,即判断Mode中是否还有Source/Timer/Observer,其中比较重要的函数有__CFRunLoopFindMode函数,该函数在查找根据Mode名称查找时,如果没有找到会尝试创建一个新的Mode,如果创建失败才会返回NULL__CFRunLoopModeIsEmpty函数用来判断Mode中的Source/Timer/Observer是否为空,如果集合中没有对象就返回true__CFRunLoopDoObservers用来触发监听器的回调函数或回调块,前文举的栗子在创建监听器并加入到RunLoop对象后,其实是将这个监听器加入到了Mode_observers数组中,所以该函数内部会遍历对应数组并调用回调函数或回调块来进行通知。

接下来就要查看__CFRunLoopRun函数的实现,但该函数源码太长有三百多行,而且包含了不少跨平台的预编译指令,由于篇幅的问题,这里不直接分析了,有兴趣的读者可以参考本系列文章第五篇iOS多线程——RunLoop与GCD、AutoreleasePool,在这篇文章中会详细讲解该函数的源码,那这里直接使用深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/中作者简化整理版本,代码如下:

__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
    
    Boolean sourceHandledThisLoop = NO;
    int retVal = 0;
    do {
        
        /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
        /// 执行被加入的block
        __CFRunLoopDoBlocks(runloop, currentMode);
        
        /// 4. RunLoop 触发 Source0 (非port) 回调。
        sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
        /// 执行被加入的block
        __CFRunLoopDoBlocks(runloop, currentMode);
        
        /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
            if (hasMsg) goto handle_msg;
        }
        
        /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
        
        /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
        /// • 一个基于 port 的Source 的事件。
        /// • 一个 Timer 到时间了
        /// • RunLoop 自身的超时时间到了
        /// • 被其他什么调用者手动唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
            mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
        }
        
        /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
        
        /// 收到消息,处理消息。
    handle_msg:
        
        /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
        if (msg_is_timer) {
            __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
        }
        
        /// 9.2 如果有dispatch到main_queue的block,执行block。
        else if (msg_is_dispatch) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        }
        
        /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
        else {
            CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
            sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
            if (sourceHandledThisLoop) {
                mach_msg(reply, MACH_SEND_MSG, reply);
            }
        }
        
        /// 执行加入到Loop的block
        __CFRunLoopDoBlocks(runloop, currentMode);
        
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            /// 进入loop时参数说处理完事件就返回。
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout) {
            /// 超出传入参数标记的超时时间了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(runloop)) {
            /// 被外部调用者强制停止了
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
            /// source/timer/observer一个都没有了
            retVal = kCFRunLoopRunFinished;
        }
        
        /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
    } while (retVal == 0);
}

上面执行代码就和前面官方文档中讲解的顺序一致,不再赘述。

在前文给了一个点击按钮的调用栈运行图,可以发现执行source0事件时是调用了一个非常长的函数来处理,为了方便查看调用栈执行的顺序,深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/一文中,作者将整个RunLoop响应函数按执行顺序列了下来,如下:

{
    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

参考文章

深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。

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

推荐阅读更多精彩内容