RunLoop源码学习

通常我们开发iOS app时接触到的是NSRunLoop,而NSRunLoop实际上是对苹果的Core Foundation框架中CFRunLoop的封装,这次我们直接通过官方文档和Core Foundation源码学习CFRunLoop

Core Foundation是纯C版本的实现,苹果已经开源了Core Foundation的源码,相关链接: 官方文档 源码下载

CFRunLoop

头文件中的声明

typedef struct __CFRunLoop * CFRunLoopRef;

__CFRunLoop的定义(只保留了主要属性)

struct __CFRunLoop {
    CFMutableSetRef _commonModes;//common mode集合,后边会讲
    CFMutableSetRef _commonModeItems;//source集合
    CFRunLoopModeRef _currentMode;//当前生效的mode
    CFMutableSetRef _modes;//当前runloop的所有mode
    struct _block_item *_blocks_head;//block链表头
    struct _block_item *_blocks_tail;//block链表尾部
};

可以看到,一个run loop对象包含的元素并不多,其中_commonModes_commonModeItems_currentMode_modes都跟mode有关,剩余两个属性是一个链表,用来保存block。

那么mode是什么,接下来看一看mode的定义

CFRunLoopMode

如下是CFRunLoopModeRef的定义:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFStringRef _name;//mode的名字
    Boolean _stopped;//用于退出当前mode的方法
    //输入源 source
    CFMutableSetRef _sources0;//非port相关事件(如用户点击)
    CFMutableSetRef _sources1;//port相关的事件(系统事件)
    //监听者 observer
    CFMutableArrayRef _observers;
    CFIndex _observerMask;//监听者注册的监听事件
    //计时器 timer
    CFMutableArrayRef _timers;
};  

一个mode包含三种类型的对象,sources(CFRunLoopSource)、timers(CFRunLoopTimer)和observers(CFRunLoopObserver),要接受run loop的回调,必须依赖这三种对象。他们和run loop以及mode之间的关系如下:

image.png

每一个线程对应一个run loop对象,每一个run loop包含多个mode,每一个mode包含多个source、timer以及observer。

每个source、timer和observer必须添加到一个或多个mode中才能生效,但是run loop同时只能运行一个mode,如果当前运行的mode不是添加的mode,则不会生效。

举个UIScrollView的例子

主线程的run loop默认运行的是NSDefaultRunLoopMode,而当滑动UIScrollView时,主线程的run loop会切换到UITrackingRunLoopMode

如果在主线程的NSDefaultRunLoopMode中加入了一个NSTimer,当用户滑动UIScrollView的时候,主线程会将run loop切换到UITrackingRunLoopMode,这时这个NSTimer就不会生效。直到用户停止滑动时,主线程将run loop切换回NSDefaultRunLoopMode,此时计时器才会生效。

系统定义了一些mode,常见的有

  • 默认mode:NSDefaultRunLoopMode(Cocoa) / kCFRunLoopDefaultMode(Core Foundation)
  • 事件跟踪:UITrackingRunLoopMode(Cocoa)
  • Common Modes:NSRunLoopCommonModes(Cocoa) / kCFRunLoopCommonModes(Core Foundation)

当我们需要执行一些优先级较高的任务时,也可以自定义mode,限制一些低优先级的事件,保证高优先级任务的执行。

每一个Mode通过name区分,Core Foundation没有对外暴露run loop mode的接口,使用者只需要关心mode的name即可,如NSDefaultRunLoopModekCFRunLoopDefaultModeUITrackingRunLoopMode,他们都是string类型(可以通过toll-free bridge转换)。

kCFRunLoopCommonModes

__CFRunLoop的定义中,如下两句定义了common modes相关的变量:

CFMutableSetRef _commonModes;//common mode集合
CFMutableSetRef _commonModeItems;//source/timer/observer的集合

Common modes不是一个mode,而是很多mode的集合。被添加到common modes中的mode,会存储在_commonModes中,同时会将_commonModeItems中的元素添加到这个mode中,这样_commonModeItems可以被_commonModes包含的每一个mode运行时监听到。

从使用者的角度来说,当添加了一个timer到NSRunLoopCommonModes中,无论当前run loop运行在哪一种mode下,只要这个mode在common modes集合中,这个NSTimer就会生效。

再拿上面UIScrollView举个例子

在主线程中,NSDefaultRunLoopModeUITrackingRunLoopMode这两个mode被系统加入了common modes中。这意味着,我们如果将NSTimer加入到common modes中,也就是添加到kCFRunLoopCommonModes,此时无论用户是否滑动UIScrollView,这个NSTimer都会生效。

这里有两种方式与common modes打交道,一个是添加自定义mode到common modes中,另一个是添加sources/timers/observers到kCFRunLoopCommonModes,来看看对应的实现

1. CFRunLoopAddCommonMode
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    //判断common modes是否已经存在该mode
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
        //获取common modes set
        CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
        //添加mode进common modes set
        CFSetAddValue(rl->_commonModes, modeName);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, modeName};
            //将common mode items中的每一个item添加到传入的mode中
            CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
        }
    }
}

当调用CFRunLoopAddCommonMode函数时,会将新的mode加入到该runloop的common modes集合中,同时,会将当前common mode items中的所有元素,添加到新的mode中。

如此一来,无论run loop被切换到哪一个mode,只要这个mode被加入到kCFRunLoopCommonModes中,就可以响应那些被添加到kCFRunLoopCommonModes的sources/timers/observers。

2. CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer

可以在将source/timer/observer三种对象加入到run loop时传入kCFRunLoopCommonModes参数,这样run loop运行在common modes的mode时,这些source/timer/observer就会生效。这里仅放一个CFRunLoopAddTimer中与common modes有关的代码,其他两种(source/observer)代码都是类似的。

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {   
    //判断是否是将timer加入到kCFRunLoopCommonModes
    if (modeName == kCFRunLoopCommonModes) {
        //获取common modes集合
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        //如果common modes items为空,则创建一个common modes items的集合
        if (NULL == rl->_commonModeItems) {
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        //将传入的timer source加入到common mode items中
        CFSetAddValue(rl->_commonModeItems, rlt);
        //将timer添加到common modes中的每一个mode中
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rlt};
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
        }
    } else {
      //非common mode的操作
    }
}

将一个timer加入到kCFRunLoopCommonModes中后,会将这个timer加入到common mode items里,同时也会将这个timer加入到common modes集合中的每一个mode中,也就是说,该函数会帮你更新所有在common modes集合中的mode,这样就达到了无论运行在任意一个common mode时,都可以使这个timer生效。

CFRunLoopSource/CFRunLoopTimer/CFRunLoopObserver

我们可以通过将source/timer/observer这三种对象加入到run loop中,当有事件发生时,通过回调接受通知。在加入到run loop时,必须指定一个mode。当然也可以从一个run loop中移除上述三种对象。

CFRunLoopSourceRef

Input source是事件发生的来源,通常产生异步事件,比如消息到达网络端口或者用户执行的操作。它的定义如下:

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;//toll-free bridge
struct __CFRunLoopSource {
    CFMutableBagRef _runLoops;
    union {
            CFRunLoopSourceContext version0;//source0
        CFRunLoopSourceContext1 version1;//source1
    } _context;
};

CFRunLoopSourceRef中,最主要的是两个变量CFRunLoopSourceContextCFRunLoopSourceContext1,它们用union来声明,意味着有两种不同的source,source0和source1。

source0

source0是一般响应应用程序事件,例如按钮的响应事件。当需要发送source0事件时,调用CFRunLoopSourceSignal函数将这个source标记为待触发,但是该函数不能唤醒runloop,需要再调用CFRunLoopWakeUp方法唤醒对应的run loop,这个source0事件才会被触发。Core Foundation中的CFSocket使用的是source0的方式实现。定义如下:

typedef struct {
    CFIndex version;//0代表是source0
    void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

它包含三个回调,

  • schedule:当添加source0到run loop时,会调用一次schedule回调方法;
  • perform:当触发source0时,会调用perform回调方法;
  • cancel:当移除或者run loop销毁时,会调用cancel回调方法。
source1

source1由run loop和内核管理,使用mach port进行通信,用于通过内核进行进程间通信,也可以用于线程间的通信。source1能够将run loop唤醒。Core Foundation中的CFMachPortCFMessagePort通过source1的方式实现。定义如下:

typedef struct {
    CFIndex version;//1代表source1
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#endif
} CFRunLoopSourceContext1;

其中

  • getPort函数指针提供一个获取port的函数
  • perform函数是回调函数

有趣的是,CFMessagePort通常用于进程间通信,如iOS越狱开发,常用的场景是前端有一个UI程序用于界面展示,后端有一个daemo精灵程序用于任务处理。而官方文档中有提到,CFMessagePort已不能再iOS7之后的系统中使用。来源。

CFRunLoopSourceRef的使用

Core Foundation提供了提供了与input sourc交互的函数

  • CFRunLoopSourceGetContext:创建source0或者source1变量
  • CFRunLoopSourceCreate:创建CFRunLoopSourceRef
  • CFRunLoopAddSource:添加CFRunLoopSourceRef到run loop中

CFRunLoopTimerRef

CFRunLoopTimerRef是和NSTimer toll-free bridge的计时器。Run loop timer并不一定可靠,如果加入的mode没有运行或者当前run loop在执行一段耗时的操作,run loop timer可能不会被触发。而且Run loop timer的触发时间依赖于计划的时间间隔,而不是实际运行的时间间隔,比如一个5秒重复的计时器,在第二次时延时了2秒,那么第三次的执行时间不会改变,仍然是第15秒时执行。

Run loop timer可以同时加入到多个run loop mode中,但中只会在第一个加入的run loop内生效。

CFRunLoopTimerRef主要包含了interval参数和Callback回调方法。

Run loop timer关于时间计算的机制以后再深入了解

CFRunLoopObserverRef

CFRunLoopObserverRef是一个观察者,它包含了一个回调指针和一个Activity参数,用于表明接受的run loop事件,具体事件定义如下,当run loop处于不同状态时,会通知obsever。

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

blocks

__CFRunLoop的定义中,还有两个关于block链表的定义

struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;

Run loop还提供了一个CFRunLoopPerformBlock函数用于添加block,和source0类似,这个方法不会主动唤醒run loop,需要调用CFRunLoopWakeUp函数主动唤醒run loop。

CFRunLoopPerformBlock的定义如下

void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void))

在run loop运行的每一次循环中,会多次检查当前是否有需要执行的block,如果有则会执行传入的block

CFRunLoopGet

在通常开发时,我们通常不需要关心run loop的生命周期。系统会自动在主线程帮我们创建run loop对象。可以通过如下接口获得主线程的run loop对象,该方法返回一个CFRunLoopRef实例。

CFRunLoopRef CFRunLoopGetMain(void);

当我们创建一个新的线程时,默认是没有初始化run loop对象的,需要调用函数获取当前线程的run loop对象。

CFRunLoopRef CFRunLoopGetCurrent(void);

如此看来,创建run loop对象的函数就是Core Foundation内部实现的了,CFRunLoopGetMainCFRunLoopGetCurrent这两个函数内部都会调用同一个方法_CFRunLoopGet0(pthread_t t),来看下具体的代码。

这里简单提一下TSD(thread specific data)的概念,在多线程环境中,因为数据空间是共享的,所以全局变量也为所有线程所共有。所以当需要一个仅在当前线程中可以访问的数据时,使用TSD来存储。TSD存储的数据仅在当前线程有效,但是可以跨函数访问。

run loop对象就存储在TSD中。

CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //__CFRunLoops是全局保存runloop对象的dict,首次运行时初始化该dict
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        //创建全局ditc,并添加主线程的runloop 对象
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //通过__CFRunLoopCreate方法创建主线程的run loop对象
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        //将dict与__CFRunLoops指针互换,然后释放ditc
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
    }
    //通过线程对象获取对应的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    //如果该线程还没有创建runloop对象,那么初始化该线程的runloop对象
    if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
    }
    //判断是否获取当前线程的runloop对象
    if (pthread_equal(t, pthread_self())) {
        //将run loop对象存放到TSD中
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            //在这里设置销毁的回调方法,当线程生命周期结束时销毁runloop对象
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

该函数的流程如下

  1. 第一次进入时,会创建一个全局的dict,用于存储每一个线程对应的run loop对象,同时也会初始化主线程的run loop对象。
  2. 根据传入的线程获取对应的run loop对象,若为空,则创建一个run loop对象,并添加到全局dict中。同时也会将这个run loop放到对应线程的TSD中并设置一个线程结束时的销毁函数回调。

通过上面的函数,我们可以获取到当前线程的run loop对象。在执行前面所讲的添加source/timer/observers函数时,都需要传入run loop对象。

系统会启动主线程的run loop的运行,对于其他线程,需要我们在获取run loop对象后主动启动。

CFRunLoopRun

首先通过一张图,了解下CFRunLoopRun的主要逻辑。

image.png

PS:图中左边的source0(port)应该是source1(port)

在Core Foundation提供了两个函数供我们启动run loop,CFRunLoopRunCFRunLoopRunInMode,函数声明如下:

//无参数,直接启动run loop,运行在default mode
void CFRunLoopRun(void);
//设置run loop运行的mode,有效期以及是否在运行source 0事件后直接退出,返回值为run loop退出的原因字段
SInt32 CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

这两个函数都会调用CFRunLoopRunSpecific函数,该函数实现如下,注释中的数字对应图中的顺序

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
    //判断mode中是否有source,timer或者block事件,如果没有,run loop会立即退出
    if (__CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        return kCFRunLoopRunFinished;
    }
    int32_t retVal = kCFRunLoopRunFinished;
    //1. run loop即将进入,通知Observers
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
      do {
        //2. 通知观察者,即将触发计时器
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3. 通知观察者即将触发source0(非port based)输入源
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        //处理blocks
        __CFRunLoopDoBlocks(rl, rlm);
        //4. 处理source0,也就是非port based输入源
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //处理完source0后会再处理下blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        //5. 检查是否有需要处理的source1事件,通常是系统级事件,倒数第三个参数传0表示立即返回
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            //跳转到handle_msg直接去处理source1事件
            goto handle_msg;
        }
        
        //没有source1则直接进入睡眠
        //6. 通知观察者Runloop即将进入睡眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //7. 调用系统内核方法mach_msg,切换到内核态接受消息
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
        //当有source1,timer或者手动唤醒时,会退出睡眠态
        //8. 通知观察者Runloop退出等待
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        handle_msg:;
        //9. 退出睡眠后,需要处理些事件
        //计时器需要触发
        if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            //触发计时器
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                //计算下一次触发的时间
                __CFArmNextTimerInMode(rlm, rl);
            }
        } else if (livePort == dispatchPort) {
            //如果有dispatch到main_queue的block,执行block。
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {//source1事件
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
        }
        //再次执行一次block回调
        __CFRunLoopDoBlocks(rl, rlm);
        
        //判断是否需要退出run loop
        if (sourceHandledThisLoop && stopAfterHandle) {
            //启动run loop时设置了执行source0后立即退出
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            //启动run loop时设置了超时时间
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            //run loop被设置为停止
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            //run loop mode被设置为停止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //当前mode被移除了
            retVal = kCFRunLoopRunFinished;
        }
    } while (0 == retVal);
    //10. run loop已经退出,通知observers
      __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
  
    return retVal;
}

通过代码可以了解到

  • run loop通过do…while循环实现,只要不满足退出的条件,run loop就会睡眠或者运行。
  • run loop需要添加source,timer或者block事件才能运行,否则会直接退出
  • run loop进入休眠时调用mach_msg函数切换到内核态(当我们在app运行时点击暂停,就可以看到调用栈停留在mach_msg_trap()这个方法)
image.png

PerformSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个source0 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

苹果对RunLoop的应用

了解了RunLoop的实现,我们看一看苹果如何使用RunLoop的。该节主要参考深入理解RunLoop,这里仅做部分展开。

viewDidLoad方法中增加断点,打印[NSRunLoop currentRunLoop]可以打印出当前线程的runLoop对象的一些详细信息

AutoreleasePool

苹果在主线程RunLoop的CommonModes中注册了两个Observer,其中回调函数都是_wrapRunLoopWithAutoreleasePoolHandler

{
  order = -2147483647, activities = 0x1, callout = _wrapRunLoopWithAutoreleasePoolHandler
}
{
  order = 2147483647, activities = 0xa0, callout = _wrapRunLoopWithAutoreleasePoolHandler
}

第一个Observer设置了最高优先级-2147483647,在进入RunLoop时会触发(0x1)。

第二个Observer设置了最低优先级2147483647,在RunLoop进入睡眠或者退出时触发(0xa0)。

_wrapRunLoopWithAutoreleasePoolHandler

通过在XCode中添加Symbolic Breakpoint增加断点,查看_wrapRunLoopWithAutoreleasePoolHandler对应汇编的代码。

image.png

启动App后,会进入该断点,可以看到代码中有两处地方会跳转到其他函数,分别是NSPushAutoreleasePoolNSPopAutoreleasePool,根据名字可以看出,一个是autlreleasePool的创建,另一个是autlreleasePool的释放,推测在该函数中会判断RunLoop当前的状态,然后执行不同的函数。

然后我们继续对NSPushAutoreleasePoolNSPopAutoreleasePool添加断点,最终调用函数分别是objc_autoreleasePoolPushobjc_autoreleasePoolPop

第一个Observer在进入RunLoop时,创建autoreleasePool,其order=-2147483647保证了是所有回调之前调用。

第二个Observer在RunLoop休眠时,首先释放autoreleasePool,然后创建autoreleasePool;在RunLoop退出时,释放autoreleasePool,其order=2147483647保证了是所有回调之后调用。

总结

本文主要对Core Foundation框架中的RunLoop源码进行学习,了解了RunLoop的实现原理,后续会对RunLoop的应用进一步探索研究。

你可能感兴趣的:(RunLoop源码学习)