什么是Runloop
Runloop顾名思义,就是运行循环。首先它根程序运行过程有关系,其次它是一种转圈圈的效果。但如果这么解释,恐怕谁都听不懂。
想要弄明白Runloop,就要搞清楚跟它有关联的一些概念,以及它自身的运作原理。
没有Runloop的程序
我们通过Xcode新建一个命令行项目,main.m
文件里的代码如下
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
运行程序,程序在执行完代码NSLog(@"Hello, World!");
之后,就会通过 return 0;
推出程序,这是一种线性的执行流程,从上到下,很容易理解。
当我们遇见Runloop
我们再新建一个iOS项目,你看到的main.m
文件是这个样子的
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我们很清楚,运行程序之后,我们会进入app的界面,然后app就不会退出了,会一直运行着。你有没有好奇过,同样都是main
函数,为啥下面的这个能够不退出呢?对,这就是Runloop的功劳。
在命令行工程里面的main.m
里面,是没有加Runloop的,而iOS工程的main.m
里面,其实在UIApplicationMain()
这个方法中,系统加上了Runloop,让程序可以一直循环运行下去不退出。
这么解释感觉还是有点僵硬,下面用伪代码的形式来描述一下UIApplicationMain()
内部情况
说白了, Runloop其实就是一个do-while
循环,每次循环一圈,都会判断一次retVal
,决定是否结束循环,继续执行循环外的代码。
Runloop的作用简述
- Runloop的
do-while
本质说明它就是为了保持程序的持续运行,不退出(试想一下手机上的app如果一打开就直接退出完事了,这个世界可能就没有手机什么事了) - 保持程序持续运行的根本目的,就是为了处理app的各种事件(触摸事件,定时器事件),因为这些事件并不是编写程序的时候就定好的,天知道用户什么时候会点击某个按钮,对吧。因此想我们一开始说的那种线性的程序运行方式,就无法处理这样的场景。当加上Runloop之后,在它的一次循环中,就可以去处理程序已经接收到的各种事件,在处理这些事件的过程中,程序还会不断的接受新来的事件,这样,下次循环就可以继续处理新来的事件。
- 如果Runloop在某次循环之后,发现程序突然没有收集到更多事件供它去处理,它就会休眠,实际上就是系统让程序停在了Runloop的
do-while
循环里面的某一段代码上(注意程序此时是停住,而不是退出哦),如果过了一会程序为Runloop接受到了新来的事件,它的do-while
循环就会被系统重新激活以继续运行。这种机制的好处是,当Runloop休眠的时候,CPU可以不用在它跟前侯着,转而把时间精力分配给别的事务,提高了CPU的使用效率。
你可把CPU想象成一个妈妈,Runloop就是刚出生的宝宝,宝宝醒的时候,妈妈就必须一直看着他,没功夫去干别的事情,宝宝睡眠的时候,妈妈就可以抓紧时间去做一些别的事情了。如果没有Runloop休眠机制,相当于这个宝宝永远不会睡觉,那这个妈妈不就凉凉了嘛~~
深入理解Runloop对象
iOS中Runloop的API
- Foundation:
NSRunLoop
- Core Foundation:
CFRunLoopRef
NSRunLoop
和CFRunLoopRef
都代表Runloop对象,NSRunLoop
是基于CFRunLoopRef
的一层OC包装,CFRunLoopRef
是开源的
Runloop对象的获取
-
Foundation
[NSRunloop currentRunLoop];
获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop];
获得主线程的Runloop对象 -
Core Foundation
CFRunLoopGetCurrent();
获得当前线程的RunLoop对象
CFRunLoopGetMain();
获得主线程的Runloop对象
Runloop与线程
为什么聊Runloop一定要搭上线程?我们知道,程序里的每一句代码,都会在线程(主线程/子线程
)里面被执行,上面四种获得Runloop对象的代码也不例外,一定是跑在线程里面的。之前我们说到,Runloop是为了让程序不退出,其实更准确地说,是为了保持某个线程不结束,只要还有未结束的线程,那么整个程序就不会退出,因为线程是程序的运行和调度的基本单元。
线程与Runloop的关系是一对一
的,一个新创建的线程,是没有Runloop对象的,当我们在该线程里第一次通过上面的API获得Runloop时,Runloop对象才会被创建,并且通过一个全局字典将Runloop对象和该线程存储绑定在一起,形成一对一关系。
Runloop会在线程结束时销毁,主线程的Runloop已经自动获取过(创建),子线程默认没有开启RunLoop(直到你在该线程获取它)。RunLoop对象创建后,会被保存在一个全局的Dictionary里,线程作为key
,Runloop对象作为value
。
关于Runloop的创建和保存,我们还可以在CF源码里面详细看看,Runloop的信息是写在CF源码文件夹的CFRunLoop.c
文件里面,我们可以在里面搜索到CFRunLoopGetCurrent()
函数的实现
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
CFRunLoopGetCurrent()
中又是通过_CFRunLoopGet0
来获得Runloop对象的
Runloop对象底层结构
我们可以在源码CFRunloop.c
中找到Runloop的定义
**************????__CFRunLoop????***********
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
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
uint32_t _winthread;
//♥️♥️♥️♥️♥️♥️♥️核心组成♥️♥️♥️♥️♥️♥️
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
//♥️♥️♥️♥️♥️♥️♥️核心组成♥️♥️♥️♥️♥️♥️
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;