美文网首页
iOS RunLoop由浅入深

iOS RunLoop由浅入深

作者: FengyunSky | 来源:发表于2020-04-20 23:12 被阅读0次

    Event Loop

    Event Loop事件循环机制,如javascript的事件循环,以及依赖其的nodejs都是采用的异步事件循环机制。

    对于上述两者,都是基于多线程,但是都是单线程执行任务代码,其依赖的就是Event Loop事件循环机制,通过事件队列注册事件及事件的观察者,事件的执行交由其他线程去执行(如I/O操作,网络请求等),nodejs采用的是libuv异步I/O线程池库;对于非异步I/O操作,如setTimeOut setInterval等,都是基于事件循环查询(每次事件处理完成后进入下一次事件循环时都会查看时间是否已到达,并且是任务是插入到任务队列尾部,因此存在误差,不过也可采用process.netxTick会将事件插入到事件循环前解析执行,且可嵌套执行);

    image.png
    image.png
    • 定时器:本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
    • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
    • idle, prepare:仅系统内部使用。
    • 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
    • 检测setImmediate() 回调函数在这里执行。
    • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)

    JavaScript单线程异步的背后——事件循环机制

    《深入浅出Nodejs》

    Nodejs Event Loop

    JavaScript 运行机制详解:再谈Event Loop

    Mach Port

    这个在《unix进程间通信》中已阐述,在这不过多阐述;

    RunLoop

    RunLoop就像其名字一样,就是运行循环,核心就是事件循环+mach port,利用事件循环注册观察相应的事件,若无事件处理,线程就去睡眠等待内核事件触发或者通过手动唤醒,不停地循环处理各种事件(如timer source0 source1事件以及dispatch分发的func block等);

    注:以下代码分析基于CF-1151.16源码;

    runloop与线程的关系

    直接上结论:runloop与线程是一一对应的,且对于主线程是默认开启的,对于其他线程,需要通过手动开启,且只能通过苹果对外的接口获取线程相应的CFRunLoopRef对象:

    CFRunLoopRef CFRunLoopGetMain(void);
    CFRunLoopRef CFRunLoopGetCurrent(void);
    

    原理就是:苹果维护了一个全局的字典对象,若字典中不存在线程对应的runloop对象就会创建并赋值,并且还利用线程私有数据(数组)存储了指定__CFTSDKeyRunLoop当前线程的CFRunLoopRef对象(同时也关联了runloop对象销毁的回调,用于线程退出销毁);策略是:优先从线程私有数据数组中获取,若获取不到就从全局字典对象中获取,若无则去创建;

    runloop对象结构分析

    CoreFoundation中关于runloop的五个类:

    CFRunLoopRef
    CFRunLoopModeRef
    CFRunLoopSourceRef
    CFRunLoopTimerRef
    CFRunLoopObserverRef
    

    其中CFRunLoopRef对象结构体如下:

    //CFRunLoop结构体结构
    struct __CFRunLoop
    {
        CFRuntimeBase _base;
        pthread_mutex_t _lock; /* locked for accessing mode list */
        __CFPort _wakeUpPort;  // used for CFRunLoopWakeUp
        Boolean _unused;
        volatile _per_run_data *_perRunData; // reset for runs of the run loop
        pthread_t _pthread;//绑定的线程pthread_t
        uint32_t _winthread;
        CFMutableSetRef _commonModes;//通用的mode set集合
        CFMutableSetRef _commonModeItems;//通用mode的itme集合
        CFRunLoopModeRef _currentMode;//当前mode
        CFMutableSetRef _modes;//所有的mode
        struct _block_item *_blocks_head;//添加的block任务,与dispatch分发的block处理不同
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    

    runloop对象的结构体为__CFRunLoop,其存储着runloop相关的锁保证线程安全(注意下NSRunLoop不是线程安全的),唤醒端口(用于CFRunLoopWakeUp外部接口调用,主要是source0),绑定的线程,mode各种集合(下面会重点阐述),block处理任务(通过CFRunLoopPerformBlock接口注册的)以及记录需要的相关信息(如运行时间_runTime _sleepTime)等;

    CFRunLoopModeRef

    image.png
    CFRunLoopRef对象中包含了若干ModeMode对象的数据结构如下:
    struct __CFRunLoopMode
    {
        CFRuntimeBase _base;
        pthread_mutex_t _lock; /* must have the run loop locked before locking this */
        CFStringRef _name;//mode名称
        Boolean _stopped;
        char _padding[3];
        CFMutableSetRef _sources0;//source0对象
        CFMutableSetRef _sources1;//source1对象
        CFMutableArrayRef _observers;//observer对象
        CFMutableArrayRef _timers;//定时器
        CFMutableDictionaryRef _portToV1SourceMap;
        __CFPortSet _portSet;//需要监听的所有mach port集合
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        //mk_timer由mach port实现<https://opensource.apple.com/source/xnu/xnu-3789.51.2/osfmk/kern/mk_timer.c>
        mach_port_t _timerPort;
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR */
        uint64_t _timerHardDeadline; /* TSR */
    };
    

    CFRunLoopModeRef对象未对外暴露,可通过CFRunLoopCopyAllModesCFRunLoopCopyCurrentMode获取所有runloop相关的Mode,通过CFRunLoopAddCommonMode添加Mode

    runloop可添加多个Mode,但只能指定一个Mode模式运行(默认是kCFRunLoopDefaultMode),且需要退出当前重新指定运行才能生效;每个Mode中可添加若干sourcetimerobserver

    对于CoreFoundation中的CFRunLoop苹果只提供了两种默认模式kCFRunLoopDefaultModekCFRunLoopCommonModes其中kCFRunLoopCommonModes只是操作common标记的字符串,用于向所有现有的Modes中添加相应的观察者,不是一种具体的Mode,不能直接用于CFRunLoopRunInMode调用运行;但上层NSRunLoopMode封装了一些相应的Mode,如

    • NSDefaultRunLoopModekCFRunLoopDefaultMode默认的模式
    • NSEventTrackingRunLoopMode模态跟踪事件时,例如鼠标拖动循环,应将运行循环设置为此模式;
    • NSModalPanelRunLoopMode运行等待模态窗口(如NSSavePanel NSOpenPanel)的输入时指定;
    • UITrackingRunLoopMode运行控件追踪时指定,如UIScrollView滑动时(这个系统会默认自动切换到此模式)

    iOS应用启动时系统默认注册了5个Mode:

    • kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode下运行的;
    • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响;
    • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用;
    • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到;
    • kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用;

    CFRunLoopSourceRef

    CFRunLoopSourceRef对象的数据结构如下:

    struct __CFRunLoopSource
    {
        CFRuntimeBase _base;
        uint32_t _bits;
        pthread_mutex_t _lock;
        CFIndex _order; /* immutable */
        CFMutableBagRef _runLoops;
        union {
            CFRunLoopSourceContext version0;  /* immutable, except invalidation */
            CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
        } _context;
    };
    
    typedef struct {
        CFIndex version;
        void *  info;
        const void *(*retain)(const void *info);
        void    (*release)(const void *info);
        CFStringRef (*copyDescription)(const void *info);
        Boolean (*equal)(const void *info1, const void *info2);
        CFHashCode  (*hash)(const void *info);
        void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
        void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
        void    (*perform)(void *info);
    } CFRunLoopSourceContext;
    
    typedef struct {
        CFIndex version;
        void *  info;
        const void *(*retain)(const void *info);
        void    (*release)(const void *info);
        CFStringRef (*copyDescription)(const void *info);
        Boolean (*equal)(const void *info1, const void *info2);
        CFHashCode  (*hash)(const void *info);
        mach_port_t (*getPort)(void *info);
        void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
    } CFRunLoopSourceContext1;
    

    其中联合体union中的versionx.version字段信息用于区分source0或者source1

    • source0,结构体中context上下文中包含了各种回调函数,主要是perform回调函数(用于执行添加到source0中的任务),当调用CFRunLoopSourceSignal时会标记__CFRunLoopSource中的_bits标记位,然后调用CFRunLoopWakeUp来唤醒runloop再下一个循环中处理此回调;

      主要用于APP内部事件,由APP负责管理触发,如UIEvent事件;

    • source1,不同于source0执行回调函数,source1还需要指定mach port,用于监听系统内核事件或其他线程发来的事件;

    CFRunLoopTimer

    struct __CFRunLoopTimer
    {
        CFRuntimeBase _base;
        uint16_t _bits;
        pthread_mutex_t _lock;
        CFRunLoopRef _runLoop;
        CFMutableSetRef _rlModes;
        CFAbsoluteTime _nextFireDate;
        CFTimeInterval _interval;        /* immutable */
        CFTimeInterval _tolerance;       /* mutable */
        uint64_t _fireTSR;               /* TSR units */
        CFIndex _order;                  /* immutable */
        CFRunLoopTimerCallBack _callout; /* immutable */
        CFRunLoopTimerContext _context;  /* immutable, except invalidation */
    };
    

    CFRunLoopTimer结构中包含了时间相关的变量,runlooptimer事件触发都会去检查当前所有的timer时间点是否达到,若达到则处理事件任务;具体触发时间事件主要包含两种mk_timerdispatch source形式,两者都是基于mach port但是触发runloop并处理回调的处理方式不同;

    mk_timer是通过__CFRunLoopDoTimers来处理,依赖于runloop来触发时间回调函数,因此基于此的NStimerperformSelector:withObject:afterDelay:(是对NSTimer的包装),都需要runloop运行;

    dispatch source(针对主队列,其他队列不是通过runloop来触发)是通过__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__中指定的外部函数_dispatch_main_queue_callback_4CF来处理,调用堆栈如下

    image.png
    dispatch_after指定主队列的时间任务,是对dispatch_source的包装,对于存在UI的应用默认主队列的runloop是开启的,若是其他工具类应用,则需要手动开启主队列runloop,否则指定主队列的dispatch source是无法生效的;

    Timer有两种实现方式分别是MK_Timer和GCD Timer,在runloop中Timer被转为了一个存了触发时间的列表,这个触发时间是一个绝对时间,会按时间大小升序排序,在最小的时间被触发后,Runloop会更新列表保证时间始终是升序排列。如果Runloop在某次运行中阻塞了很长时间,Timer的触发会受到影响。过期的时间点会被移除而不会去触发。

    具体的NSTimerGCD Timer实现剖析可参考从NSTimer的失效性谈起(二):关于GCD Timer和libdispatch

    不过在源码中的USE_DISPATCH_SOURCE_FOR_TIMERS未生效,暂时未搞清问题,待后续补充;

    CFRunLoopObserver

    struct __CFRunLoopObserver
    {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;
        CFRunLoopRef _runLoop;
        CFIndex _rlCount;
        CFOptionFlags _activities;          /* immutable */
        CFIndex _order;                     /* immutable */
        CFRunLoopObserverCallBack _callout; /* immutable */
        CFRunLoopObserverContext _context;  /* immutable, except invalidation */
    };
    

    CFRunLoopObserver观察者对象指定了runloop相应状态变化(_activities指定需要观察的类型)及状态变化的回调指针_callout,具体观察的选项包括:

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
        kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
        kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
        kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
    };
    

    RunLoop事件循环

    image.png image.png
    runloop事件循环的逻辑如上图,对于黄色的Block是通过CFRunLoopPerformBlock添加的block任务,若runloop任务处理完成后就会休眠等待source1timer、或者手动被唤醒来继续下一次循环处理任务;

    RunLoop实践

    AutoReleasePool

    主要是利用runloop observer观察注册的事件:kCFRunLoopEntrykCFRunLoopBeforeWaitingkCFRunLoopExit,分别用于autoreleasepool push/pop操作来创建/释放内存池,并且保证自动内存池创建优先级其他回调之前,释放内存池在其他回调之后,进而不会导致内存泄露;

    事件响应/手势识别

    对于IOKit.framework生成的IOHIDEvent(如触摸/锁屏/静音/传感器加速等)会发送给SpringBoard接收,并通过mach port发送给注册了相应端口的source1应用进程,进而触发事件回调__IOHIDEventSystemClientQueueCallback,并通过_UIApplicationHandleEventQueue内部注册source0事件进行事件应用内部分发;

    手势识别就是将上面识别的手势UIGestureRecognizer标记为待处理,并注册了observer监测BeforeWaiting事件,触发回调来处理待处理的手势;

    界面更新

    苹果注册了observer监测BeforeWaiting/Eixt事件,当事件发生时会将已提交到全局容器待界面绘制的任务执行并更新UI,如果中间执行大量逻辑计算的任务导致runloop迟迟不触发ui更新的话,就会导致绘制ui的帧被丢弃即“丢帧”,进而引发ui卡顿,FaceBook推出的开源项目AsyncDisplayKit 就是防止主线程存在大量与ui不相关的任务处理(通过后台线程处理)阻塞ui更新,来避免“丢帧”提升界面流畅度;

    GCD

    对于提交至主队列的任务,如dispatch_source timerdispatch_async,都是主队列runloop中监听相对应的mach port事件,当事件发生时(timer到期或dispatch_async添加到主队列任务),libdispatch就会通过mach port端口向监听该端口的runloop发送唤醒消息,被唤醒的runloop触发回调函数__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__,该回调函数内部的_dispatch_main_queue_callback_4CF实际是由libdispatch定义处理的,即处理相应的任务;

    网络请求

    对于NSURLConnection实现原理如下图,具体为:

    image.png

    当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
    NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

    AFNetworking使用的常驻线程用于后台接收delegate回调,当有任务需要处理时,通过performSelector:onThread:将任务提交给该线程的runloop来处理,具体后台常驻线程创建如下:

    + (void)networkRequestThreadEntryPoint:(id)__unused object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
            NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
            [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runLoop run];
        }
    }
     
    + (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;
    }
    

    最重要的就是在runloop中添加了的未接收任何消息的NSMachPort进去,防止runloop退出进而线程退出;

    小知识

    宏定义 do{ }while(0)

    • 帮助定义复杂的宏以避免错误
    #define DO_SOMETHING() foo1();foo2();
    
    if (a>0)
      DO_SOMETHING();
    //展开后如下
    if (a>0)
      foo1();
      foo2();
    
    • 避免使用goto跳转

      int foo() {
        if (error1) {
            do_something();
            goto END:
        }
        if (error2) {
            do_something2();
            goto END;
        }
      END:
        xxx;
      }
      
      //使用do{}while(0)
      int foo() {
        do {
            if (error1) 
                do_something();
        if (error2) 
            do_something2();
        } while(0)
        
        xxx;
      }
      
    • 控制代码块

    • 避免由空宏定义造成的警告

      内核中由于不同架构的限制,很多时候会用到空宏,。在编译的时候,这些空宏会给出warning,为了避免这样的warning,我们可以使用do{...}while(0)来定义空宏: #define EMPTYMICRO do{}while(0); 这种情况不太常见,因为有很多编译器,已经支持空宏。

    do{...}while(0)的妙用

    CHECK_FOR_FORK()宏定义用途

    主要对于非移动端平台,如Mac OSX,进程调用fork生成子进程,一般是直接调用exec或类似的函数执行新的程序,而对于依赖Core Founadtion / Cocoa / Core Data 框架的应用,必须调用 exec 函数,否则这些框架也许不能正确的工作。

    Warning: When launching separate processes using the fork function, you must always follow a call to fork with a call to exec or a similar function. Applications that depend on the Core Foundation, Cocoa, or Core Data frameworks (either explicitly or implicitly) must make a subsequent call to an exec function or those frameworks may behave improperly.

    -- 摘自Threading Programming Guide

    理解:应该是避免进程使用vfork系统调用继续使用父进程的数据,导致影响父进程,因此要求立即调用exec去执行新的程序;

    Reference

    RunLoop --- CHECK_FOR_FORK()

    RunLoop 源码阅读

    Dispatch Sources

    深入浅出 GCD 之 dispatch_queue

    Threading Programming Guide -- Runloop

    iOS刨根问底-深入理解RunLoop

    CFRunLoopRef

    深入理解RunLoop

    iOS线下分享《RunLoop》by 孙源@sunnyxx

    demo

    https://github.com/FengyunSky/notes/blob/master/local/code/runloop.tar

    相关文章

      网友评论

          本文标题:iOS RunLoop由浅入深

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