一、RunLoop 介绍
程序启动的原理和过程
start
-> (加载framework,动态静态链接库,启动图片,Info.plist等)
-> main函数
-> UIApplicationMain函数
:
- 初始化`UIApplication`单例对象
- 初始化`AppDelegate`对象,并设为`UIApplication`对象的代理
- 检查`Info.plist`设置的`xib`文件是否有效,如果有则解冻`Nib`文件并设置`outlets`,创建显示`key window`、`rootViewController`、与`rootViewController`关联的`根view`(没有关联则看`rootViewController`同名的`xib`),否则`launch`之后由程序员手动加载。
- 建立一个主事件循环,其中包含UIApplication的Runloop来开始处理事件。
Runloop的概念
当有持续的异步任务需求时,我们会创建一个独立的生命周期可控的线程。RunLoop
就是控制线程生命周期并接收事件进行处理的机制。RunLoop
是iOS
事件响应与任务处理最核心的机制,它贯穿iOS
整个系统。使系统更加流畅、省电、响应快,用户体验好。
OSX/iOS
系统中,提供了两个这样的对象:NSRunLoop
和 CFRunLoopRef
。
CFRunLoopRef
是在 CoreFoundation
框架内的,它提供了纯 C
函数的API
,所有这些API
都是线程安全的。
NSRunLoop
是基于 CFRunLoopRef
的封装,提供了面向对象的 API
,但是这些API
不是线程安全的。
Runloop比喻
进程是一家工厂,线程是一个流水线,Run Loop
就是流水线上的主管;当工厂接到商家的订单分配给这个流水线时,Run Loop
就启动这个流水线,让流水线动起来,生产产品;当产品生产完毕时,RunLoop
就会暂时停下流水线,节约资源。
RunLoop
管理流水线,流水线(线程)才不会因为无所事事被工厂(进程)销毁;而不需要流水线时,就会辞退RunLoop
这个主管,即退出线程,把所有资源释放。
RunLoop
并不是iOS
平台的专属概念,在任何平台的多线程编程中,为控制线程的生命周期,接收处理异步消息都需要类似RunLoop
的循环机制实现,Android
的Looper
就是类似的机制。
Runloop特性
- 主线程的
RunLoop
在应用启动的时候就会自动创建 - 其他线程则需要在该线程下自己启动
- 不能自己创建
RunLoop
-
RunLoop
并不是线程安全的,所以需要避免在其他线程上调用当前线程的RunLoop
-
RunLoop
负责管理autorelease pools
-
RunLoop
负责处理消息事件,即输入源事件和计时器事件
Runloop的目的
- 使程序一直运行接受用户输入
- 决定程序在何时应该处理哪些
Event
- 调用解耦(对于编程经验为0的完全没搞懂这个意思,解释为
Message Queue
) - 节省
CPU
时间
二、RunLoop 相关接口
在 CoreFoundation
里面关于 RunLoop
有5个类:
-
CFRunLoopRef
:RunLoop
对象 -
CFRunLoopModeRef
:运行模式 -
CFRunLoopSourceRef
:输入源/事件源 -
CFRunLoopTimerRef
:定时源 -
CFRunLoopObserverRef
:观察者
其中 CFRunLoopModeRef
类并没有对外暴露,只是通过 CFRunLoopRef
的接口进行了封装。他们的关系如下:
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source / Timer / Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode (_currentMode) ,如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
Mode
CFRunLoopMode
和 CFRunLoop
的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
五种 Mode 模式 :
kCFRunLoopDefaultMode
:默认模式,主线程是在这个运行模式下运行。
UITrackingRunLoopMode
:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)。
UIInitializationRunLoopMode
:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用。
GSEventReceiveRunLoopMode
:接受系统内部事件,通常用不到。
kCFRunLoopCommonModes
:伪模式,不是一种真正的运行模式,实际是 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 的结合 ==> NSRunLoopCommonModes。
NSRunLoopCommonModes
kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
,UITrackingRunLoopMode
是苹果公开的两个模式,可以通过mode name
来操作对应的 mode
。 同时苹果还提供了一个操作 Common
标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes)
,你可以用这个字符串来操作 Common Items
,或标记一个 Mode
为 “Common
”。使用时注意区分这个字符串和其他 mode name
。
kCFRunLoopCommonModes
对应的 _commonModes
:一个 Mode
可以将自己标记为"Common
"属性(通过将其 ModeName
添加到 RunLoop
的 “commonModes
” 中)。每当 RunLoop
的内容发生变化时,RunLoop
都会自动将 _commonModeItems
里的 Source
/ Observer
/ Timer
同步到具有 “Common
” 标记的所有Mode
里。
NSRunLoopCommonModes
实例解析:(广告图轮播或者TableView
滑动时Timer
):
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时默认所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时手动滑动一个轮播图时或者TableView,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
CFRunLoop对外暴露的管理 Mode 接口
CFRunLoop对外暴露的管理 Mode 接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 mode item
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopSourceRef
CFRunLoopSourceRef
是事件产生的地方。Source
有两个版本:Source0
和 Source1
。
Source0
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source)
,将这个Source
标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop
,让其处理这个事件。Source1
包含了一个mach_port
和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source
能主动唤醒RunLoop
的线程。事件是在
Sources0
中处理的。至于Source1
主要是用来接收、分发系统事件,然后再分发到Sources0
中处理。
CFRunLoopTimerRef
CFRunLoopTimerRef
是时间的触发器,它和 NSTimer
是 toll-free bridged
的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop
时,RunLoop
会注册对应的时间点,当时间点到时,RunLoop
会被唤醒以执行那个回调。
CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,每个 Observer
都包含了一个回调(函数指针),当 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
};
上面的 Source
/ Timer
/ Observer
被统称为 mode item
,一个 item
可以被同时加入多个 mode
。但一个 item
被重复加入同一个 mode
时是不会有效果的。如果一个 mode
中一个item
都没有,则 RunLoop
会直接退出,不进入循环。
可以通过以下代码验证RunLoop的几种状态:
// 创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd",activity);
});
// 添加观察者到当前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放observer
CFRelease(observer);
RunLoop执行顺序
1. Notify observers that the run loop has been entered.// 通知观察者 RunLoop 已经启动。
2. Notify observers that any ready timers are about to fire. // 通知观察者即将要开始定时器。
3. Notify observers that any input sources that are not port based are about to fire. // 通知观察者任何即将启动的非基于端口的源。
4. Fire any non-port-based input sources that are ready to fire. // 启动任何准备好的非基于端口的源(Source0)。
5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9. //如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
6. Notify observers that the thread is about to sleep. // 通知观察者线程进入休眠状态。
7. Put the thread to sleep until one of the following events occurs: //将线程置于休眠状态,知道下面的任一事件发生才唤醒线程。
* An event arrives for a port-based input source. // 某一事件到达基于端口的源
* A timer fires. // 定时器启动。
* The timeout value set for the run loop expires. // RunLoop 设置的时间已经超时。
* The run loop is explicitly woken up. // RunLoop 被唤醒。
8. Notify observers that the thread just woke up. // 通知观察者线程将被唤醒。
9. Process the pending event. // 处理未处理的事件。
* If a user-defined timer fired, process the timer event and restart the loop. Go to step 2. // 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
* If an input source fired, deliver the event. // 如果输入源启动,传递相应的消息。
* If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2. // 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
10. Notify observers that the run loop has exited. // 通知观察者RunLoop结束。
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
// 6. 通知Observers,即将进入休眠
// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
三、RunLoop的应用
Runloop与线程和自动释放池相关:
Runloop
的寄生于线程:一个线程只能有唯一对应的runloop
;但这个根runloop
里可以嵌套子runloops
;- 自动释放池寄生于
Runloop
:程序启动后,主线程注册了两个Observer
监听runloop
的pop、push与sleep
。一个最高优先级OB
监测Entry
状态;一个最低优先级OB
监听BeforeWaiting
状态和Exit
状态。 -
线程(创建)
-->runloop将进入
-->最高优先级OB创建释放池
-->runloop将睡
-->最低优先级OB销毁旧池创建新池
-->runloop将退出
-->最低优先级OB销毁新池
-->线程(销毁)
Runloop与线程
- 苹果不允许直接创建
RunLoop
,它只提供了两个自动获取的函数:CFRunLoopGetMain()
和CFRunLoopGetCurrent()
。 - 线程和
RunLoop
是一一对应的,其关系是保存在一个全局的Dictionary
里。 - 只能在当前线程中操作当前线程的
RunLoop
,而不能去操作其他线程的RunLoop
。 -
RunLoop
对象在第一次获取RunLoop
时创建,销毁则是在线程结束的时候。 - 主线程的
RunLoop
对象系统自动帮助我们创建好了,而子线程的RunLoop
对象需要我们主动获取,因为子线程刚创建时并没有RunLoop
,如果你不主动获取,那它一直都不会有。
Runtime相关的NSTimer
//NSTimer:
// 创建一个定时器(需要手动加到runloop的mode中)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// 默认已经添加到主线程的runLoop的DefaultMode中
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// performSEL方法
// 内部会创建一个Timer到当前线程的runloop中(如果当前线程没runloop则方法无效;performSelector:onThread: 方法放到指定线程runloop中)
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
相关类型(GCD的timer与CADisplayLink)
- GCD的timer:
dispatch_source_t
类型,可以精确的参数,不用以来runloop
和mode
,性能消耗更小。
dispatch_source_set_timer(dispatch_source_t source, // 定时器对象
dispatch_time_t start, // 定时器开始执行的时间
uint64_t interval, // 定时器的间隔时间
uint64_t leeway // 定时器的精度
);
- CADisplayLink :
Timer
的tolerance
表示最大延期时间,如果因为阻塞错过了这个时间精度,这个时间点的回调也会跳过去,不会延后执行。
CADisplayLink
是一个和屏幕刷新率一致的定时器,如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和NSTimer
相似,只是没有tolerance容忍时间),造成界面卡顿的感觉。
RunLoop处理事件
界面刷新:
UI改变
——>标记UI控件处于待处理状态
——>注册监听
——>遍历待处理对象进行处理
——>更新UI
。
当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。-
事件响应:
程序启动 ——> 用户点击屏幕 ——> 创建事件——>创建自动释放池 ——> Application响应事件 ——> 事件处理完毕销毁自动释放池。
需要特别注意的是:如果没有使用alloc new copy retain 方法而创建了对象,则内部全是使用了autorelease方法。所以使用自动释放池能对这些对象进行及时释放 手势识别:
如果上一步的_UIApplicationHandleEventQueue()
识别到是一个guesture
手势,会调用Cancel
方法将当前的touchesBegin/Move/End
系列回调打断。随后系统将对应的UIGestureRecognizer
标记为待处理。
苹果注册了一个Observer
监测BeforeWaiting
(Loop即将进入休眠) 事件,其回调函数为_UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的GestureRecognizer
,并执行GestureRecognizer
的回调。
当有UIGestureRecognizer
的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。GCD任务:
当调用dispatch_async(dispatch_get_main_queue(), block)
时,libDispatch
会向主线程的RunLoop
发送消息,RunLoop
会被唤醒,并从消息中取得这个block
,并在回调里执行这个block
。Runloop
只处理主线程的block
,dispatch
到其他线程仍然是由libDispatch
处理的。NStime(略)
-
网络请求
关于网络请求的接口:最底层是CFSocket
层,然后是CFNetwork
将其封装,然后是NSURLConnection
对CFNetwork
进行面向对象的封装,NSURLSession
是iOS7
中新增的接口,也用到NSURLConnection
的loader
线程。所以还是以NSURLConnection
为例。
当开始网络传输时,NSURLConnection
创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private
。其中CFSocket
线程是处理底层socket
连接的。NSURLConnectionLoader
这个线程内部会使用RunLoop
来接收底层socket
的事件,并通过之前添加的Source0
通知到上层的Delegate
。
滑动与图片刷新;
当tableview
的cell
上有需要从网络获取的图片的时候,滚动tableView
,异步线程会去加载图片,加载完成后主线程就会设置cell
的图片,但是会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode
下进行,当滚动tableView
的时候,RunLoop
是在 UITrackingRunLoopMode
下进行,不去设置图片,而是当停止的时候,再去设置图片。
-(void)viewDidLoad {
[super viewDidLoad];
// 只在NSDefaultRunLoopMode下执行(刷新图片)
[self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]];
}
常驻子线程,保持子线程一直处理事件。
为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runOne) object:nil];
[self.thread start];
}
- (void) runOne{
NSLog(@"----任务1-----");
// 下面两句代码可以实现线程保活
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(runTwo) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void) runTwo{
NSLog(@"----任务2------");
}
监测卡顿
所谓的卡顿一般是在主线程做了耗时操作,卡顿监测的主要原理是在主线程的 RunLoop
中添加一个 observer
,检测从 即将处理Source(kCFRunLoopBeforeSources)
到 即将进入休眠(kCFRunLoopBeforeWaiting)
花费的时间是否过长。如果花费的时间大于某一个阙值,则认为卡顿,此时可以输出对应的堆栈调用信息。App卡顿监测