RunLoop概念
RunLoop介绍
RunLoop 是什么?RunLoop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 RunLoop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会去找对应的 Handler 处理事件。RunLoop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
没有Runloop的程序
我们通过Xcode新建一个命令行项目,main.m
文件里的代码如下
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
程序在执行完代码NSLog(@"Hello, World!");
之后,就会通过 return 0;
推出程序,这是一种线性的执行流程。
我们再新建一个iOS项目,你看到的main.m
文件是这个样子的
#import
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我们会进入app的界面,然后app就不会退出了,会一直运行着。
在命令行工程里面的main.m
里面,是没有加Runloop的,而iOS工程的main.m
里面,其实在UIApplicationMain()
这个方法中,系统加上了Runloop,让程序可以一直循环运行下去不退出。
iOS
项目,在main
函数中系统就会自动帮我们创建runloop
对象:return UIApplicationMain(argc, argv, nil, appDelegateClassName);
.
RunLoop
的基本作用就是:
保证程序的基本运行.程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
处理App中的各种事件 (比如:触摸事件,定时器事件 等等).
节省 CPU 资源,提高程序性能: 该做事时做事,没有事的时候就休息.(程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CPU,现在没有事情做,我要去休息,这时CPU就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情)
RunLoop
工作原理的伪代码大概如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = 0;
do {
//睡眠中等待消息
int message = sleep_and_wait();
//处理消息
retVal = process_message(message);
} while (retVal = 0);
return 0;
}
}
流程:条件成立的时候一直循环:有事情就处理事情,没有事情就休眠睡觉.Runloop其实就是一个do-while
循环,每次循环一圈,都会判断一次retVal
,决定是否结束循环,继续执行循环外的代码。
RunLoop对象
iOS
中提供了两套API
来访问RunLoop
:
-
Foundation : NSRunLoop
: OC 框架 -
Core Foundation : CFRunLoopRef
: C 语言框架
NSRunLoop
和CFRunLoopRef
都代表Runloop对象,NSRunLoop
是基于CFRunLoopRef
的一层OC包装,CFRunLoopRef
是开源的我们下载好源代码后新建一个项目,把源代码拖到项目中.
Runloop对象的获取
- Foundation
[NSRunloop currentRunLoop];
获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop];
获得主线程的Runloop对象 - Core Foundation
CFRunLoopGetCurrent();
获得当前线程的RunLoop对象
CFRunLoopGetMain();
获得主线程的Runloop对象
CFRunLoop.c
文件 -> 然后找到CFRunLoopGetCurrent
函数 -> 进入_CFRunLoopGet0
函数
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//根据线程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
⚽️⚽️⚽️⚽️⚽️⚽️
static CFMutableDictionaryRef __CFRunLoops = NULL; //字典
// 获取 runloop 对象 参数:传入一个 字典 和 key (线程)
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
//如果 runloop 不存在 , 就创建,并放到字典中
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);
}
⚽️⚽️⚽️⚽️⚽️⚽️
}
RunLoop是在第一次获取的时候创建的,并且
RunLoop和 线程 是 一一对应的关系,
RunLoop是存放在一个全局字典中:以线程作为
key,
RunLoop作为
value.
Runloop与线程
为什么聊Runloop一定要搭上线程?我们知道,程序里的每一句代码,都会在线程(
主线程/子线程
)里面被执行,上面四种获得Runloop对象的代码也不例外,一定是跑在线程里面的。之前我们说到,Runloop是为了让程序不退出,其实更准确地说,是为了保持某个线程不结束,只要还有未结束的线程,那么整个程序就不会退出,因为线程是程序的运行的调度的基本单元。线程与Runloop的关系是
一对一
的,一个新创建的线程,是没有Runloop对象的,当我们在该线程里第一次通过上面的API获得Runloop时,Runloop对象才会被创建,并且通过一个全局字典将Runloop对象和该线程存储绑定在一起,形成一对一关系。Runloop会在线程结束时销毁,主线程的Runloop已经自动获取过(创建),子线程默认没有开启RunLoop(直到你在该线程获取它)。RunLoop对象创建后,会被保存在一个全局的Dictionary里,线程作为
key
,Runloop对象作为value
。
Runloop对象底层结构
我们可以在源码CFRunloop.c
中找到Runloop的定义
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
// 核心组成
pthread_t _pthread;//RunLoop对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode;//当前运行的mode
CFMutableSetRef _modes;//存储的是CFRunLoopModeRef
// 核心组成
struct _block_item *_blocks_head;//doblocks的时候用到
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
RunLoop Mode Mode可以视为事件的管家,一个Mode管理着各种事件,它的结构如下:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode名称
Boolean _stopped; //mode是否被终止
char _padding[3];
//几种事件
// 核心组成
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers; //通知
CFMutableArrayRef _timers;//定时器
// 核心组成
CFMutableDictionaryRef _portToV1SourceMap;//字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
一个CFRunLoopMode对象有一个name,若干source0、source1、timer、observer和若干port,可见事件都是由Mode在管理,而RunLoop管理Mode。
Runloop相关的5个相关的类
- CFRunLoopRef——这个就是Runloop对象
- CFRunLoopModeRef——其内部主要包括四个容器,分别用来存放
source0
、source1
、observer
以及timer
- CFRunLoopSourceRef——分为
source0
和source1
source0
:包括 触摸事件处理、[performSelector: onThread: ]
source1
:包括 基于Port的线程间通信、系统事件捕捉 - CFRunLoopTimerRef——
timer
事件,包括我们设置的定时器事件、[performSelector: withObject: afterDelay:]
- CFRunLoopObserverRef——监听者,Runloop状态变更的时,会通知监听者进行函数回调,UI界面的刷新就是在监听到Runloop状态为
BeforeWaiting
时进行的。
对于以上这几个类相互之间的关系,可以通过如下的图来描绘
从图中可看出,一个RunLoop对象里面包含了若干个RunLoopMode
,RunLoop内部是通过一个集合容器_modes
来装这些RunLoopMode
的。
RunLoopMode内部核心内容是4个数组容器,分别用来装source0
,source1
,observer
和timer
,RunLoop对象内部有一个_currentMode
,它指向了该RunLoop对象的其中一个RunLoopMode
,它代表的含义是RunLoop当前所运行的RunLoopMode
,所谓“运行”也就是说,RunLoop当前只会执行_currentMode
所指向的RunLoopMode
里面所包括的事件(source0、source1、observer、timer
).RunLoop对象内部还包括一个线程对象_pthread
,这就是跟它一一对应的那个线程对象。
RunLoop Source
Run Loop Source分为Source、Observer、Timer三种,他们统称为ModeItem。
CFRunLoopSource
根据官方的描述,CFRunLoopSource是对input sources的抽象。CFRunLoopSource分source0和source1两个版本,它的结构如下:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; // 用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
source0
source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态.
App自己管理的UIEven,包括触摸事件处理、[performSelector: onThread: ]
,这个也可以通过代码来验证一下。首先看一下触摸事件,在ViewController
里面重写方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"点击屏幕");
}
#9 0x00007fff2039038a in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
可以看出系统是通过一个CF的函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
来调用UIKit进行事件处理的
source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)。官方也指出可以自定义Source,因此对于CFRunLoopSourceRef来说它更像一种协议,框架已经默认定义了两种实现,如果有必要开发人员也可以自定义,详细情况可以查看官方文档。
RunLoop 的状态:
RunLoop
有以下几种状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), //即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入 休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 以上所有状态
};
下面我们写代码来验证一下这些状态的切换.首先写代码测试一下,NStimer
唤醒RunLoop
:
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
}
RUN>
可以看出,Runloop的状态切换时,都会被
observer
监听到。
我们再创建一个UITextView
,拖动UITextView
看看RunLoop
状态的切换情况
- (void)viewDidLoad {
[super viewDidLoad];
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
}
RUN>
拖动
UITextView
看看2021-05-12 12:46:02.808851+0800 Interview03-RunLoop[2854:125671] kCFRunLoopExit - kCFRunLoopDefaultMode 2021-05-12 12:46:02.809032+0800 Interview03-RunLoop[2854:125671] kCFRunLoopEntry - UITrackingRunLoopMode 2021-05-12 12:46:04.280133+0800 Interview03-RunLoop[2854:125671] kCFRunLoopExit - UITrackingRunLoopMode 2021-05-12 12:46:04.280280+0800 Interview03-RunLoop[2854:125671] kCFRunLoopEntry - kCFRunLoopDefaultMode
可以看到
RunLoop
频繁的在kCFRunLoopDefaultMode
和UITrackingRunLoopMode
之间切换.
特别备注
本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!