前言
接上篇 核心机制 ,本文主要介绍RunLoop在应用中的实践。
iOS/OS X系统中很多基础功能,比如自动释放池就是由RunLoop实现或者协助实现的,所以RunLoop是iOS系统中基础中的基础,组件中的组件。
- 由RunLoop直接管理的机制有:自动释放池、定时器、视图刷新等机制。这些机制的生命周期完全由RunLoop管理。
- 需要RunLoop支持的机制有:事件响应、动画、异步方法调用、网络请求等机制。这些机制借助了RunLoop的特性完成自己的功能。
管理自动释放池
自动释放池,AutoreleasePool有两种管理方式,一种方式是由程序员负责管理,通过AutoreleasePool块将块中的临时变量在出块的时候释放掉,主要在循环读取大文件中会用到。
另一种方式就是由系统管理AutoreleasePool的创建和销毁,实质上这个系统管理就是Runloop管理的。
App启动后,主线程RunLoop中会注册Observer,分别在RunLoopEntry、BeforeWaiting和Exit时调用。回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
RunLoopEntry Observer 在RunLoop进入的时候会被触发,在_wrapRunLoopWithAutoreleasePoolHandler()函数中调用_objc_autoreleasePoolPush()创建自动释放池。order是- 2^31,优先级最高,确保在所有回调之前创建自动释放池。
RunLoopBeforWaiting Observer 在RunLoop进入休眠之前被触发,同样在_wrapRunLoopWithAutoreleasePoolHandler()函数中处理,但是调用的方法不同,分别调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()方法。释放旧的自动释放池,同时创建新的自动释放池。
RunLoopExit Observer 在RunLoop退出的时候被触发。调用_objc_autoreleasePoolPop()释放自动释放池。
由于RunLoop管理AutoreleasePool,所以在线程中执行代码,无论是事件回调,还是Timer回调都回被AutoreleasePool环绕,所以不会有内存泄露的问题。
管理定时器
timer是RunLoop的事件源之一,timer添加到RunLoop之后,RunLoop会在timer的时间点上注册定时事件。因为各种原因,RunLoop执行回调的时间点并不准确,可能在执行一个长任务,可能在其他mode下。Timer有个属性叫Tolerance,宽容度,这个属性标示了当timer被触发的时候同标定的时间点允许有多大的误差度。
如果超过宽容度在这个时间点timer的回调函数不会被执行。同样的如果某个时间点被错过了,则这个时间点也会被跳过,回调函数不会被触发执行。也就出现了,1秒执行一次的timer,理论上1分钟应该执行60次,但是出现了执行57、58次的情况。
NSTimer同CFRunLoopTimerRef 是 toll-free bridged的。底层由XNU 内核的mk_timer 驱动。
管理视图刷新
当视图内容更新的时候,调用layoutSubView方法进行重新布局,调用drawRect方法进行重绘。我们都知道在开发的时候不能直接调用layoutSubviews或者drawRect方法,而是调用setNeedsLayout方法触发重新布局,setNeedsDisplay方法触发重新绘制。这样做的目的是为了效率和流畅度,众所周知界面的重新布局和重新绘制都是非常耗时的操作,如果在短时间内频繁进行这个操作,CPU就没办法进行其他操作,影响app整体的运行效率和流畅度。所以将需要重排、重绘的View和Layer进行标记,在一次RunLoop循环中只进行一次重排、重绘操作。
这个视图刷新的机制就需要RunLoop去支持。RunLoop Observer会在即将进入休眠 BeforeWaiting 和 退出 Exit 的时候调用CFRunLoopObservermPv()函数,这个函数会遍历所有有标记的View和Layer,执行真正的重新布局和重新绘制方法。达到刷新视图界面的目的。
CFRunLoopObservermPv()
QuartzCore::observer_callback:
CA::commit();
CA::commit_transaction();
layout_and_display_if_needed();
layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
display_if_needed();
[CALayer display];
[UIView drawRect];
支持事件响应
在RunLoop中事件源分为Source0和Source1。Source1事件是可以主动唤醒RunLoop的,Source1除了回调函数外还有一个mack_port端口,通过这个端口来接收系统事件,回调函数是_IOHIDEventSystemClientQueueCallback()。
当硬件事件如触屏、摇晃、翻转、锁屏,系统会由IOKit产生一个用户设备(human interface devices)事件。事件类型:IOHIDEvent。由SpringBoard组件负责接收。
SpringBoard 只接收按键,触屏,加速,接近传感器等4种 事件。之后通过mach_port发送给注册的应用进程,应用进程通过Source1事件源响应这个事件,并通过_UIApplicationHandleEventQueue()进行分发。
_UIApplicationHandleEventQueue方法会将IOHIDEvent对象封装成UIEvent对象再进行分发,手势、屏幕旋转交给Window处理,点击事件交给响应者链处理。touchBegin、touchMove、touchEnd都是在这个方法中调用的。
手势事件同touch事件是互斥的,如果UIEvent被识别成一个手势,则不会当成touch事件来处理。系统会调用Cancel将touchBegin、touchMove中断。
当_UIApplicationHandleEventQueue()识别一个手势时,会将对应的手势标记为待处理。当RunLoop 通过Observer 准备进入到休眠状态时,Observer的回调函数会处理所有标记为待处理的手势,并执行手势的回调方法。
支持动画渲染
Core Animation
Core Animation 在呈现的过程中有三个tree。
- model tree
- presentation tree
- render tree
model tree是我们可以直接操作的tree,当修改CALayer的时候,CALayer的属性值会修改model tree。
presentation tree 是layer在屏幕中的真实位置也是一个CALayer对象。可以通过view.layer.presentationLayer 获得。presentation是只读的
render tree 是私有的,应用开发无法访问到。render tree在专用的render server 进程中执行,是真正用来渲染动画的地方,线程优先级高于主线程。所以即使app主线程阻塞,也不会影响到动画的绘制工作。无论隐式还是显式动画都是在当前线程的RunLoop结束后提交到render tree。因为 commit transaction 操作是从app进程到render server 进程是IPC,会有进程间通讯开销,所以官方不推荐我们手动 commit transaction。
CADisplayLink
CADisplayLink 的selector是在屏幕内容刷新完成的时候调用。实质上是向RunLoop注册了一个Source0事件。CADisplayLink一般被用来执行自定义动画和播放视频,相比于CoreAnimation的方式,CADisplayLink会导致部分绘制工作放在了App的进程中进行,增大了CPU和内存的开销,更容易引发性能问题。
CADisplayLink可以用来播放视频,使用AVPlayerItemVideoOutput 提供一个样板缓冲区(sample buffers),输出到CAEAGLLayer 上。CAEAGLLayer 是 Core Animation Embedded Apple Graphics Library 的缩写。OpenGL ES渲染出来的图层在iOS中必须使用 CAEAGLLayer 。通过CADisplayLink 从缓冲区拿纹理内容,呈现在屏幕上。
官方代码
支持异步方法调用
performSelector
上图引自苹果的官方文档,除了输入源和定时源之外,RunLoop还是performSelector的基础设施。
我们使用 performSelector:onThread: 或者 performSelecter:afterDelay: 时,实际上系统会创建一个Timer并添加到当前线程的RunLoop中。所以如果当前线程没有RunLoop,performSelector 方法就会失效。
GCD
dispatch_async() 方法,当第一个参数是主线程队列的时候,libDispatch 会向主线程RunLoop发送mack_msg 消息。如果RunLoop在休眠态,会被唤醒,从消息中取得dispatch_async() 第二个参数 block 并执行。
为了确保GCD的有效性, dispatch_async() 到其他线程是由libDispatch处理,并不涉及到RunLoop。
支持网络请求
在iOS中进行网络通讯功能的开发一般都是基于NSURLConnection。NSURLConnection的底层是CFNetwork,CFNetwork是基于CFSocket的。NSURLConnection是基于socket的面向对象的网络库。
在iOS7之后苹果提供了NSURLSession,相比NSURLConnection提供了更丰富的功能,如身份验证、后台下载等。底层都是基于CFNetwork和CFSocket。
NSURLConnection的start()方法中,会获取当前的RunLoop,getCurrentRunLoop,然后在其中的defaultMode中添加Source0事件用于接收网络回调。
NSURLConnection会创建两个新线程:
- com.apple.CFSocket.private 线程,负责处理socket连接
- com.apple.NSURLConnectionLoader 线程, 用于接受底层socket 的 Source1 事件,通过 Source0 事件通知到NSURLConnection的start所在的RunLoop中。
参与整个网络通讯的有3个线程,2个RunLoop。
线程 | RunLoop | 作用 |
---|---|---|
com.apple.CFSocket.private | 无 RunLoop | 处理socket连接 |
com.apple.NSURLConnectionLoader | 有RunLoop | 1、接收CFSocket的Source1通知。2、向应用线程的RunLoop的Source0发送通知 |
应用线程 | 有RunLoop | 通过Source0接收NSURLConnectionLoader 发送的通知,并回调delegate |
Run Loop应用实践
Run Loop主要有以下三个应用场景:
- 维护线程的生命周期,让线程不自动退出
- 创建常驻线程,执行一些会一直存在的任务。该线程的生命周期跟App相同
- 在一定时间内监听某种事件,或执行某种任务的线程
维护线程的生命周期
isFinished为Yes时退出。
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
@autoreleasepool {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
}
创建常驻线程
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
在一定时间内监听某种事件
- 如下代码,在30分钟内,每隔30s执行onTimerFired:。这种场景一般会出现在,如我需要在应用启动之后,在一定时间内持续更新某项数据。
@autoreleasepool {
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
target:self
selector:@selector(onTimerFired:)
userInfo:nil
repeats:YES];
[runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}
- AFNetworking中RunLoop的创建
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 这里主要是监听某个 port,目的是让RunLoop不会退出,确保该 Thread 不会被回收
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread =
[[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
RunLoop 开发注意
//错误做法
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
while (!self.isCancelled && !self.isFinished) {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
};
//正确做法
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
@autoreleasepool {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
}
参考文章
http://iphonedevwiki.net/index.php/IOHIDFamily
https://en.wikipedia.org/wiki/SpringBoard
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
https://developer.apple.com/reference/foundation/urlsession
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html