一、什么是 RunLoop
- 从字面上来说,RunLoop 可以被翻译为跑圈,但通常状况下我们都称之为运行循环或者消息循环
- RunLoop 对我们开发者来说有什么意义呢?
1.)RunLoop 可以让程序运行不退出我们所写的程序之所以可以一直运行,而不是像程序中的方法一样,运行完就结束,就是 RunLoop 的功劳,所以 RunLoop 也可以被理解为一个死循环(下面会详细解释)
2.)Runloop 可以让程序等待事件的输入
3.)没有事件的时候,使线程休眠,一旦有事件输入,就唤醒线程去处理
二、初探 RunLoop
打个招呼
现阶段,我们在平时的开发中,与 RunLoop 接触最多的地方,大概就是 NSTimer 了。
NSTimer *timer = [NSTimer timerWithTimeInterval:1
repeats:YES
block:^(NSTimer * _Nonnull timer) {
NSLog(@"定时器正在运转");
}];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
那么,这个 RunLoop 具体又是什么呢?我们可以打印一下
NSLog(@"%@",[NSRunLoop currentRunLoop]
我的天啊,怎么打印出这么一堆东西,那些 Sources/Observers/Timers 究竟是什么,OMG~~~~
然而你会发现此时的你并不清楚这些是什么,没关系,我们会慢慢解释
又见面了
前面说到我们打印了 RunLoop ,结果打印出许多难以理解的东西,现在我们就来研究一下,这些让人头晕的东西究竟是什么
打开 RunLoop 的头文件,我们看到了下面这些
对照着前面的打印结果再看,这下是不是感觉好点了?我们再往下看
头文件的描述是不是看的你依旧有些晕晕的?没关系,我们继续
根据官方文档中的描述,与 RunLoop 有关的类有五个(即上图中显示的五个类)
那么这五个类又是做什么的呢,我们再看一张图
一个 RunLoop 可以有多个 Mode (模式),每个模式又包含若干个 Source/Observer/Timer
这里请注意上图中Source/Observer/Timer 的类型,除却 Source 是 NSSet 类型,Observer 及 Timer 都是 NSArray 类型的,这说明一个 Mode 中实际上是包含了许多的 Source/Observer/Timer
RunLoop 一次只能运行一种 Mode ,这个 Mode 被称为CurrentMode ,
切换 Mode 需要退出当前 Mode ,这样做是为了避免不同组的 Source/Observer/Timer 混淆
这里需要注意的是Runloop一次只能运行一个mode,并不与上述的一个RunLoop 可以有多个Mode 冲突,下面会具体解释
那么 Source/Observer/Timer/Mode 究竟又是什么呢?
在研究这个问题的之前,我们再来看看一些基础的东西
我们已经知道,程序之所以可以不退出,在没有事件需要处理的时候会进入睡眠状态,等待接收事件,那么这是为什么呢?
实际上,这是因为在主线程中有一个RunLoop 一直在运行,并且这个 RunLoop 是系统创建的,如果我们想要拿到这个 RunLoop ,只需要在主线程中调用 方法 currentRunLoop 就可以了
主线程的 RunLoop 是在什么时候创建的呢?我们可以探究一下
当我们创建一个干净项目的时候,项目里除了基础文件,别的什么也不会有,当然除了 Main.m 文件中,那么猫腻会不会就出现在这里呢?
//新建项目中,唯一一个显得不是那么干净的方法
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我们对代码进行如下测试
int main(int argc, char * argv[]) {
NSLog(@"000 - 我要出来了");
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"111 -- 你猜我能出来吗");
}
NSLog(@"222 -- 你猜我能出来吗");
}
最终只有第一句打印了出来,所以我们不得不猜测是不是因为 main 函数中的方法是一个死循环,所以后面的代码不会执行
当然猜测并没有用,我们还是需要找到证据,打开 RunLoop 的源码,找到 RunLoop 的实现方法 (Apple的部分源文件已经开源,这里附上下载地址:https://opensource.apple.com/release/os-x-1010.html 进入页面下载 CF-1151.16 即可
)
由于代码过长,我们对代码进行了折叠,这段三百多行的代码中,有我们 RunLoop 会一直运行的原因
这段代码中,我同样折叠了一部分代码,然而我们可以很清楚的看到一个 do while 循环,这就是我们的 RunLoop 可以循环的原因。
原先我一直认为这段代码就是RunLoop中最关键的代码,直到跟一位前辈的进行交流时才发现我对RunLoop的理解有一部分是不正确的。正真的核心代码其实是这一段。
do {
if (kCFUseCollectableAllocator) {
// objc_clear_stack(0);
// <rdar://problem/16393959>
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {
// Leave livePort as the queue port, and service timers below
rlm->_timerFired = false;
break;
} else {
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner loop.
break;
}
} while (1);
我们需要清楚的一点是,一个 RunLoop 对应一个线程,这一点我们同样可以去 RunLoop 的源文件中看到
iOS 中有两套API访问 Foundation(NSRunLoop), CoreFoundation (CFRunLoopRef)
使用 NSRunLoop ,我们可以通过
//使用 NSRunloop ,我们可以通过下面两个方法分别拿到当前线程以及主线程的 Runloop
[NSRunLoop mainRunLoop];
[NSRunLoop currentRunLoop];
//使用 CFRunLoopRef ,我们可以通过下面两个方法分别拿到当前线程以及主线程的 Runloop CFRunLoopGetMain();
CFRunLoopGetCurrent();
打开 CFRunLoopRef 的源文件,搜索 CFRunLoopGetCurrent,找到 CFRunLoopGetCurrent 的函数方法
我们可以看到这个方法最终返回的是一个函数方法的结果,搜索这个函数方法,我们去看看这个方法究竟是何方神圣
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
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);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
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)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
可以很清楚的看见,这个函数实际上就是创建 RunLoop 的方法,并且需要传入一个线程
这会儿,大家应该清醒点了吧
那就让你们的视线集中到下面这张图上
这张图是官方文档中的,很直观的描述了 RunLoop
一个线程中有一个 RunLoop ,接收到事件之后会对事件进行操作,没有事件的时候会进入睡眠状态。
接收事件的类型分为两类
输入源:输入源可以是键盘鼠标,NSPort, NSConnection 等对象
定时源:NSTimer事件
现在我们真的要去了解一下 Source/Observer/Timer/Mode 了
CFRunLoopSourceRef
按照官方文档,Source分类(理论性的分类,了解就好)
Port - Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
按照函数调用栈,Source分类(实际应用中的)
Source0: 非基于Port
Source1: 基于Port,通过内核和其他线程通信,接收、分发系统事件
通过函数调用堆栈进一步了解 Source
函数调用堆栈是倒过来看的,在途中我们可以清楚的看见标红的位置就是 Source0 事件,它在 RunLoop 运行后被执行,虽然我们没有看见 Source1 ,但这并不代表它没有,当有事件产生式,系统会将事件包装由 Source1 分发给 Source0 处理
CFRunLoopObserverRef
它是一个观察者,用来监听RunLoop状态的改变,可以监听以下几个状态
/* 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 //所有行为
};
创建观察者,监听 RunLoop 的状态改变
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"---RunLoop状态发生变化----%lu",activity);
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
//注意,这里需要对 observer 进行 release
CFRelease(observer
);
注意内存管理,凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次Release
打印出来的结果如下,我们可以通过 Observer 对程序的运行进行拦截,让程序在我们需要的时候做我们想做的事情
这里用一张图片让大家更直观的去看到 Observer 的状态改变
CFRunLoopTimerRef
NSTimer *timer = [NSTimer timerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"定时器正在运转");
}];
//定时器只在 NSDefaultRunLoopMode 的模式下运行,一旦切换到其他模式,将不再运行
//可以使用UIScorllView及其子类来验证,这里为了方便,我们选取用UITextFiled来检验
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
//定时器被标记为 NSRunLoopCommonModes ,切换到其他模式也可以运行
//具体为什么,在 CFRunLoopModeRef 中我们再进行了解
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
下图是使用UITextFiled检验定时器运行的结果
图中标注位置,有明显时间间隔,这段时间,是对 UITextFiled 进行了拖动,定时器停止运行,一旦松开,定时器又再度运行
CFRunLoopModeRef
CFRunLoopModeRef 类并没有对外暴露,只是通过CFRunLoopRef 的接口进行了封装
系统默认注册了5个Mode
kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode: 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他的Mode影响
UIInitializationRunLoopMode: 在刚启动App是进入的第一个Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统时间的内部Mode,通常用不到
kCFRunLoopCommonMode: 这是一个占位用的Mode,不是一种真正的Mode
上文添加定时器时,给RunLoop指定为此Mode,只是起到一个标记作用,实际上是给让RunLoop拥有两种模式,即kCFRunLoopDefaultMode、UITrackingRunLoopMode,所以定时器才可以在两种模式下都能运行
最后附上孙源大神,15年博客中有关RunLoop的面试题及答案
[※※※]runloop和线程有什么关系?
总的说来,Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了 run loop 对象方便配置和管理线程的 run loop (以下都以 Cocoa 为例)。每个线程,包括程序的主线程( main thread )都有与之相应的 run loop 对象。
runloop 和线程的关系:
主线程的run loop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main()函数
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];
参考链接:《Objective-C之run loop详解》。
[※※※]runloop的mode作用是什么?
model 主要是用来指定事件在运行循环中的优先级的,分为:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
UITrackingRunLoopMode:ScrollView滑动时
UIInitializationRunLoopMode:启动时
NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的 Mode 有两个:
NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
NSRunLoopCommonModes(kCFRunLoopCommonModes)
[※※※※]以+ scheduledTimerWithTimeInterval...
的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。利用这个机制,ScrollView滚动过程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode模式下处理的事件会影响ScrollView的滑动。
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。
同时因为mode还是可定制的,所以:
Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到>NSRunLoopCommonModes(kCFRunLoopCommonModes)来解决。代码如下:
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
//将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];
[※※※※※]猜想runloop内部是如何实现的?
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
或使用伪代码来展示下:
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
int main(int argc, char * argv[]) {
//程序一直运行状态
while (AppIsRunning) {
//睡眠状态,等待唤醒事件
id whoWakesMe = SleepForWakingUp();
//得到唤醒事件
id event = GetEvent(whoWakesMe);
//开始处理事件
HandleEvent(event);
}
return 0;
}
参考链接:
《深入理解RunLoop》
摘自博文CFRunLoop,原作者是微博@我就叫Sunny怎么了