RunLoop知识点整理

Posted by CoderLeonidas on May 19, 2017

RunLoop知识点整理

概述

顾名思义,RunLoop翻译过来是跑圈的意思,其实是一个运行循环,也可以简单理解为是一个有退出条件的死循环。它有一下几个基本的作用:

  • 保证程序的持续运行
  • 处理App中的各种事件
  • 节省CPU资源,提升程序性能

RunLoop在系统中应用十分广泛,以下模块都会用到RunLoop:

本质

Mach是XNU的内核,进程、线程和虚拟内存等对象通过端口port发消息进行通信,Runloop通过mach_msg()函数发送消息.

如果没有port消息,内核会将线程置于等待状态mach_msg_trap() 。 如果有消息,判断消息类型处理事件,并通过modeItem的callback回调

伪代码演示什么是RunLoop

  • 如果没有RunLoop:
1
2
3
4
5
int main(int argc, char * argv[]) {
    NSLog(@"execute main function");
    return 0;
}

假如没有RunLoop,那么程序进入main函数后,就会打印“execute main function”后,立刻执行完并返回,而此时程序也就运行结束了,这显然是不合理的。

  • 如果有了RunLoop:
1
2
3
4
5
6
7
8
9
int main(int argc, char * argv[]) {
	static BOOL isRunning = YES;
	do {
		doSomeThing();// 处理各种任务和事件等
	} while (isRunning);

    return 0;
}

有RunLoop的情况下,相当于在程序结束前,里边执行了一个死循环,在这个循环中程序可以一直接受事件处理各种任务,程序不会立马退出,可以保持程序的执行状态。

以上伪代码演示了RunLoop的意义所在,但RunLoop的真面目并非像伪代码这么简单。

OC中的RunLoop

main.m文件的入口函数main()中是这样的:

1
2
3
4
5
6
7
8
9
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);
}

这里UIApplicationMain函数内部其实已经默认开启了一个RunLoop,因此UIApplicationMain函数其实一直没有返回,保持了程序的持续运行。而这个默认开启的RunLoop和主线程有关。

RunLoop对象

Cocoa中有2套RunLoop的API:

框架 相关类文件 语言
Foundation NSRunLoop Objective-C
CoreFoundation CFRunLoop C
  • CFRunLoop 是一套C语言的API
  • NSRunLoop 是基于CFRunLoop 的OC封装
  • CFRunLoop 开源,想研究其底层实现,需要研究CFRunLoop

RunLoop与线程

RunLoop与线程是息息相关的。RunLoop的存在,就是为了配合线程工作、保证线程的生命周期。

  • 每条线程都可以有一个与之对应的唯一的RunLoop对象
  • 主线程的RunLoop已经是自动创建并开启的;子线程的RunLoop需要手动创建并开启后,才会生效
  • RunLoop在第一次获取时创建,在线程结束时销毁

RunLoop的获取

  • Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象

[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

  • Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象

CFRunLoopGetMain(); // 获得主线程的RunLoop对象

CFRunLoopGetMain 和 CFRunLoopGetCurrent 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

CAS:

CompareAndSwap 的缩写,翻译过来就是比较并替换。 CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

可以看出,runloop的获取其实是懒加载的。这里我们更加关注子线程runloop对象的获取。CFRunLoopGetCurrent 函数中根据子线程获取一个runloop的实现在_CFRunLoopGet0 函数里,下面是这个函数的实现源码。

_CFRunLoopGet0 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//获得runloop(创建runloop)
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());
        // 使用字典保存主线程-主线程对应的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不存在,那么就为该线程创建一个对应的runloop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        // 把当前子线程和对应的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)) {
         /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时(__CFFinalizeRunLoop函数会在线程退出时执行,释放掉所有资源)。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

RunLoop相关类

Core Foundation中关于RunLoop的5个类

1
2
3
4
5
CFRunLoopRef 
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

CFRunLoopRef

代表RunLoop对象。

CFRunLoopModeRef

代表RunLoop的运行模式。

  • 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer,而这些Source/Timer/Observer又称为modeItem

  • 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。打个比方,空调有制冷、制热等模式,空调运行的时候必须指定其中的一个模式来运行。那么这当前运行的模式,就是空调的CurrentMode。

  • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入

  • 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响

CFRunLoopModeRef数据结构(精简版)

1
2
3
4
5
6
7
8
9
struct __CFRunLoopMode {
    CFStringRef _name; // mode的名称
    CFMutableSetRef _sources0; // source0,集合
    CFMutableSetRef _sources1; // source1,集合
    CFMutableArrayRef _observers; // 观察者, 数组
    CFMutableArrayRef _timers; // 定时器, 数组
    __CFPortSet _portSet; // 端口,集合
};

这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

系统默认注册了5个Mode:

mode 作用 定义 使用频率
kCFRunLoopDefaultMode App的默认Mode,通常主线程是在这个Mode下运行 定义在CFRunLoop.h中 常用
UITrackingRunLoopMode 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 定义在UIApplication.h中 常用
UIInitializationRunLoopMode 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用   不常用
GSEventReceiveRunLoopMode 接受系统事件的内部 Mode,通常用不到   不常用
kCFRunLoopCommonModes 这是一个占位用的Mode,不是一种真正的Mode 定义在CFRunLoop.h中 常用

CFRunLoopSourceRef

代表事件源(输入源)

以前的分法:

  • Port-Based Sources
  • Custom Input Sources
  • Cocoa Perform Selector Sources

现在的分法

  • Source0:非基于Port的
  • Source1:基于Port的

Source有两个版本:Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

CFRunLoopTimerRef

代表基于时间的触发器。它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef

代表观察者,能够监听RunLoop的状态改变。每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个

1
2
3
4
5
6
7
8
9
10
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),        // 
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

  • kCFRunLoopEntry

即将进入Loop

进入事件处理循环之前,运行循环的入口。 每次调用CFRunLoopRun和CFRunLoopRunInMode都会发生一次此活动。

  • kCFRunLoopBeforeTimers

即将处理timers

在事件处理循环内部,处理timer事件之前

  • kCFRunLoopBeforeSources

即将处理sources

在事件处理循环内部,处理source事件之前

  • kCFRunLoopBeforeWaiting

即将进入休眠

在事件处理循环内部,runloop休眠之前,等待source或者timer事件来触发。 如果以0秒的超时时间调用CFRunLoopRunInMode,则不会发生此活动。如果source0触发,则它也不会在事件处理循环的特定迭代中发生。

  • kCFRunLoopAfterWaiting

刚从休眠中唤醒

在事件处理循环内部,在runloop唤醒之后但在处理将其唤醒的事件之前。 仅当runloop在当前循环期间进入睡眠状态时,才会发生此活动。

  • kCFRunLoopExit

即将退出Loop

退出事件处理循环后,runloop的出口。 每次调用CFRunLoopRun和CFRunLoopRunInMode都会发生一次此活动。

  • kCFRunLoopAllActivities

所有先前阶段的组合。

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。runloop要运行,必须保证有事件可以处理,而事件就是Source或者Timer,对是否有Observer没要求。

RunLoop数据局结构(精简版)

1
2
3
4
5
6
7
struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
};

这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

用于mode管理的接口:

1
2
3
4
5
6
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);

苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。

线程、RunLoop、mode、modeItem的关系

RunLoop的处理逻辑

runloop启动的时候需要选择一个运行模式,检查运行模式是否为空(是否有”source timer”),如果为空那么直接退出,不为空则开启

CFRunLoopRun 函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用DefaultMode启动
// RunLoop的主函数,是一个死循环
void CFRunLoopRun(void) {	/* DOES CALLOUT */
    int32_t result;
    do {
        
        //CFRunLoopRunSpecific具体处理runloop的运行情况
        //CFRunLoopGetCurrent()  当前runloop对象
        //kCFRunLoopDefaultMode  runloop的运行模式的名称
        //1.0e10                 runloop默认的运行时间,即超时为10的九次方
        //returnAfterSourceHandled 回调处理
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
        
        //如果runloop没有停止且没有结束则继续循环
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

在指定的mode下运行

1
2
3
4
5
6
7
// 用指定的Mode启动,允许设置RunLoop超时时间
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}


CFRunLoopRunSpecific伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    // 0.1 根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);

    // 0.2 如果mode里没有source/timer 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    // 1.1 通知 Observers: RunLoop 即将进入 loop。---(OB会创建释放池)
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    // 1.2 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
            // 2.1 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

            // 2.2 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            // 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            // 2.3 RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            // 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            // 2.4 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

            // 3.1 如果没有待处理消息,通知 Observers: RunLoop 的线程即将进入休眠(sleep)。--- (OB会销毁释放池并建立新释放池)
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

            // 3.2. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /*
                一个基于 port 的Source1 的事件。
                一个 Timer 到时间了
                RunLoop 启动时设置的最大超时时间到了
                被手动唤醒
             */
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }

            // 3.3. 被唤醒,通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

            // 4.0 处理消息。
        handle_msg:
            // 4.1 如果消息是Timer类型,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            }
            // 4.2 如果消息是dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            }
            // 4.3 如果消息是Source1类型,处理这个事件
            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);

            // 5.1 如果处理事件完毕,启动Runloop时设置参数为一次性执行,设置while参数退出Runloop
            if (sourceHandledThisLoop && stopAfterHandle) {
                retVal = kCFRunLoopRunHandledSource;
                // 5.2 如果启动Runloop时设置的最大运转时间到期,设置while参数退出Runloop
            } else if (timeout) {
                retVal = kCFRunLoopRunTimedOut;
                // 5.3 如果启动Runloop被外部调用强制停止,设置while参数退出Runloop
            } else if (__CFRunLoopIsStopped(runloop)) {
                retVal = kCFRunLoopRunStopped;
                // 5.4 如果启动Runloop的modeItems为空,设置while参数退出Runloop
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                retVal = kCFRunLoopRunFinished;
            }
            // 5.5 如果没超时,mode里没空,loop也没被停止,那继续loop,回到第2步循环。
        } while (retVal == 0);
    }

    // 6. 如果第6步判断后loop退出,通知 Observers: RunLoop 退出。--- (OB会销毁新释放池)
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
    

RunLoop的应用

官方文档的场景:

Use ports or custom input sources to communicate with other threads.

Use timers on the thread.

Use any of the performSelector… methods in a Cocoa application.

Keep the thread around to perform periodic tasks.

翻译一下:

  • 使用port或者自定义的数据源来与其他线程进行通信

  • 在线程上使用timer

  • 使用performSelector...方法簇

  • 保持线程执行某些定期任务

此外还有以下场景,也能用到runloop:

  • 开启常驻线程
  • AutoreleasePool
  • 监听runloop状态

RunLoop与AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop,这个稍后我会再单独写一页博客来分析。

PerformSelecter

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

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

GCD

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

RunLoop 的实际应用举例

AFNetworking

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

1
2
3
4
5
6
7
8
9
10
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。 绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。 UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。 具体的代码可以看这里:_ASAsyncTransactionGroup。

参考文章

CAS相关问题

深入理解RunLoop

RunLoop面试题分析

Threading Programming Guide中的RunLoop章节

CF框架中的CFRunLoop源码