Runloop学习

什么是Runloop:

RunLoop是一个接收处理异步消息事件的循环,一个循环中:等待事件发生,然后将这个事件送到能处理它的地方。

  • 从字面看是跑圈的意思,就是保证线程循环执行,不会立即结束。基本的作用就是保持程序的持续运行,处理app中的各种事件。通过runloop,有事运行,没事就休息,可以节省cpu资源,提高程序性能。

  • 一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop得手动启动(调用run方法)。

  • RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。

什么时候需要Runloop:

  • 使用端口或自定义输入源来和其他线程通信
  • 使用线程的定时器
  • Cocoa中使用任何performSelector…的方法
  • 使线程周期性工作

Runloop对象:

OC中有两套RunLoop对象:

  • Foundation:NSRunLoop;
  • Core Foundation:CFRunLoopRef;

NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API。

Runloop与线程:

  • 每条线程都有唯一的一个与之对应的RunLoop对象;
  • OC中不允许外部创建RunLoop对象,只能通过第一次获取来由内部创建。主线程的RunLoop在启动时会自动创建好了,子线程的RunLoop需要主动创建;

获取RunLoop对象的方法:

  • Foundation:

    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
    
  • Core Foundation:

    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopGetMain(); // 获得主线程的RunLoop对象
    

RunLoop相关类:

  1. CFRunLoopModeRef:
    CFRunLoopModeRef代表RunLoop的运行模式。一个RunLoop包含若干个Mode,每个Mode又包含若干个(set)Source/(array)Timer/(array)Observer,每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

  2. CFRunLoopTimerRef:

  • CFRunLoopTimerRef是基于时间的触发器;
  • CFRunLoopTimerRef基本上说的就是NSTimer,它受RunLoop的Mode影响;
  • GCD的定时器不受RunLoop的Mode影响;
  1. CFRunLoopSourceRef:

  2. CFRunLoopObserverRef:
    CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变。可以监听的时间点有以下几个:

  • kcfRunLoopEntry(即将进入loop)//1
  • kcfRunLoopBeforeTimers(即将处理timer)//2
  • kcfRunLoopBeforeSources(即将处理source)//4
  • kcfRunLoopBeforeWaiting(即将进入休眠)//32
  • kcfRunLoopAfterWaiting(刚从休眠中唤醒)//64
  • kcfRunLoopExit(即将退出loop)//128

RunLoop处理逻辑:

  1. 通知Observer:即将进入Loop;

  2. 通知Observer:将要处理Timer;

  3. 通知Observer:将要处理Source0;

  4. 处理Source0;

  5. 如果有Source1,跳到第9步;

  6. 通知Observer:线程即将休眠;

  7. 休眠,等待唤醒:

    • Source0(port)
    • timer启动
    • RunLoop设置的timer已经超时
    • Runloop被外部手动唤醒
  8. 通知Observer:线程刚被唤醒;

  9. 处理唤醒时收到的消息:

    • 如果用户定义的定时器启动,处理定时器事件并重启Runloop。进入步骤2;
    • 如果输入源启动,传递相应的消息。
    • 如果RunLopp被显式唤醒而且时间还没超时,重启RunLoop,进入步骤2;
  10. 通知Observer:即将退出Loop;

图片.png

Runloop如何工作

图片.png

代码可以大致表述如下:

 //程序一直运行状态
 while (AppIsRunning) {
      //睡眠状态,等待唤醒事件
      id whoWakesMe = SleepForWakingU  p();
      //得到唤醒事件
      id event = GetEvent(whoWakesMe);
      //开始处理事件
      HandleEvent(event);
 }

RunLoop主要处理以下6类事件:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
  • Observer事件,runloop中状态变化时进行通知。(微信卡顿监控就是利用这个事件通知来记录下最近一次main runloop活动时间,在另一个check线程中用定时器检测当前时间距离最后一次活动时间过久来判断在主线程中的处理逻辑耗时和卡主线程)。

  • Block事件,非延迟的NSObject PerformSelector立即调用,dispatch_after立即调用,block回调。

  • Main_Dispatch_Queue事件:GCD中dispatch到main queue的block会被dispatch到main loop执行。

  • Timer事件:延迟的NSObject PerformSelector,延迟的dispatch_after,timer事件。

  • Source0事件:处理如UIEvent,CFSocket这类事件。需要手动触发。触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。

  • Source1事件:处理系统内核的mach_msg事件。(推测CADisplayLink也是这里触发)。

RunLoop执行顺序的伪代码:

SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
//通知即将进入runloop__CFRUNLLOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(KCFRunLoopEntry);
do {
    __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);

    __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

    __CFRunLoopDoBlocks();  //一个循环中会调用两次,确保非延迟的NSObject PerformSelector调用
                    和非延迟的dispatch_after调用在当前runloop执行,还有回调block。

    __CFRunLoopDoSource0(); //例如UIKit处理的UIEvent事件

    CheckIfExistMessagesInMainDispatchQueue(); //GCD dispatch main queue

    __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); //即将进入休眠,会重绘一次界面
 
    var wakeUpPort = SleepAndWaitForWakingUpPorts();
 
    // mach_msg_trap,陷入内核等待匹配的内核mach_msg事件
    // Zzz...

    // Received mach_msg, wake up
    __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
    
    // Handle msgs
    if (wakeUpPort == timerPort) {
         __CFRunLoopDoTimers();
    } else if (wakeUpPort == mainDispatchQueuePort) {
      //GCD当调用dispatch_async(dispatch_get_main_queue(),block)时,
            libDispatch会向主线程的runloop发送mach_msg消息唤醒runloop,并在这里执行。
             这里仅限于执行dispatch到主线程的任务,dispatch到其他线程的仍然是libDispatch来处理。
      
         __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
    } else {
         __CFRunLoopDoSource1();  //CADisplayLink是source1的mach_msg触发?
    }
    __CFRunLoopDoBlocks();
} while (!stop && !timeout);

//通知observers,即将退出runloop
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBERVER_CALLBACK_FUNCTION__(CFRunLoopExit);

iOS 渲染过程

图片.png

通常来说,计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号如下图,逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

图片.png

在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。从上图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。

屏幕渲染和离屏渲染

OpenGL中,GPU屏幕渲染有以下两种方式:

  1. On-Screen Rendering 意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。

  2. Off-Screen Rendering 意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

如果将不在GPU的当前屏幕缓冲区中进行的渲染都称为离屏渲染,那么就还有另一种特殊的“离屏渲染”方式:CPU渲染。如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。

备注:CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程,一个简单的异步绘制过程大致如下:

- (void)display { 
    dispatch_async(backgroundQueue, ^{ 
        CGContextRef ctx = CGBitmapContextCreate(...); 
        // draw in context... 
        CGImageRef img = CGBitmapContextCreateImage(ctx); 
        CFRelease(ctx); 
        dispatch_async(mainQueue, ^{ 
            layer.contents = img;
        });
     });
}

相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:

  1. 创建新缓冲区:要想进行离屏渲染,首先要创建一个新的缓冲区。
  2. 上下文切换:离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。

iOS Core Animation 分析

Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 标记,并通过 CATransaction 提交到一个中间状态去。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 Core Animation 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,通过 DisplayLink 稳定的刷新机制会不断的唤醒runloop,使得不断的有机会触发observer回调,从而根据时间来不断更新这个动画的属性值并绘制出来。

为了不阻塞主线程,Core Animation 的核心是 OpenGL ES 的一个抽象物,所以大部分的渲染是直接提交给GPU来处理。 而Core Graphics/Quartz 2D的大部分绘制操作都是在主线程和CPU上同步完成的,比如自定义UIView的drawRect里用CGContext来画图。

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
    CA::Transaction::commit();
        CA::Context::commit_transaction();
            CA::Layer::layout_and_display_if_needed();
                CA::Layer::layout_if_needed();
                      [CALayer layoutSublayers];
                      [UIView layoutSubviews];
                CA::Layer::display_if_needed();
                      [CALayer display];
                      [UIView drawRect];

iOS动画坑

iOS的视图在动画过程默认是不响应事件的,所以,如果想在移动的buttton时相应点击事件,需要特殊处理。

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(click:)];  
[self addGestureRecognizer:tap]; 

- (void)click:(UITapGestureRecognizer *)tap {  
    CGPoint touchPoint = [tap locationInView:self.view];
    if ([self.weakBtn.layer.presentationLayer hitTest:touchPoint])
    {
        //处理事件
    }
} 

最重要的是别忘记设置动画的options为 UIViewAnimationOptionAllowUserInteraction

//第一个动画可以相应点击事件
[UIView animateWithDuration:5 delay:0 options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction animations:^{
    btn.frame = CGRectMake(10, 200, 100, 30);
} completion:^(BOOL finished) {

    //第二个动画不能相应点击事件
    [UIView animateWithDuration:5 animations:^{
        btn.frame = CGRectMake(100, 200, 100, 30);
    }];
}];

autorelease 对象在什么情况下会被释放:

分两种情况:手动干预释放和系统自动释放。

  • 手动干预释放,就是指定autoreleasepool,当前作用域大括号结束就立即释放;
  • 系统自动释放,不手动指定autoreleasepool,Autorelease对象会在当前的 runloop 迭代结束时释放;

kCFRunLoopEntry(1):第一次进入会自动创建一个autorelease;
kCFRunLoopBeforeWaiting(32):进入休眠状态前会自动销毁一个autorelease,然后重新创建一个新的autorelease;
kCFRunLoopExit(128):退出runloop时会自动销毁最后一个创建的autorelease;

参考文章:

iOS事件处理机制与图像渲染过程
iOS离屏渲染优化

你可能感兴趣的:(Runloop学习)