RunLoop
程序会进入do...while循环,处理事件的循环。在没有消息处理时,会进入休眠表面资源占用。
RunLoop处理消息的流程是“接收消息->恢复活跃->处理消息->进入休眠”。
RunLoop指的是NSRunloop或者CFRunLoopRef,CFRunLoopRef是纯C的函数,而NSRunloop仅仅是CFRunloopRef的OC封装,并未提供额外的其他功能。
作用
- 保持程序持续运行。程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
- 处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
- 节省CPU资源,提高程序性能。程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情。
Runloop的构成
CFRunLoopModeRef:运行模式
- CFRunLoopRef //runloop对象
- CFRunLoopModeRef//运行模式
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
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,实际是kCFRunLoopDefaultMode 和 UITrackingRunLoopMode的结合。
sources0和_sources1
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 流程

Ruloop 的应用
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与自动释放池(@autoreleasepool)
苹果在主线程 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
常用的NSTimer,定时器类;基机制也是基于RunLoop运行的,只是在指定的间隔时间发送消息给需要处理的回调方法。
需要注意的是,如果RunLoop没有监视定时器相关模式,那么定时器将不会运行。
如果定时器开始时,RunLoop正在处理前面的事件,那么它会等RunLoop处理完了才开始。如果Run Loop不再运行,那么定时器也永远不再启动了。
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 就被标记为待处理,然后被提交到一个全局的容器去,再在上面的回调执行时才会被取出来进行绘制和调整。
监控系统卡顿
监控主线程状态,在一定时间内没有变化,就可判定为卡顿。 卡顿监测的主要原理是在主线程的RunLoop 中添加一个 observer,检测从 即将处理Source(kCFRunLoopBeforeSources) 到 即将进入休眠 (kCFRunLoopBeforeWaiting) 花费的时间是否过长。如果花费的时间大于某一个阙值,则认为卡顿,此时可以输出对应的堆栈调用信息。
MachPort
MachPort的工作方式其实是将NSMachPort的对象添加到一个线程所对应的RunLoop中,并给NSMachPort对象设置相应的代理。在其他线程中调用该MachPort对象发消息时会在MachPort所关联的线程中执行相关的代理方法。
@interface DPMessageViewController ()<NSMachPortDelegate>
@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<NSMachPortDelegate>
{
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
实例应用
参考资料
www.jianshu.com/p/fcb271f69…
www.jianshu.com/p/ae0118f96…
blog.ibireme.com/2015/05/18/…
www.jianshu.com/p/e9b4fafcb…
