iOS NSRunloop
什么是Runloop
Runloop即运行循环。为什么你的APP放在那里不去动它,在某个时间点去操作它,它还会给你反馈。就是因为Runloop的存在。
总结一下,因为Runloop的存在,保证你的程序不会死。
主要负责什么?
- 使程序一直运行并接受用户输入
- 决定程序在何时处理一些Event
- 调用解耦(Message Queue)
- 节省CPU时间(没事的时候闲着,有事的时候处理)
谁依赖NSRunloop
- NSTimer
- UIEvent
- autorelease
- NSObject(NSDelaydPerforming)
- NSObject(NSThreadPerformAddtion)
- CADisplayLink
- CATransition
- CAAnimation
- dispatch_get_main_queue()
- AFNetworking(NSURLConnection)
- ...
主线程几乎所有的函数都从以下的6个之1的调起
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
构成元素
因为NSRunloop是对CFRunloop的封装,所以这里只看CFRunLoop就可以了。
CFRunLoopTimer的封装
系统提供的NSTimer、CADisplayLink、performSelector等都是对CFRunLoopTimer的封装。
CFRunLoopSource
Source是RunLoop的数据源抽象类(用OC的话来讲就是protocol)。
RunLoop定义了两个版本的Source,分别是Source0和Source1。
- Source0:处理APP内部事件、APP自己负责管理(触发),如UIEvent、CFSocket
- Source1:由RunLoop和内核管理,Mach Port驱动,如CFMachPort、CFMessagePort
CFRunLoopObserver
观察者,向外部报告RunLoop当前状态的更改
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
框架中很多机制都由CFRunLoopObserver触发,比如CAAnimation
举例:
self.navigationController pushViewController:<#(nonnull UIViewController *)#> animated:<#(BOOL)#>
当程序执行完这行代码时,我们可以看到经历push动画之后,到达了一个新的界面。
但其实并不是执行完这行代码就出现了Push的动画。
其实,执行这段代码时不会立刻就掉push动画,而是要RunLoop循环一圈收集所有的Animation操作,汇集起来一起去调。
CFRunLoopObserver与AutoreleasePool
对象的释放并不是在{}括号结束。而是稍微延迟了一点。
堆栈如下:
_wrapRunLoopAutoreleasePoolHandler
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
UIKit通过RunLoopOberser在RunLoop两次Sleep间对AutoreleasePool进行Pop和Push,将这次Loop产生的Autorelease对象释放。
也就是RunLoop跑一圈没事了就睡,被唤醒了再跑下一圈,在两次sleep之间对自动释放池进行释放。
CFRunLoopMode
注意
RunLoop在同一段时间只能且必须在一种特定Mode下Run。
更换Mode时,需要停止当前Loop,然后重启新Mode。
Mode是iOS滑动顺畅的关键。
类型
- NSDefaultRunLoopMode
默认状态(空闲状态),比如点击按钮都是这个状态 - UITrackingRunLoopMode
滑动时的Mode。比如滑动UIScrollView时。 - UIInitializationRunLoopMode
私有的,APP启动时。就是从iphone桌面点击APP的图标进入APP到第一个界面展示之前,在第一个界面显示出来后,UIInitializationRunLoopMode就被切换成了NSDefaultRunLoopMode。 - NSRunLoopCommonModes
它是NSDefaultRunLoopMode和UITrackingRunLoopMode的集合。结构类似于一个数组。在这个mode下执行其实就是两个mode都能执行而已。
典型的应用场景这样:当前界面有开启一个NSTimer,并且滑动UIScrollView。正常开启NSTimer后,滑动UIScrollView时它是不滑动的。解决办法就是把这个timer加入到当前的RunLoop,并把RunLoop的mode设置为NSRunLoopCommonModes。这样就可以保证不管你是NSDefaultRunLoopMode里跑,还是UITrackingRunLoopMode里跑,这个timer都可以执行。
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.0625
target:self
selector:@selector(progressChange)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
当你开始滑动UIScrollView时,RunLoop的mode状态变化如下:
NSDefaultRunLoopMode -> UITrackingRunLoopMode -> NSDefaultRunLoopMode
开始滑动时,第一次mode的切换会把NSDefaultRunLoopMode停掉。然后开启新的UITrackingRunLoopMode。当滑动停止时,由UITrackingRunLoopMode切换回NSDefaultRunLoopMode,这时UITrackingRunLoopMode被停止,又切换回了老的NSDefaultRunLoopMode(这个老的NSDefaultRunLoopMode应该是重新开始的)。
RunLoop和GCD的关系
RunLoop和GCD的关系,准确来说是只要使用了dispatch_get_main_queue(),就与RunLoop有了关系。
因为GCD中dispatch到main queue的block被分发到main RunLoop执行。
RunLoop的挂起和唤醒
我写了个demo,运行,然后点击debug栏的暂停,查看堆栈,如下:
- 指定用于唤醒的mach_port接口
- 调用mach_msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach_msg_trap状态
- 由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活。
RunLoop迭代执行顺序
伪代码:
SetupThisRunLoopRunTimeoutTimer(); //by GCD timer
do{
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
__CFRunLoopDoSource0();
CheckIfExistMessagesInMainDispatchQueue(); //GCD
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
var wakeUpPort = SleepAndWaitForWakingUpPorts();
//mach_msg_trap
//Zzz...
//Received mach_msg , wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//Handle msgs
if(wakeUpPort == timerPort){
__CFRunLoopDoTimers();
}else if(wakeUpPort == mainDispatchQueuePort) {
//GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
}else {
__CFRunLoopDoSource1();
}
__CFRunLoopDOBlocks();
}while(!stop && !timeout);
代码解读
//首先do..while循环不能是一个死循环,所以在这里设置一个过期时间
//这件事是GCD干的,用来检测do..while循环跑了多久
SetupThisRunLoopRunTimeoutTimer();
//开始跑循环
do{
//告诉observer我要跑timer了
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
//告诉observer我要跑source了
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
//程序跑到这里会查询Source0有什么消息
__CFRunLoopDoSource0();
//询问GCD你有没有存在主线程的东西需要我帮你调
CheckIfExistMessagesInMainDispatchQueue(); //GCD
//告诉observer我要睡了,RunLoop进入到挂起状态
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
//进入trap状态,程序跑到这里就卡在这不动了,等待被某个Port唤醒
var wakeUpPort = SleepAndWaitForWakingUpPorts();
//被唤醒后,告诉observer我被唤醒了
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//假如是被timer唤醒的
if(wakeUpPort == timerPort){
//就去循环遍历和timer有关的回调
__CFRunLoopDoTimers();
}else if(wakeUpPort == mainDispatchQueuePort) {
//如果是主线程的GCD把我唤醒的,那RunLoop就知道GCD要让它做事了,然后就取调GCD的这些事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
}else {
//如果都不是,就是Source1,Source1是基于Port事件的,比如网络某个端口来数据了,就会把RunLoop唤醒,去对来的数据进行处理
__CFRunLoopDoSource1();
}
__CFRunLoopDoBlocks();
}while(!stop && !timeout);
//判断条件:有没有被外部干掉 && 到了过期时间
//如果过期时间不手动进行设置的话,默认值是一个很大的值,可能是Int_Max
AFNetworking是如何玩转RunLoop的
+ (void)networkRequestThreadEntryPoint:(id)_unuserd object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
//为了不让runloop run起来没事干导致消失
//所以给runloop加了一个NSMachPort,给它一个mode去监听
//实际上port什么也没干,就是让runloop一直在等,目的就是让runloop一直活着
//这是一个创建常驻服务线程的好方法
NSRunloop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkReuqestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread =
[[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
一个TableView延迟加载图片的新思路
以前是怎么解决的?
通过UITableView的代理方法,判断如果处于滑动状态就不去设置cell上的图片,如果没有处于滑动状态就取设置cell上的图片。
而现在通过Runloop就有一个十分简便的方法。
//在cell里面把设置图片的事情在NSDefaultRunloopMode里面去做。
//当主线程的tableview不再滑动的时候就会去设置图片
UIImage *dowloadImage = ...;
[self.iconImageView performSelector:@selector(setImage:) withObject:dowloadImage afterDelay:0 inModes:@[NSDefaultRunloopMode]];
这样去设置图片就简便了很多,不用再去判断tableview的代理方法。代码也会很清爽。
iOS NSRunloop的更多相关文章
- iOS NSRunloop的简单理解
最近学习了下NSRunloop. 作一下简单的理解: 1.runloop与线程的关系,每一个线程创建是都会有伴有一个runloop诞生,runloop用来接收事件源,让线程执行事件.当没有事件处理时, ...
- [iOS]浅谈NSRunloop工作原理和相关应用
一. 认识NSRunloop 1.1 NSRunloop与程序运行 那么具体什么是NSRunLoop呢?其实NSRunLoop的本质是一个消息机制的处理模式.让我们首先来看一下程序的入口——main ...
- IOS开发中NSRunloop跟NSTimer的问题
在Windows时代,大家肯定对SendMessage,PostMessage,GetMessage有所了解,这些都是windows中的消息处理函数,那对应在ios中是什么呢,其实就是NSRunloo ...
- iOS多线程的初步研究(三)-- NSRunLoop
弄清楚NSRunLoop确实需要花时间,这个类的概念和模式似乎是Apple的平台独有(iOS+MacOSX),很难彻底搞懂(iOS没开源,呜呜). 官网的解释是说run loop可以用于处理异步事件, ...
- iOS开发之NSRunLoop的进一步理解
http://www.cnblogs.com/pengyingh/articles/2343920.html iPhone应用开发中关于NSRunLoop的概述是本文要介绍的内容,NSRunLoop是 ...
- IOS OC 多任务定时器 NSRunLoop 管理 NSTimer
下面有两种做法 1.使用日期组件 NSDateComponents 2.使用NSString 生成一个日期 // 创建一个日历对象 NSCalendar *calendar = [NSCalenda ...
- IOS 多线程02-pthread 、 NSThread 、GCD 、NSOperationQueue、NSRunLoop
注:本人是翻译过来,并且加上本人的一点见解. 要点: 1.前言 2.pthread 3.NSThread 4.Grand Central Dispatch(GCD) 5.Operation Queue ...
- IOS中的多线程和NSRunLoop概述(转载)
线程概述 有些程序是一条直线,从起点到终点,如Hello World,运行打印完,它的生命周期便结束了:有些程序是一个圆,不断循环,直到将它切断,如操作系统,一直运行直到你关机. 一个运行着的程序就 ...
- iOS总结_UI层自我复习总结
UI层复习笔记 在main文件中,UIApplicationMain函数一共做了三件事 根据第三个参数创建了一个应用程序对象 默认写nil,即创建的是UIApplication类型的对象,此对象看成是 ...
随机推荐
- [NOIP2017]时间复杂度
题目描述 小明正在学习一种新的编程语言 A++,刚学会循环语句的他激动地写了好多程序并 给出了他自己算出的时间复杂度,可他的编程老师实在不想一个一个检查小明的程序, 于是你的机会来啦!下面请你编写程序 ...
- Servlet3.0异步请求
在Servlet3.0之前,Servlet采用Thread-Per-Request的方式处理请求 即每次Http请求都有一个线程从头到尾负责处理 如果一个请求需要进行IO操作,比如访问数据库.调用第三 ...
- 搭建时间服务器(linux)
我们搭建集群环境的时候,时间必须是要统一的,才能保证集群数据的一致性. 一般操作是直接使用NTP,跟默认的时间服务器同步,但是最好还是让所有节点跟集群中的某台作为时间服务器的节点同步. 步骤:(节点有 ...
- Redis之基本数据类型
Redis 基本数据类型 Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合). redis类型 含义 S ...
- 解题报告:hdu1248寒冰王座 - 完全背包模板
2017-09-03 16:16:38 writer:pprp 完全背包问题:从左向右进行扫描,用一维阵列进行分析 代码如下: /* @theme:hdu1248 寒冰王座 @writer:pprp ...
- Javascript实用技巧
1. 给参数赋默认值 //通常写法 function dateRender(format){ if(format){ format = 'Y-m-d'; } // code } //强推 functi ...
- Memcached CAS 命令
Memcached CAS(Check-And-Set 或 Compare-And-Swap) 命令用于执行一个"检查并设置"的操作 它仅在当前客户端最后一次取值后,该key 对应 ...
- JS怎么计算html标签里文字的宽度
方法: 做一个空的html 标签 id为“ruler”,样式为“position:absolute;visibility: hidden; white-space: nowrap;z-index: - ...
- js事件在不同浏览器之间的差异
目录: 1. 介绍 2. 不同浏览器之间的差异 2.1 添加事件的方法 2.2 事件对象event 2.3 event中的属性/方法 3. 总结 1. 介绍 javascript与HTML之间的交互是 ...
- Art-Template模板引擎(原生写法与简洁写法)
模板引擎:把js数据转换成html需要的页面,这就是模板引擎需要做的事 • native原生语法 1. 准备数据 2. 把数据转化成html格式的字符串 使用模板引擎 artT ...