iOS runloop相关知识点

1.前言

runloop跟iOS息息相关,当新建一个App的时候,就算没有新增任何代码,App也能一直运行下去,是因为UIApplicationMain就已经启动了一个runloop。

runloop代码本质上是一个do.while循环。一般线程执行完代码后就会退出,所以线程配合runloop就能实现线程常驻,让线程处于:接受消息-等待-处理的循环中。大部分框架都有类似的机制,比如Node.js的Event loop。

2.创建

runloop在iOS中分别有CoreFoundation层的CFRunloop,是线程安全的,还有Foundation层的NSRunloop,基于CFRunloop的封装,非线程安全。

runloop不能手动创建,只有在获取的时候,系统才会去创建,获取的方法有两个:

CFRunLoopGetMain()  //获取主线程
CFRunLoopGetCurrent()   //获取当前线程

runloop和线程是一一对应的,有个全局的Dictionary保存着两者,其中key是pthread_t,Value是CFRunloopRef,runloop的创建和pthread_t对应大概是下面的形式:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    OSSpinLockUnLock(&loopsLock);
    return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

3.数据结构

runloop主要有5个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer,大概关系如下所示:

每次调用runloop时,只能指定一个Mode,也就是CurrentMode,要切换Mode需要退出runloop重新指定,主要是让Mode里的事件相互不影响。

CFRunLoopSourceRef
source0只有一个函数指针回调,不能主动触发,使用时需先调用CFRunLoopSourceSignal(source)标记为待处理,然后调用CFRunLoopWakeUp(runloop)唤醒runloop处理事件。
Source1

CFRunLoopTimerRef
包含一个时间长度和指针回调,加入runloop时,会注册相应时间点,时间点到后,runloop会唤醒回调。

CFRunLoopObserverRef
包含一个指针回调,主要观察runloop的状态,但状态发生变更时,runloop会触发相关的回调。
runloop有下面几种状态:

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

4.Mode

runloop的数据结构大概如下:

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

这里有个commonModes,通过CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);将Mode添加到commonMode里可以使Mode带有common属性,runloop内容发生改变时,会自动将commonModeItem里的事件同步到commonMode里的Mode里。
因为Mode会切换,比如滑动时会切换到UITrackingRunLoopMode,这时你注册的事件是不会在runloop触发的,只有将事件添加到commonModeItem里,才会同步到所有commonMode里。

Mode只能通过modeName来访问,比如CFRunLoopRunInMode(CFStringRef modeName, ...);

添加事件的方法有下面几个:

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

5.内部流程

runloop的流程图大概如下:

其中最重要的底层的是休眠那步,这里调用的是Mach的mach_msg()函数,Mach是底层内核。
在iOS硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架

当线程处于等待状态时,点击暂停,会看到正处于mach_msg_trap()的函数中。

OBserves:
iOS本身会监听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);
}

6.应用

AutoreleasePool
APP启动后注册了两个通知,回调到_wrapRunLoopWithAutoreleasePoolHandler()
第一个通知是Entry时,回调会调用_objc_autoreleasePoolPush()创建自动释放池。
第二个通知是BeforeWaiting时,回调会调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池。
在Exit时,也会调用_objc_autoreleasePoolPop(),完成循环。

事件响应
iOS注册了一个source1的事件,回调到__IOHIDEventSystemClientQueueCallback(),然后会继续调用_UIApplicationHandleEventQueue(),把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发。

手势识别
当上面的 _UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新
当UIView/CAlayer的Frame被修改或者被设置为setNeedLayout时,会被标记为待处理,然后保存到全局容器中,苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

PerformSelecter
PerformSelecter是通过生成一个timer到runloop实现的。

GCD
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()里执行这个 block.

参考资料:

深入理解RunLoop


https://github.com/CoderLN/Runtime-RunLoop

发表评论

电子邮件地址不会被公开。 必填项已用*标注