RunLoop 的作用是什么?它的内部工作机制.
概念
RunLoop是一个相对抽象的概念,在程序运行中循环做一些事情,主要应用于:定时器(Timer)、PerformSelector、GCD Async To Main Queue、事件详情、手势识别、界面刷新、网络请求、AutoreleasePool
我们想象一个场景:为什么App程序启动之后能够持续运行在前台呢?
int main(int argc, char * argv[]){
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
UIApplicationMain的大致实现原理就是:(伪代码)
int retVal = 0;
do {
int message = sleep_and_wait(); //睡眠中等待消息(比如响应点击各种事件)
retVal = proess_message(message);//处理消息,更改返回值,如果为0,代表程序退出,不为0,程序持续运行。
}while(retVal == 0);
正因为RunLoop底层在执行一个while循环,来维持程序的不退出。
RunLoop的基本作用:
- 保持程序不会马上退出,而是保持运行状态。
- 处理App中的各种事件(比如触摸,定时器时间等)
- 节省CPU资源,提高程序性能,有事做做事,没事做休眠。
RunLoop跟线程的关系
为什么聊Runloop一定要搭上线程,我们知道,程序里的每一句代码,都会在线程里面执行,下面要讲到获取Runloop对象的代码也不例外,一定是跑在线程里面的.之前我们说道,Runloop是为了让程序不退出,其实更准确地说,是为了保持某个线程不结束,只要还有未结束的线程,那么整个程序就不会退出,因为线程是程序的运行调度的基本单元.
线程与Runloop的关系是一对一的,一个新创建的线程,是没有Runloop对象的,当我们在该线程里第一次通过上面的API获得Runloop时,Runloop对象才会被创建,并且通过一个全局字典将Runloop对象和该线程存储绑定在一起,形成一对一关系。
Runloop会在线程结束时销毁,主线程的Runloop已经自动获取过(创建),子线程默认没有开启RunLoop(直到你在该线程获取它)。RunLoop对象创建后,会被保存在一个全局的Dictionary里,线程作为key,Runloop对象作为value。
- 每条线程都有唯一的一个与之对用的RunLoop对象
- RunLoop保存在一个全局的Dictionary中,线程作为key,RunLoop作为value
- 线程创建时并没有RunLoop对象,RunLoop会在第一次获取的时候创建。
- RunLoop会在线程结束的时候销毁
- 主线程的RunLoop已经自动创建,子线程默认不开启RunLoop。
RunLoop有两种获取方式
在OC中:[NSRunLoop currentRunLoop]
C的:CFRunLoopRef runloop = CFRunLoopGetCurrent();
启动一个runloop有以下三种方法:
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
这三种方式无论通过哪一种方式启动runloop,如果没有一个输入源或者timer附加于runloop上,runloop就会立刻退出。
-
第一种方式,runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
-
第二种方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
-
第三种方式,runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出。
前两种启动方式会重复调用runMode:beforeDate:方法。
我们还可以在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;
};
**************????__CFRunLoopMode????***********
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
Boolean _stopped;
char _padding[3];
//♥️♥️♥️♥️♥️♥️♥️核心组成♥️♥️♥️♥️♥️♥️
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
//♥️♥️♥️♥️♥️♥️♥️核心组成♥️♥️♥️♥️♥️♥️
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
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 */
};
退出RunLoop的方式
第一种启动方式的退出方法
文档说,如果想退出runloop,不应该使用第一种启动方式来启动runloop。
如果runloop没有input sources或者附加的timer,runloop就会退出。
虽然这样可以将runloop退出,但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。
第二种启动方式runUntilDate:
可以通过设置超时时间来退出runloop。
第三种启动方式runMode:beforeDate:
通过这种方式启动,runloop会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。
如果我们想控制runloop的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:,
具体可以参考苹果文档给出的方案,如下:
NSRunLoop *myLoop = [NSRunLoop currentRunLoop];
myPort = (NSMachPort *)[NSMachPort port];
[myLoop addPort:_port forMode:NSDefaultRunLoopMode];
BOOL isLoopRunning = YES; // global
while (isLoopRunning && [myLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
//关闭runloop的地方
- (void)quitLoop
{
isLoopRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}
总之
如果不想退出runloop可以使用第一种方式启动runloop;
使用第二种方式启动runloop,可以通过设置超时时间来退出;
使用第三种方式启动runloop,可以通过设置超时时间或者使用CFRunLoopStop方法来退出。