iOS底层Runloop

作者: 崔希羽 | 来源:发表于2021-01-19 15:19 被阅读0次

    RunLoop介绍
    RunLoop是与线程相关的基本基础结构的一部分。RunLoop直译为运行循环,是线程内用于运行事件处理以响应传入事件的一个循环。RunLoop的作用就是为了在有事件到达时唤醒线程以处理各种事件(如touch事件,计时器,performSelector等),事件处理完让线程进入休眠以节省cpu资源。

    RunLoop与线程是一一对应的,主线程的Runloop默认是开启,子线程需要手动开启。为什么perfromSelector:withObject:afterDelay:在子线程为什么不好使?原因就是该方法会在当前线程的RunLoop的defaultMode下添加一个Timer,而子线程RunLoop没有开启,因此RunLoop会直接退出不会执行,可以在这个方法后面使用[[[NSRunLoop currentRunLoop] run];手动开启解决。


    RunLoop Modes
    RunLoop有多种Mode,Mode是Input sources、Timer sources和Observers的集合。RunLoop在同一时刻只能在一种Mode下运行,并且只有在该Mode下的事件才能被处理,只有该Mode下的观察者才会被发送通知。苹果定义的常见Mode有五种:

    • NSDefaultRunLoopMode:它是默认的,通常情况下都是在这个模式下运行
    • NSConnectionReplyMode:用来监控NSConnection对象的回复的,很少能够用到
    • NSModalPanelRunLoopMode:用于标明和Mode Panel相关的事件
    • NSEventTrackingRunLoopMode:用于跟踪触摸事件触发的模式
    • NSRunLoopCommonModes:这是一个可配置的模式,默认会包含default、modal、track三种,添加到这种模式下的事件在它包含的mode中都可以被处理,另外还可以使用CFRunLoopAddCommonMode函数添加自定义模式。

    RunLoop 输入源
    RunLoop从两种不同的源接收事件,分别是Input sourcesTimer sourcesInput sources传递异步事件,通常是来自其他线程或其他应用程序;Timer sources传递同步事件,以预定时间或重复间隔触发。这两种类型的源在事件到达时会使用特定的handler来处理事件。下图是苹果介绍Runloop与输入源的经典结构图

    Runloop与输入源结构图
    • Port-Based Sources:Cocoa中可以简单的创建一个NSPort对象添加到RunLoop,port对象会处理所需输入源的创建和配置;而在CoreFoundation中,必须手动创建端口及其运行循环源。
    • Custom Input Sources:自定义输入源必须要使用CoreFoundation中的CFRunLoopSourceRef相关联的函数
    • Cocoa Perform Selector Sources:使用NSObject提供的performSelector方法
    • Timer Sources:使用NSTimer或CFRunLoopTimer创建,Timer会有预定的时间触发,若为重复定时器,那么将会在指定的次数*时间间隔后执行,哪怕是被延迟。如果触发时间延迟太多以致错过了一个或多个预定的触发时间,则计时器在错过的时间段内只触发一次,之后将被重新安排为下一个计划的触发时间。

    RunLoop除了处理输入源之外,还可以为RunLoop的行为生成了一些通知,为这些通知添加观察者就可以在线程上做一些其他处理,注册观察者的api在Core Foundation框架中。如使用通过监听RunLoop的状态变化来监测主线程是否发生了卡顿。RunLoop Observer的几种状态:

    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),//启动
        kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timers
        kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources
        kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6),//休眠状态 等待被唤醒
        kCFRunLoopExit = (1UL << 7),//退出
        kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态集合
    };
    
    RunLoop结构图

    RunLoop原理

    查看RunLoop的run底层源码,当CFRunLoopRunResult不等于stopped和finished时会执行do...while循环,调用CFRunLoopRunSpecific()函数

    typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
        kCFRunLoopRunFinished = 1,
        kCFRunLoopRunStopped = 2,
        kCFRunLoopRunTimedOut = 3,
        kCFRunLoopRunHandledSource = 4
    };
    
    void CFRunLoopRun(void) {    /* DOES CALLOUT */
        int32_t result;
        do {
            result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
            CHECK_FOR_FORK();
        } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
    }
    

    可以看出主要逻辑就在CFRunLoopRunSpecific()函数中,其内部主要逻辑为

    • 首先根据modeName找到对应的mode
    • 通知Observers,RunLoop即将进入kCFRunLoopEntry
    • 进入循环,调用核心函数__CFRunLoopRun()
    • 通知Observers,RunLoop即将退出kCFRunLoopExit

    核心函数__CFRunLoopRun()内部循环处理流程

    • 0 启动一个10^10的计时器,并开启循环
    • 1 通知Observers:将要处理Timers事件
    • 2 通知Observers:将要处理Source0事件
    • 3 执行所有准备就绪的Source0
    • 4 如果有Source1准备就绪,立即开始处理Source1
    • 5 通知Observers:线程即将进入休眠
    • 6 休眠,等待唤醒
    • 7 休眠中被唤醒kCFRunLoopAfterWaiting,若为Timers则执行1,若为source1则执行4,若为GCD则执行GCD回调函数,若被其他调用者手动唤醒,则继续从1开始循环
    • 8 事件被处理或超时等改变了RunLoop状态则跳出循环,返回已处理/超时/停止/结束状态。

    通过源码可以看到RunLoop处理事件的回调函数有六种,也可以通过debug断点回调,然后查看汇编代码进行佐证,分别为:

    • GCD主队列:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
    • observer源:CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
    • 调用timer:CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
    • block应用: CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
    • source0:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
    • source1:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION

    RunLoop相关面试题

    1. 如何创建一个常驻线程?
      NSRunLoop无法通过alloc创建,直接在子线程中使用CFRunLoopGetCurrent()或[NSRunLoop currentRunLoop]在第一次时即会进行创建,随后添加一个Port/Source到Runloop,最后调用run启动。如果Runloop的mode中一个item都没有,那么runloop会直接退出。

    2.performSelector:withObject:afterDelay:这个方法在子线程中调用有什么问题?
    会不起作用,因为这个方法的实现是在当前的runloop的defaultmode下添加一个timer,因为runloop在子线程默认是不开启的,因此不会执行,可在这此方法之后添加调用[[NSRunLoop currentRunLoop] run];或者使用GCD来实现.

    1. 利用runloop解释界面的渲染过程?
      在调用[UIView setNeedDisplay],或者直接调用调用[CAlayer setNeedsDisplay]时,相当于给当前的layer打上了一个脏标记,标识为需要重绘,但此时并没有直接进行绘制。而是会在当前的Runloop即将进行休眠时,即CFRunLoopBeforeWaiting状态时才开始绘制。
      • [CAlayer setNeedsDisplay]会调用[CALayer display]
      • 若layer没有delegate,则会[CALayer drawInContext:]
      • 有delegate判断是否实现了- (void)displayLayer:(CALayer *)layer异步绘制代理方法
      • 若没有实现异步绘制的代理方法,则会继续进行系统绘制的流程,CALayer内部会创建一个Backing Store用于获取图形上下文
      • 调用delegate方法- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx,并返回- (void)drawRect:(CGRect)rect的回调
      • 最终 CALayer 都会将位图提交到 Backing Store ,最后提交给 GPU,绘制结束

    4.解释一下事件响应的过程?
    苹果注册了一个基于mach port的source1用来接收系统事件,当一个硬件事件发生(触摸/摇晃/锁屏等),首先会由系统的IOKit.framework封装成一个IOHIDEvent,并且由内核接受,内核接收到事件之后会通过mach port转发给App进程,随后source1即会被触发,然后在回调的内部会把IOHIDEvent处理并包装成UIEvent事件,然后发送给UIWindow进行响应处理。

    5.autoreleasePool是在何时进行释放?
    App启动后,苹果在主线程的Runloop注册了两个observer,第一个observer监视的RunLoop的Entry事件,在这个事件的回调内会调用autoreleasePoolPush()创建自动释放池,它的优先级最高,保证创建的动作在其他所有回调之前;第二个observer监视的RunLoop的BeforWaiting和Exit两个事件,在BeforWaiting时调用autoreleasePoolPop()释放旧池和autoreleasePoolPush()创建新池,在Exit时调用autoreleasePoolPop()释放旧池,这个observer的优先级最低,保证释放发生在其他回调之后。

    相关文章

      网友评论

        本文标题:iOS底层Runloop

        本文链接:https://www.haomeiwen.com/subject/ipmjaktx.html