RunLoop原理

532 阅读7分钟

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…