runloop
runloop是用来处理事件的循环。NSRunloop是CFRunloop的封装,CFRunloop是一套C接口,源码地址。
runloop处理消息的流程是“接收消息->恢复活跃->处理消息->进入休眠”。
runloop作用
- 保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
- 处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
- 节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CPU,现在没有事情做,我要去休息,这时CPU就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情。
runloop的构成
- CFRunLoopRef //runloop对象
- CFRunLoopModeRef//运行模式
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
- CFRunLoopModeRef//运行模式
它的结构关系如下
struct __CFRunLoop {
pthread_t _pthread;//线程
CFMutableSetRef _commonModes; // commonModes下的两个mode(kCFRunloopDefaultMode和UITrackingMode)
CFMutableSetRef _commonModeItems; // 在commonModes状态下运行的对象(例如Timer)
CFMutableSetRef _modes; // 运行的所有模式(CFRunloopModeRef类)
CFRunLoopModeRef _currentMode;//在当前loop下运行的mode
...
};
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
CFRunLoopModeRef
一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响。下面是5种Mode。
kCFDefaultRunLoopMode App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
UIInitializationRunLoopMode 在刚启动App时第进入的第一个Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode 接受系统事件的内部Mode,通常用不到
kCFRunLoopCommonModes 这是一个占位用的Mode,不是一种真正的Mode
CommonModes
一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
sources0和_sources1
(1)以前的分法
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
(2)现在的分法:
Source0 : 触摸事件,PerformSelectors,非基于Port的
Source1 : 基于Port的线程间通信,基于Port的
_timers
定时执行的定时器,底层基于使用mk_timer实现,受RunLoop的Mode影响(GCD的定时器不受RunLoop的Mode影响),当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。如果线程阻塞或者不在这个Mode下,触发点将不会执行,一直等到下一个周期时间点触发。
timer和source1(也就是基于port的source)可以反复使用,比如timer设置为repeat,port可以持续接收消息,而source0在一次触发后就会被runloop移除。
_observers
添加监听的方法:
监听返回的状态:
enum CFRunLoopActivity {
kCFRunLoopEntry = (1 << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1 << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1 << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1 << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1 << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1 << 7), // 即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 包含上面所有状态
};
runloop流程
runloop与线程
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
runloop与GCD
- runLoop 的超时时间就是使用 GCD 中的 dispatch_source_t来实现的
- 执行GCD MainQueue 上的异步任务
runloop用到了GCD,当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
runloop与自动释放池
苹果在主线程 RunLoop 里注册了两个 Observer:
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop),
BeforeWaiting(准备进入睡眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
Runloop的作用
NSTimer
方式一
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
方式二
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
- (void)newThread{
@autoreleasepool{
//在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES];
//开始执行新线程的Run Loop,如果不启动run loop,timer的事件是不会响应的
[[NSRunLoop currentRunLoop] run];
}
}
自动释放池
PerformSelecter...
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。
这个过程的详细情况可以参考这里。
SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。
随后苹果注册的那个 Source1 就会触发回调,并调用_UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。
随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
UI更新
即准备进入睡眠和即将退出 loop 两个时间点,会调用函数更新 UI 界面.
当在操作 UI 时,某个需要变化的 UIView/CALayer 就被标记为待处理,然后被提交到一个全局的容器去,再在上面的回调执行时才会被取出来进行绘制和调整。
GCD
RunLoop 底层会用到 GCD 的东西,GCD 的某些 API 也用到了 RunLoop。如当调用了 dispatch_async(dispatch_get_main_queue(), block)时,主队列会把该 block 放到对应的线程(恰好是主线程)中,主线程的 RunLoop 会被唤醒,从消息中取得这个 block,回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 来执行这个 block
AFNetWorking 3.0以前的线程保活
子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的 RunLoop,保持 RunLoop 运行,则使子线程保持不死。AFNetworking 基于 NSURLConnection 时正是这样做的,希望在后台线程能保持活着,从而能接收到 delegate 的回调。
这一点充分体现了:我们控制了runloop ,就是控制了app 的生死。
/* 返回一个线程 */
+ (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;
}
/* 在新开的线程中执行的第一个方法 */
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
// 获取当前线程对应的 RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 为 RunLoop 添加 source,模式为 DefaultMode
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 开始运行 RunLoop
[runLoop run];
/ /或者
//[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:4]];
}
}
因为 RunLoop 启动前必须设置一个 mode,而 mode 要存在则至少需要一个 source / timer。所以上面的做法是为 RunLoop 的 DefaultMode 添加一个 NSMachPort 对象,虽然消息是可以通过 NSMachPort 对象发送到 loop 内,但这里添加的 port 只是为了 RunLoop 一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的 timer 来既保持 RunLoop 不退出也不占用资源
AsyncDisplayKit
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
监控系统卡顿
监控主线程状态,在一定时间内没有变化,就可判定为卡顿。这个会在之后的文章讲。
MachPort
MachPort的工作方式其实是将NSMachPort的对象添加到一个线程所对应的RunLoop中,并给NSMachPort对象设置相应的代理。在其他线程中调用该MachPort对象发消息时会在MachPort所关联的线程中执行相关的代理方法。
@interface DPMessageViewController ()
@property (nonatomic, strong) UIAlertAction * ac;
@end
@implementation DPMessageViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSPort *port = [NSMachPort port];
port.delegate = self;
[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
[NSThread detachNewThreadSelector:@selector(oooooo:) toTarget:[DPMessageViewModel new] withObject:port];
}
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"子线程的消息%@", message);
}
@end
@interface DPMessageViewModel : NSObject
{
NSPort *remotePort;
NSPort *myPort;
}
@end
@implementation DPMessageViewModel
- (void)oooooo:(NSMachPort *)port{
@autoreleasepool {
remotePort = port;
[[NSThread currentThread] setName:@"MyWorkerClassThread"];
[[NSRunLoop currentRunLoop] run];
myPort = [NSPort port];
myPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
[self sendPortMessage];
}
}
- (void)sendPortMessage{
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[@"1",@"2"]];
[remotePort sendBeforeDate:[NSDate date] msgid:100 components:array from:myPort reserved:0];
}
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"接收到父线程的消息...\n");
}
@end
总结
- 线程是独立调度和分派的基本单位,RunLoop和自动释放池为线程服务;
- RunLoop是一个事件循环,让线程休眠和线程保活成为了可能,线程休眠可以节省CPU资源;
- 自动释放池一定存在于线程之中,解决了资源的释放问题。
参考