RunLoop相关知识
RunLoop,翻译过来是运行环路。我们在创建命令行项目和创建ios项目时,发现命令行项目当最后一行代码执行完后项目就自动退出了,而ios项目确可以一直运行,知道用户手动点击退出按钮。这就是因为ios项目在main函数中自动创建了runLoop,从而可以使项目可以一直响应用户的操作。
int main(int argc, char * argv[]) {
    @autoreleasepool {
       //这行代码 会自动创建主线程的RunLoop
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
我们可以将这个过程我们可以简化成:
  
我们从这个过程可以看出RunLoop的基本作用
 保持程序的持续运行
 处理App中的各种事件(比如触摸事件、定时器事件等)
 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
  ......
我们平时开发中,涉及到RunLoop的挺多的,比如说定时器、手势识别、网络请求等等,
  
一、RunLoop的结构
iOS中有2套API来访问和使用RunLoop:
①Foundation:NSRunLoop,它是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
②Core Foundation:CFRunLoopRef,它提供了纯 C 函数的 API,所有这些 API都是线程安全的。(CFRunLoopRef是开源的)
两者关系:

所以我们获取RunLoop对象也有两种方法:
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象 Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
因为CFRunLoopRef是开源的,所以我们可以通过它来看一下它的实现结构。来到CFRunLoop.c文件中,找到了RunLoop的结构体定义:
//已剔除非必要部分
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};
这里的Set和数组类似,只不过数组是有序的,而set是无序的,都是用来存放数据的,所以 CFMutableSetRef可以理解成可变数组,也就是说在一个RunLoop对象中,存储着一个线程对象,三个可变数组,一个当前模式。那么CFRunLoopModeRef又是什么呢?
我们找到了它的定义:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
//剔除了其他无关属性
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
所以RunLoop的结构是这样的:

_pthread就是RunLoop对应的线程,每条线程都有唯一的一个与之对应的RunLoop对象。
_commonModeItems和_commonModes是用来存放某些特定模式和模式内事件的,接下来会讲到。
_currentMode,RunLoop当前所处的模式,当前模式是从_modes里面选择的。
_modes:RunLoop的运行模式,一共有五种,但是我们经常用的就两三种:
- kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
- UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)页面滚动式所处的模式
- kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
我们上面提到的_commonModeItems和_commonModes就是存放kCFRunLoopCommonModes这种模式的数据的。CommonModes其实并不是一种真正的模式,而是指可以在标记为Common Modes的模式下运行的伪模式。 目前被标记为Common Modes的模式: kCFRunLoopDefaultMode,UITrackingRunLoopMode,简单来说目前kCFRunLoopCommonModes就是指kCFRunLoopDefaultMode+UITrackingRunLoopMode。比如,我们经常遇到在tableview添加定时器后,当tableview滚动后timer就不响应了。
这是因为tableview滚动式处在UITrackingRunLoopMode模式下的,而定时器默认是处在kCFRunLoopDefaultMode下的,所以当模式切换后,RunLoop就无法响应之前模式的时间了,故而无法响应定时器时间。所以我们的方案是将定时器添加到RunLoop的kCFRunLoopCommonModes模式下,这样无论是否滑动tableview都可以响应定时器事件了。
这里还需要注意的一点是:如果需要切换 Mode,只能退出Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
接下来,我们再来看一下这个RunLoop中的模式指的是什么?有什么作用?
我们前面通过源码,看到了CFRunLoopMode的结构,里面有sources0、sources1、timer、observers,其实这里面就存储着app要处理的种种事情,它们分别负责不同的工作。它们的分工是这样的:(个人认为sources0和sources1其实是一个整体,当事件发生时sources1先去获取这个时间,涉及不到端口或内核或其他线程的事情的话就交给sources0处理,其余的自己处理)
sources0:只包含了一个回调(函数指针),它并不能主动触发事件,比如点击事件等操作都是通过sources0处理的。
sources1:包含了一个 mach_port 和一个回调(函数指针),用于通过内核和其他线程相互发送消息,这种 Source 能主动唤醒 RunLoop 的线程。
timer:是基于时间的触发器,其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
observers:是观察者,当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
RunLoop的状态有一下几种:


需要注意的一点是:如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
二、RunLoop与线程
关于RunLoop与线程的关系,我们可以总结以下几点:
每条线程都有唯一的一个与之对应的RunLoop对象 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop,子线程没有开启RunLoop的话就跟命令行项目一样,任务执行完就会结束 RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value RunLoop会在线程结束时销毁
接下来,我们通过源码来验证:
当我们获取线程的Runloop的时候,发现RunLoop没有获取到话,都会调用__CFRunLoopGet0, 并把线程作为参数传递
  
继续,跳转至__CFRunLoopGet0,如下:

发现,RunLoop与线程的关系是一对一的,并且用了个全局字典保存了起来,线程作为key,RunLoop作为value。
我们发现如果线程没有启用RunLoop后会执行完马上销毁:

添加RunLoop后,发现还是运行完就销毁:这是因为如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

所以我们需要往Model中添加一个数据:

发现确实执行完后,线程阻塞了,一直没有被销毁,这是因为当runtime创建后,如果没有被事件唤醒后它就一直在休眠,cpu就不会继续处理事情,所以阻塞在这。
三、RunLoop的运行逻辑
我们在了解RunLoop的结构以及与线程的关系后,我们再来看一下RunLoop的运行流程:

接下来,我们通过源码来看一下RunLoop是如何处理这些事件的?
关于入口的查找,我们可以现在touchesBegan:方法中打个断点,查看程序是怎么执行到这的:

//RunLoop入口
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
//通知Observers 进入RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); //RunLoop的具体运行
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); //通知Observers 退出RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
} //RunLoop的具体运行
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int32_t retVal = ;
do {
//通知Observers 即将处理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知Observers 即将处理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//处理Block
__CFRunLoopDoBlocks(rl, rlm); //处理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
//处理Block
__CFRunLoopDoBlocks(rl, rlm);
} Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // 如果当前是主线程的runloop,并且主线程有事情需要处理,则跳转至handle_msg处理,即跳过休眠 这条指令网上大部分说法是指判断Sources1中是否有事情处理,个人觉得这个说法不太对,这篇文章中有正面:资料
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, , &voucherState, NULL)) {
goto handle_msg;
} //通知Observers 即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//开始休眠
__CFRunLoopSetSleeping(rl); //等待别的消息来唤醒当前线程 如果没有消息就会一直在这休眠 阻塞在这 cpu不工作 有消息的话则唤醒执行
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? : TIMEOUT_INFINITY, &voucherState, &voucherCopy); //结束休眠
__CFRunLoopUnsetSleeping(rl);
//通知Observers 结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); //handle_msg
handle_msg:;
if (被timer唤醒) {
//处理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
}
else if (被gcd唤醒) {
//处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {//被sources1唤醒
//处理Sources1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
//处理Block
__CFRunLoopDoBlocks(rl, rlm); //处理返回值
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 ( == retVal); return retVal;
}
简化成流程图 则是:
  
四、RunLoop的应用
控制线程生命周期(线程保活),比较经典的就是AFNetworking案例;
解决NSTimer在滑动时停止工作的问题,这个是我们平时开发中遇到过的;
相关参考资料:
RunLoop相关知识的更多相关文章
- RunLoop相关知识的总结
		
RunLoop 即运行循环,也叫事件循环,本质为一个死循环.iOS一个程序运行起来之后,默认会开启一个运行循环,有需要处理的操作时,比如用户的输入事件时,RunLoop会自己跑起来运行,没有需要处理的 ...
 - Runloop基础知识
		
*:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...
 - 【Python五篇慢慢弹(5)】类的继承案例解析,python相关知识延伸
		
类的继承案例解析,python相关知识延伸 作者:白宁超 2016年10月10日22:36:57 摘要:继<快速上手学python>一文之后,笔者又将python官方文档认真学习下.官方给 ...
 - 移动WEB像素相关知识
		
了解移动web像素的知识,主要是为了切图时心中有数.本文主要围绕一个问题:怎样根据设备厂商提供的屏幕尺寸和物理像素得到我们切图需要的逻辑像素?围绕这个问题以iphone5为例讲解涉及到的web像素相关 ...
 - listener监听器的相关知识
		
从别人的博客上我学习了listener的相关知识现在分享给大家 1.概念: 监听器就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上 ...
 - UIViewController相关知识
		
title: UIViewController 相关知识date: 2015-12-13 11:50categories: IOS tags: UIViewController 小小程序猿我的博客:h ...
 - 【转】java NIO 相关知识
		
原文地址:http://www.iteye.com/magazines/132-Java-NIO Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的 ...
 - NSString使用stringWithFormat拼接的相关知识
		
NSString使用stringWithFormat拼接的相关知识 保留2位小数点 1 2 3 4 //.2代表小数点后面保留2位(2代表保留的数量) NSString *string = [NSSt ...
 - iOS网络相关知识总结
		
iOS网络相关知识总结 1.关于请求NSURLRequest? 我们经常讲的GET/POST/PUT等请求是指我们要向服务器发出的NSMutableURLRequest的类型; 我们可以设置Reque ...
 
随机推荐
- MySQL5.7本地首次登录win10报错修改
			
1.打开MySQL目录下的my.ini文件,在文件的最后添加一行“skip-grant-tables”,保存并关闭文件.(Win10默认安装,my.ini在C:\ProgramData\MySQL\M ...
 - 小白两篇博客熟练操作MySQL  之   第二篇
			
小白两篇博客熟练操作MySQL 之 第二篇 一. 视图 视图是一个虚拟表,其本质是根据SQL语句获取动态的数据集,并为其命名,用户使用时只需使用名称即可获取结果集, 并可以将其当做表来使用. s ...
 - 3.3.4 lambda 表达式
			
lambda表达式常用来声明匿名函数,即没有函数名字的临时使用的小函数,例如第2章中列表对象的sort()方法以及内置函数sorted()中key参数.lambda表达式只可以包含一个表达式,不允许包 ...
 - Navicat premium连接Oracle报ORA-12545错误
			
1:ORA-12545 原因: 这里填localhost,127.0.0.1,或者远程ip.
 - Package pdftex.def Error: PDF mode expected, but DVI mode detected!
			
本系列文章由 @yhl_leo 出品,转载请注明出处. 文章链接: http://blog.csdn.net/yhl_leo/article/details/51646781 在如下使用LaTeX编译 ...
 - Android音乐、视频类APP常用控件:DraggablePanel(1)
			
 Android音乐.视频类APP常用控件:DraggablePanel(1) Android的音乐视频类APP开发中,常涉及到用户拖曳视频.音乐播放器产生一定交互响应的设计需求,最典型的以You ...
 - [TS-A1488][2013中国国家集训队第二次作业]魔法波[高斯消元]
			
暴力直接解异或方程组,O(n^6)无法接受,那么我们考虑把格子分块,横着和竖着分别分为互不影响的块,这样因为障碍物最多不超过200个,那么块的个数最多为2*(800+200)=2000个,最后用bit ...
 - Surround the Trees HDU 1392 凸包
			
Problem Description There are a lot of trees in an area. A peasant wants to buy a rope to surround a ...
 - F - True Liars 带权并查集
			
After having drifted about in a small boat for a couple of days, Akira Crusoe Maeda was finally cast ...
 - 认识GIT之入门
			
前言 GIT是非常优秀的源代码版本管理工具,经过几年的发展,已经变得非常成熟以及流行,不同于其他的源代码管理系统,值得使用.GIT官网下载在线安装包,经常会中途退出,很有可能的原因是被墙了,所以建议使 ...