iOS底层原理总结探寻RunLoop本质

释放双眼,带上耳机,听听看~!

上篇《iOS底层原理总结 – 探寻Runtime本质(四)》

本篇学习总结:

  • RunLoop简介
  • RunLoop 基本作用
  • RunLoop开启
  • RunLoop对象
  • RunLoop跟线程之间的关系
  • RunLoop结构体
  • RunLoop相关类及作用
  • RunLoop处理逻辑
  • RunLoop退出
  • RunLoop应用

好了,带着问题,我们一一开始阅读吧 ?
RunLoop是一个老生常谈的知识点了,本篇RunLoop记录的比较细碎,开始RunLoop之前我们先看一下关于RunLoop的面试题
面试题

1.讲讲 RunLoop,项目中有用到吗?
2.RunLoop内部实现逻辑?
3.Runloop和线程的关系?
4.timer 与 Runloop 的关系?
5.程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
6.Runloop 是怎么响应用户操作的, 具体流程是什么样的?
7.说说RunLoop的几种状态?
8.Runloop的mode作用是什么?

一.RunLoop简介
  • 什么是RunLoop呢?

RunLoop就是运行循环,在程序运行过程中循环做一些事情,如果没有RunLoop程序执行完毕就会立刻退出,如果有RunLoop程序会一直运行,并且时时刻刻在等待用户的输入操作。
RunLoop可以在需要的时候自己跑起来,在没有操作的时候就停下来休息,充分节省CPU资源,提高程序性能。

二.RunLoop 基本作用

1.保持程序持续运行,程序一启动默认开启主线程,主线程一开起来就会跑一个主线程对应的RunLoopRunLoop保证主线程不会被销毁,也保证了程序持续进行
2.处理App中各种事件(比如:触摸事件 ,定时器事件,Selector事件
3.节省CPU资源,提高程序性能

RunLoop 官方图解如下:

 

RunLoop内部运行原理
官方图.png

解析图片: 通过图片可以看出,RunLoop在循环过程中,当接收到Input sources或者Timer source时就会交给对应的处理方去处理,当没有事件消息传入的时候,RunLoop让线程进入休眠阶段。后期会更近一步讲解RunLoop,此处不做过多解释。

三.RunLoop开启

当我们打开App的时候,首先进入的是UIApplicationMain函数,RunLoop的作用是维持程序不退出,说明在UIApplicationMain函数中就启动了RunLoop,每一个应用对应一个默认线程,默认线程必须要有一个对应的RunLoop

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

进入UIApplicationMain函数

UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);

我们发现int main函数返回int数值,我们添加一些断点验证一下UIApplicationMain函数是否开启RunLoop

int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"开始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"结束");
return re;
}
}

运行程序,我们发现只会打印开始,并不会打印结束,这恰好验证了我们前面所说,UIApplicationMain函数中,开启了一个和主线程相关的RunLoop。导致UIApplicationMain函数不会返回,一直在运行中,也就是保证了程序的持续运行。

我们来看一下RunLoop的源码

// 用DefaultMode启动
void CFRunLoopRun(void) {   /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

我们发现UIApplicationMain 函数中创建的RunLoop,内部确实有一个do-while循环,正因为这个do-while循环,UIApplicationMain函数才不会退出,如果没有这个do-while循环,while代码执行一次就会退出RunLoop,进而UIApplicationMain函数也会返回数值,导致程序结束。

四.RunLoop对象

RunLoop有两组对象,一组是C语言的CoreFoundation框架的CFRunLoopRef对象,另一组是OC语言的Fundation框架的NSRunLoop对象。其中Fundation框架是基于CFRunLoopRef的一层分装,这里我们主要研究CFRunLoopRef源码。

1.如何获取RunLoop对象

Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
五.RunLoop跟线程之间的关系

线程跟RunLoop到底什么关系呢?这里系统总结一下,

1.每条线程都有唯一的一个与之对应的RunLoop对象。
2.RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。
3.主线程的RunLoop在程序启动的时候自动创建好了,子线程的RunLoop需要主动创建。
4.RunLoop在第一次获取时创建,在线程结束时销毁。

通过源码查看上述对应关系

// 拿到当前Runloop 调用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// 查看_CFRunLoopGet0方法内部
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);
// 根据传入的主线程获取主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 从字典里面拿,将线程作为key从字典里获取一个loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
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之间是一一对应的,其关系是保存在一个Dictionary里,所以我们创建子线程RunLoop时,只需要子线程中获取当前线程的RunLoop对象即可[NSRunLoop currentRunLoop],如果不获取,那子线程就不会创建与之相关联的RunLoop,并且只能在一个线程的内部获取其RunLoop对象,方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有的话则会创建一个,并将与之对应的子线程存入字典中,当线程结束时,RunLoop会被销毁。

六.RunLoop结构体

想要了解RunLoop结构体,我们还得看源码。通过源码我们找到__CFRunLoop结构体。

//这是将数据汇总在一个结构体中
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;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

除一些记录性属性外,主要来看一下一下两个成员变量

CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

CFRunLoopModeRef其实是指向__CFRunLoopMode结构体的指针,__CFRunLoopMode结构体源码如下:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
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 */
};

主要查看一下成员变量

CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

通过上面分析我们知道,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个·
Source0/Source1/Timer/Observer,而RunLoop启动时只能选择其中一个Model作为currentMode

Source1/Source0/Timers/Observer分别代表什么
1.Source0:触摸事件,PerformSelectors
2.Source1:基于Port的线程间通信

我们通过代码验证一下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"点击了屏幕");
}

打断点之后打印堆栈信息,当xcode工具区打印的堆栈信息不全时,可以在控制台通过bt指令打印完整的堆栈信息,由堆栈信息中可以发现,触摸事件确实是会出发Source0事件。

touchesBegan触发Source0事件.png

同样的方式验证performSelector堆栈信息

dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});

可以发现PerformSelectors同样是出发Source0事件。

PerformSelectors触发Source0事件.png

3.Timer:定时器,比如NSTimer
代码验证一下

[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer调用了");
}];

按照上述方式打印堆栈信息

 

Timer堆栈信息.png

4.Observer:监听器,用于监听RunLoop的状态,UI界面刷新

七.RunLoop相关类及作用

通过上面分块细碎的总结,我们对RunLoop内部结构有了大致的了解,接下来详细分析RunLoop的相关类,以下为Core Foundation中关于RunLoop的5个类

1. CFRunLoopRef:获取线程对应的RunLoop对象
2. CFRunLoopModeRef:RunLoop运行模式,只能选择一种,在不同的模式中做不同的操作。
3. CFRunLoopSourceRef:事件源,输入源
4. CFRunLoopTimerRef:定时器时间
5. CFRunLoopObserverRef:观察者

1. CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式
一个RunLoop包含若干个Mode,每个Mode中又包含若干个Source、Timer、Observer,每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称为CurrentMode,如果需要切换Mode,只能退出当前Mode,重新进入一个Mode,这样做的目的是
为了分隔不同组的Source、Timer、Observer,让其互不影响,如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立刻退出该Mode。
用一张图总结RunLoop跟若干个Mode的关系

CFRunLoopModeRef示意图.png

系统默认注册的5个Mode

RunLoop有一种模式,其中常见的为两种

1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

Mode间的切换
我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况,我们通过一段代码来看一下。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 加入到RunLoop中才可以运行
// 1. 把定时器添加到RunLoop中,并且选择默认运行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 当textFiled滑动的时候,timer失效,停止滑动时,timer恢复
// 原因:当textFiled滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了
// 2. 当我们将timer添加到UITrackingRunLoopMode模式中,此时只有我们在滑动textField时timer才会运行
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 3. 那个如何让timer在两个模式下都可以运行呢?
// 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer
// 3.2 使用站位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,下面两种模式被打上标签
//0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
//2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
// 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
-(void)show
{
NSLog(@"-------");
}

上述几种情况的代码都可以运行看一下运行结果,
a .当我们把NSTimer添加到RunLoop的默认模式下,即kCFRunLoopDefaultMode,滑动界面,NSTimer不起作用。
b .我们将NSTimer添加到滑动模式,即UITrackingRunLoopMode,滑动界面,NSTimer是起作用的。
c.当我们将NSTimer添加到RunLoop模式兼具两种模式,即NSRunLoopCommonModes,滑动界面,NSTimer起作用。
此时的NSRunLoopCommonModes,并不是一种真正的Mode,它是一种标志,代表同时满足两组模式即kCFRunLoopDefaultModeUITrackingRunLoopMode

同样道理的还有ImgeView的显示,我们直接来看代码

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
// performSelector默认是在default模式下运行,因此在滑动ScrollView时,图片不会加载
// [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 ];
// inModes: 传入Mode数组
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];

使用GCD也是创建计时器的,GCD的计时器更为精确,

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.创建一个GCD定时器
/*
第一个参数:表明创建的是一个定时器
第四个参数:队列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
// 局部变量,让指针强引用
self.timer = timer;
//2.设置定时器的开始时间,间隔时间,精准度
/*
第1个参数:要给哪个定时器设置
第2个参数:开始时间
第3个参数:间隔时间
第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
GCD的单位是纳秒 所以要*NSEC_PER_SEC
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.设置定时器要执行的事情
dispatch_source_set_event_handler(timer, ^{
NSLog(@"---%@--",[NSThread currentThread]);
});
// 启动
dispatch_resume(timer);
}

2. CFRunLoopSourceRef(事件源,输入源)
Source分为两种,
Source0:非基于Port的用于用户主动触发的事件(点击button或点击屏幕)
Source1:基于Port的通过内核和其他线程相互发送消息(与内核相关)

3. CFRunLoopObserverRef(观察者)
我们直接来看代码,给RunLoop添加监听者,监听其运行状态

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建监听者
/*
第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
第三个参数 Boolean repeats:YES:持续监听 NO:不持续
第四个参数 CFIndex order:优先级,一般填0即可
第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
*/
/*
所有事件
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
};
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 给RunLoop添加监听者
/*
第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
第二个参数 CFRunLoopObserverRef observer 监听者
第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的内存管理(Core Foundation)
凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
*/
CFRelease(observer);
}

我们来看一下打印结果

 

监听者监听RunLoop运行状态.png

以上可以看出CFRunLoopObserverRef确实是监听RunLoop的状态,包括唤醒,休息,以及处理各种事件。

八.RunLoop处理逻辑

我们再来分析一下RunLoop的处理逻辑,就会简单明了很多,回头看官方RunLoop解释图

 

官方文档RunLoop处理逻辑.png

1.源码解析
下面代码是精简只保留主流程的代码

// 共外部调用的公开的CFRunLoopRun方法,其内部会调用CFRunLoopRunSpecific
void CFRunLoopRun(void) {   /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
// 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
// 通知Observers : 进入Loop
// __CFRunLoopDoObservers内部会调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函数
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心的Loop逻辑
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers : 退出Loop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
// 精简后的 __CFRunLoopRun函数,保留了主要代码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知Observers:即将处理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知Observers:即将处理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 处理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 如果有Sources1,就跳转到handle_msg标记处
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
// 通知Observers:即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 进入休眠,等待其他消息唤醒
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
// 醒来
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopUnsetSleeping(rl);
// 通知Observers:已经唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
if (被Timer唤醒的) {
// 处理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被GCD唤醒的) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Sources1唤醒的
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 执行Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}

上述源码中,相应处理事件函数内部还会调用更底层的函数,内部函数调用才是真正处理事件的函数,通过上面bt打印全部堆栈信息也可以得到验证

__CFRunLoopDoObservers 内部调用
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION

__CFRunLoopDoBlocks 内部调用 CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK

__CFRunLoopDoSources0 内部调用
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION

__CFRunLoopDoTimers 内部调用 CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION

GCD 调用 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE

__CFRunLoopDoSource1 内部调用 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION

2.RunLoop处理逻辑流程图
我们按照源码重新梳理一下流程

RunLoop处理逻辑流程图.png

记住上面的流程,对RunLoop整个工作流程就有了进一步的理解跟记忆。

九.RunLoop退出
  1. 主线程销毁时对应RunLoop退出
    2.Mode中有一些Timer 、Source、 Observer,这些保证Mode不为空时保证RunLoop没有空转,并且是在运行时,当Mode中为空的时候,RunLoop会立刻退出
    3.我们在启动RunLoop的时候可以设置什么时候停止。
[NSRunLoop currentRunLoop]runUntilDate:(停止时间)];
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:(停止时间)];
十.RunLoop应用

1.常驻线程
常驻线程的作用:我们知道,一般子线程任务执行完毕之后就会被销毁了,那么如果我们需要开启一个子线程,在程序运行过程中永远存在,那么如何让子线程永远活着,这时候就会用到常驻线程的方法,给子线程开起一个RunLoop,AFNetworking采用的就是这种技术。

note:子线程执行完操作以后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。

子线程开起RunLoop的代码

#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 创建子线程并开启
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(void)show
{
// 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。
// 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来
NSLog(@"%s",__func__);
// 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入
// 添加Source [NSMachPort port] 添加一个端口
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 添加一个Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//创建监听者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 给RunLoop添加监听者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 2.子线程需要开启RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
NSLog(@"%@",[NSThread currentThread]);
}
@end

注意:创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃

2.自动释放池
Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大,这显然不是我们想要的。
那么什么时候释放,怎么释放呢?

RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新工作时,Timer,Source等新的事件就会被放到新的空的自动释放池中,当RunLoop退出的时候也会被释放。
注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池

@autorelease{
// 执行代码
}

当我们深入理解RunLoop相关知识后,在回去看面试题也就不难了。

人已赞赏
iOS文章

iOS项目运行错误总结

2020-5-10 21:03:12

iOS文章

iOS底层原理总结 - 探寻KVO本质

2020-5-10 22:18:04

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索