RunLoop

深入学习RunLoop。看CFRunLoop.h和CFRunLoop.c。
源码:https://opensource.apple.com/tarballs/CF/
API

RunLoop 的概念

其实,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。它的方法只应在当前线程的上下文中被调用。您不应该尝试调用NSRun​Loop在不同线程中运行的对象的方法,因为这样做可能会导致意外的结果。一个NSRun​Loop对象处理用于来源如从窗口系统鼠标和键盘事件,输入NSPort对象,和NSConnection对象。一个NSRun​Loop对象也处理NSTimer事件。

基本作用

  • 保持程序的持续运行(比如主运行循环)
  • 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息

RunLoop的工作模式

Run loop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。两种源都使用程序的某一特定的处理例程来处理到达的事件。

RunLoop 内部结构

CFRunloop.h文件中

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
//CFRunloop.h
typedef CFStringRef CFRunLoopMode CF_EXTENSIBLE_STRING_ENUM;

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;//是事件产生的地方

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;//观察者

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;//基于时间的触发器
// CFRunLoop.c
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFRunLoopModeRef _currentMode;
...
};

struct __CFRunLoopMode {
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
};

RunLoop 与线程的关系

  1. 每条线程都有唯一的一个与之对应的RunLoop对象
  2. 主线程的RunLoop已经自动启动的,子线程的RunLoop需要主动创建。
  3. RunLoop在第一次获取时创建,在线程结束时销毁

iOS的应用程序里面,程序启动后会有一个如下的main()函数

1
2
3
4
5
int  main(int argc,char * argv []){
@autoreleasepool {
return UIApplicationMain(argc,argv,nilNSStringFromClass([AppDelegate class ]));
}}
}}

重点是UIApplicationMain()函数,这个方法会为主线程设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候还能立马响应。

  1. 在任何一个Cocoa程序的线程中,都可以通过以下代码来获取到当前线程的运行循环。

NSRunLoop * runloop = [NSRunLoop currentRunLoop];

关系;

  • 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
  • 每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
  • 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。
  • 这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopMode

系统默认注册了5个Mode:(前两个跟最后一个常用)

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

CFRunLoopRef

1
2
CFRunLoopRef CFRunLoopGetCurrent(void);//获取当前线程
CFRunLoopRef CFRunLoopGetMain(void);//获取主线程

CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(输入源)

按照官方文档的分类

  • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到
  • Custom Input Sources,用户手动创建的 Source
  • Cocoa Perform Selector Sources, Cocoa 提供的 performSelector 系列方法,也是一种事件源

按照函数调用栈的分类
源码中 source 只有两个版本:source0 和 source1,它们的区别在于它们是怎么被标记 (signal) 的。

  • Source0:非基于Port的。 app 内部的消息机制,使用时需要调用 CFRunLoopSourceSignal()来把这个 source 标记为待处理,然后掉用 CFRunLoopWakeUp() 来唤醒 RunLoop,让其处理这个事件。
  • Source1:基于Port的。包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒 RunLoop 的线程。

CFRunLoopTimerRef

CFRunLoopTimerRef是基于时间的触发器,在 iOS 用到的 NSTimer 或者 performSelector:afterDelay: 都是通过它来实现的。使用时先设置一个时间长度和一个回调,然后将其加入 RunLoop,这样 RunLoop 就会注册对应的时间点,当到了该时间点时就会唤醒 RunLoop 来执行那个回调。iOS7 之后,timer 还可有一个 tolerance,因为 timer 不太准确,如上面提到的,某个 mode 下的 timer 在 RunLoop 切换 mode 时可能就失效了,而 tolerance 则用来计算最后能执行那个回调的时间点。

基本上说的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响

GCD的定时器不受RunLoop的Mode影响

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

可以监听的时间点有以下几个:

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
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
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

我们可以使用 CFRunLoopObserverCreateWithHandler() 来创建 observer,创建时设置要监听的状态变化和回调,再用 CFRunLoopAddObserver() 来给 RunLoop 添加 observer,当该 RunLoop 状态发生在监听类型内的变化时,observer 就会执行回调 :

运行逻辑

获取RunLoop

苹果不允许我们创建 RunLoop,要获取主线程或当前线程对应的 RunLoop,只能通过 CFRunLoopGetMain 或 CFRunLoopGetCurrent 函数,获取过程大致如下:

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
// 全局的 dictionary, key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 第一次进入时,创建全局 dictionary
if (!__CFRunLoops) {
// 创建可变字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable();
// 先创建主线程的 RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 主线程的 RunLoop 存进字典中
CFDictionarySetValue(dict, pthread_main_thread_np(), mainLoop);
}

// 用 传进来的线程 作 key,获取对应的 RunLoop
CFRunLoopRef loop = CFDictionaryGetValue(__CFRunLoops, t);

// 如果获取不到,则新建一个,并存入字典
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
}
return loop;
}

// 获取主线程的 RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np());
return __main;
}

// 获取当前线程的 RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
return _CFRunLoopGet0(pthread_self());
}

可见,线程和 RunLoop 是一一对应的,对应关系保存在一个全局的 dictionary 中。RunLoop 类似懒加载,只有在第一次获取的时候才会创建。当线程销毁时,也销毁对应的 RunLoop。

RunLoop的运行

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
// 用 DefaultMode 启动(do-while循环)
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 用指定的 mode 启动
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

// RunLoop 的实现
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
// 根据 modeName 找到对应的 mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 判断 mode 是否为空 (即 source/timer 皆空),是的话则返回
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
return ;
}
// 通知 observers: 即将进入 loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 进入 loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 observers: 即将退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

return;
}
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
// 进入 RunLoop 后
static int32_t __CFRunLoopRun() {
// 设置 timer
dispatch_source_t timeout_timer = NULL;
// 设置过期时间
seconds = 9999999999.0;

int32_t retVal = 0;

// 开始 loop
do {
// 告诉 observer:要处理 timer
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 告诉 observer:要处理 sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

// 执行被加入的 block
__CFRunLoopDoBlocks(rl, rlm);

// 处理 Sources0(非 port)
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}

if (!didDispatchPortLastTime) {
// 如果有 GCD 分发到 main queue 的 block
if (__CFRunLoopServiceMachPort(dispatchPort, &msg)) {
// 跳过睡眠阶段,直接去处理消息
goto handle_msg;
}
}

// 通知 observers:即将进入睡眠
if () __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

// 调用 mach_msg 等待接受 mach_port 的消息,线程将进入睡眠
__CFRunLoopServiceMachPort(waitSet, &msg, ...);

// 通知 observers:刚被唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

// 处理消息的标记
handle_msg:;

// 通过判断端口,找出要处理的事件
if (MACH_PORT_NULL == livePort) {
// 纯粹是被手动唤醒的,无消息,则不做任何处理
} else if (livePort == rlm->_timerPort) {
// 被 timer 唤醒,则触发这个 timer 的回调
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (livePort == dispatchPort) {
// 被 GCD 唤醒,则执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
// 如果被 source1(基于 port) 唤醒的,则处理这个事件
__CFRunLoopDoSource1(rl, rlm, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
mach_msg(reply, MACH_SEND_MSG);
}
}

// 执行加入到 loop 的 block
__CFRunLoopDoBlocks(rl, rlm);

// 判断是否应该退出 loop
if (sourceHandledThisLoop && stopAfterHandle) {
// 传入的参数是否说明应该在处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 是否过期
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 是否被强制停止
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm)) {
// mode 是否为空,即 source、timer 为空
retVal = kCFRunLoopRunFinished;
}

// 都不是,则继续 loop
} while (0 == retVal);

return retVal;
}

而判断 mode 的逻辑大致如下:

1
2
3
4
5
6
__CFRunLoopModeIsEmpty() {
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
return true;
}

应用

AutoreleasePool(自动释放池)

一般我们比较关心的是自动释放池什么时候会释放?
在打印 [NSRunLoop currentRunLoop] 的结果中我们可以看到与自动释放池相关的:

1
2
<CFRunLoopObserver>{activities = 0x1, callout = _wrapRunLoopWithAutoreleasePoolHandler} 
<CFRunLoopObserver>{activities = 0xa0, callout = _wrapRunLoopWithAutoreleasePoolHandler}

即 app 启动后,苹果会给 RunLoop 注册很多个 observers,其中有两个是跟自动释放池相关的,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
第一个 observer 监听的是 activities = 0x1(kCFRunLoopEntry),也就是在即将进入 loop 时,其回调会调用 _objc_autoreleasePoolPush() 创建自动释放池;
第二个 observer 监听的是 activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit),即监听的是准备进入睡眠和即将退出 loop 两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用 _objc_autoreleasePoolPop()释放旧的释放池,并调用 _objc_autoreleasePoolPush() 创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出 loop 时则会 _objc_autoreleasePoolPop() 释放池子。

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件。
_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 就被标记为待处理,并被提交到一个全局的容器去。

定时器

NSTimer 其实就是 CFRunLoopTimerRef。
解决界面卡顿

PerformSelecter

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

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

RunLoop 与 GCD

RunLoop 底层会用到 GCD 的东西,GCD 的某些 API 也用到了 RunLoop。 例如 dispatch_async()。
当调用 dispatch_async(dispatch_get_main_queue(), block)时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

UIImageView 延迟加载图片

给 UIImageView 设置图片可能耗时不少,如果此时要滑动 tableView 等则可能影响到界面的流畅。解决是:使用 performSelector:withObject:afterDelay:inModes: 方法,将设置图片的方法放到 DefaultMode 中执行。

AFNetworking

子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的 RunLoop,保持 RunLoop 运行,则使子线程保持不死。AFNetworking 基于 NSURLConnection 时正是这样做的,希望在后台线程能保持活着,从而能接收到 delegate 的回调。具体做法是:

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
/* 返回一个线程 */
+ (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;
}
/* 在新开的线程中执行的第一个方法 */
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
// 获取当前线程对应的 RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 为 RunLoop 添加 source,模式为 DefaultMode
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 开始运行 RunLoop
[runLoop run];
}
}

因为 RunLoop 启动前必须设置一个 mode,而 mode 要存在则至少需要一个 source / timer。所以上面的做法是为 RunLoop 的 DefaultMode 添加一个 NSMachPort 对象,虽然消息是可以通过 NSMachPort 对象发送到 loop 内,但这里添加的 port 只是为了 RunLoop 一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的 timer 来既保持 RunLoop 不退出也不占用资源。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,

与 Runloop 相关的坑

日常开发中,与 runLoop 接触得最近可能就是通过 NSTimer 了。一个 Timer 一次只能加入到一个 RunLoop 中。我们日常使用的时候,通常就是加入到当前的 runLoop 的 default mode 中,而 ScrollView 在用户滑动时,主线程 RunLoop 会转到 UITrackingRunLoopMode 。而这个时候, Timer 就不会运行。

有如下两种解决方案:

第一种: 设置RunLoop Mode,例如NSTimer,我们指定它运行于 NSRunLoopCommonModes ,这是一个Mode的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。
第二种: 另一种解决Timer的方法是,我们在另外一个线程执行和处理 Timer 事件,然后在主线程更新UI。
在 AFNetworking 3.0 中,就有相关的代码,如下:

1
2
3
4
5
- (void)startActivationDelayTimer {
self.activationDelayTimer = [NSTimer timerWithTimeInterval:self.activationDelay target:self
selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes];
}

这里就是添加了一个计时器,由于指定了 NSRunLoopCommonModes,所以不管 RunLoop 出于什么状态,都执行这个计时器任务。

面试题

  1. [※※※]runloop和线程有什么关系?线程没有runloop可以吗?
  • 主线程的run loop默认是启动的。
    iOS的应用程序里面,程序启动后会有一个如下的main()函数
1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

  • 对其它线程来说,run loop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。

  • 在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。

NSRunLoop *runloop = [NSRunLoop currentRunLoop];

  1. [※※※]runloop的mode作用是什么?
    model主要是用来指定事件在运行循环中的优先级的,分为:
  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
  • UITrackingRunLoopMode:ScrollView滑动时
  • UIInitializationRunLoopMode:启动时
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):模式集合

苹果公开提供的模式有两个:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
  • NSRunLoopCommonModes(kCFRunLoopCommonModes)
  1. [※※※※]以+ scheduledTimerWithTimeInterval…的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

NSTimer有什么需注意的以及和RunLoop的关系?

scrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。

如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

同时因为mode还是可定制的,所以:

Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//将timer添加到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
//然后再添加到NSRunLoopCommonModes里
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  1. [※※※※※]猜想runloop内部是如何实现的?
1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
  1. 如何刷新View界面?

  2. 主线程runloop(mainRunloop)主要执行事件:
    负责创建Autoreleasepool和释放autoreleasepool, 周期大概是event loop(事件循环);
    事件响应—>当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 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 标记为待处理。

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

  1. NSTimer事件

  2. PerformSelecter—>performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去

参考:
iOS RunLoop进阶:http://www.jianshu.com/p/2c067bdc7e47#
解密-神秘的RunLoop:http://www.jianshu.com/p/cf4915508929
http://sunshineyg888.github.io/2016/05/21/RunLoop-%E8%AF%A6%E8%A7%A3/
RunLoop源码观察:http://tutudev.com/2016/06/28/runloop/

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

文章目录
  1. 1. RunLoop 的概念
  2. 2. RunLoop的工作模式
    1. 2.1. RunLoop 内部结构
      1. 2.1.1. CFRunLoopMode
      2. 2.1.2. CFRunLoopRef
      3. 2.1.3. CFRunLoopSourceRef
      4. 2.1.4. CFRunLoopTimerRef
      5. 2.1.5. CFRunLoopObserverRef
  3. 3. 运行逻辑
    1. 3.1. 获取RunLoop
    2. 3.2. RunLoop的运行
  4. 4. 应用
    1. 4.1. AutoreleasePool(自动释放池)
    2. 4.2. 事件响应
    3. 4.3. 手势识别
    4. 4.4. 界面更新
    5. 4.5. 定时器
    6. 4.6. PerformSelecter
    7. 4.7. RunLoop 与 GCD
    8. 4.8. UIImageView 延迟加载图片
    9. 4.9. AFNetworking
    10. 4.10. AsyncDisplayKit
  5. 5. 与 Runloop 相关的坑
  6. 6. 面试题
本站总访问量 本站访客数人次 ,