• RunLoop解析


    一、什么是RunLoop?

    先看看官网对他的描述
    在这里插入图片描述
    RunLoop是一个处理对象输入源的类,例如Port的调度,触摸事件,UI刷新事件,定时器事件,Selector事件,RunLoop还负责管理Timer事件。RunLoop和Thread对象是一一对应的,我们不能创建RunLoop,只可以获取,这里注意并不是每个Thread都有RunLoop。
    那么RunLoop的逻辑是什么呢?其实可以抽象的把它理解为跑圈,有事件处理的时候处理事件,在没有事件处理的时候,进入睡眠模式,从而节省CPU资源,底层逻辑是利用Do-while实现的。

    function loop() {
        initialize();
        do {
            var message = get_next_message();
            process_message(message);
        } while (message != quit);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这种模型被称为Event Loop,在很多系统和框架里都有实现,这种模式的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠,在有消息后立即被唤醒。

    OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
    CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
    NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。可以看到在官方的描述中,也说了RunLoop并非是线程安全的。

    二、RunLoop和线程

    上面我们提到了Thread和RunLoop是一一对应的,每个线程只能有一个RunLoop对象,且与之对应。
    这样看来RunLoop和线程是息息相关的,一般线程默认情况下执行完任务就会退出,不再执行任务,我们加入RunLoop后就可以实现跑圈,在没有任务时休眠,而不是退出。
    那么RunLoop对象是如何创建的?
    实际上主线程的RunLoop对象系统自动帮助我们创建好了,而子线程的RunLoop对象需要我们主动创建,但是apple并不允许我们直接创建RunLoop,而是提供了两个自动获取的函数,CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

    /// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
    static CFMutableDictionaryRef loopsDic;
    /// 访问 loopsDic 时的锁
    static CFSpinLock_t loopsLock;
     
    /// 获取一个 pthread 对应的 RunLoop。
    CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
        OSSpinLockLock(&loopsLock);
        
        if (!loopsDic) {
            // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
            loopsDic = CFDictionaryCreateMutable();
            CFRunLoopRef mainLoop = _CFRunLoopCreate();
            CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
        }
        
        /// 直接从 Dictionary 里获取。
        CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
        
        if (!loop) {
            /// 取不到时,创建一个
            loop = _CFRunLoopCreate();
            CFDictionarySetValue(loopsDic, thread, loop);
            /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
            _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
        }
        
        OSSpinLockUnLock(&loopsLock);
        return loop;
    }
     
    CFRunLoopRef CFRunLoopGetMain() {
        return _CFRunLoopGet(pthread_main_thread_np());
    }
     
    CFRunLoopRef CFRunLoopGetCurrent() {
        return _CFRunLoopGet(pthread_self());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    从上面的代码可以看出,线程和RunLoop之间是一一对应的,其关系是保存在一个全局的Dictionary里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

    三、RunLoop的结构

    RunLoop位于苹果的Core Foundation库中,而Core Foundation库则位于iOS架构分层的Core Service层中(值得注意的是,Core Foundation是一个跨平台的通用库,不仅支持Mac,iOS,同时也支持Windows):
    在这里插入图片描述
    在这里插入图片描述
    苹果官方将整个系统大致划分为上述4个层次:
    应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。
    应用框架层即开发人员接触到的 Cocoa 等框架。
    核心框架层包括各种核心框架、OpenGL 等内容。
    Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

    RunLoop公开的接口,有下面几个类:

    CFRunLoopRef
    CFRunLoopModeRef
    CFRunLoopSourceRef
    CFRunLoopTimerRef
    CFRunLoopObserverRef
    
    • 1
    • 2
    • 3
    • 4
    • 5
    // Foundation:
    NSRunLoop : NSObject
    
    // Core Foundation:
    struct __CFRunLoop
    __CFRunLoop * CFRunLoopRef
    
    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;
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们其中对应的关系如下:
    在这里插入图片描述

    CFRunLoopMode

    每次RunLoop开始Run的时候,都必须指定一个Mode,称为RunLoop Mode。Mode指定了在这次的Run中,RunLoop可以处理的任务。对于不属于当前Mode的任务,则需要切换RunLoop至对应Mode下,再重新调用run方法,才能够被处理。
    一个RunLoop包含着若干个Mode,每个Mode又包含若干个Source/Timer/Observer。每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode叫做CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

    struct __CFRunLoopMode {
        CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
        CFMutableSetRef _sources0;    // Set
        CFMutableSetRef _sources1;    // Set
        CFMutableArrayRef _observers; // Array
        CFMutableArrayRef _timers;    // Array
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    RunLoop Source

    苹果文档将RunLoop能够处理的事件分为Input sources和timer事件。下面这张图取自苹果官网,不要注意那些容易让人混淆的细节,只看Thread , Input sources 和 Timer sources三个大方块的关系即可,不要关注里面的内容。
    在这里插入图片描述

    CFRunLoopSourceRef是时间产生的地方。Source有两个版本:Source0和Source1
    Source0只包含了一个回调(函数指针)它并不能主动触发事件。使用时,你想要先调用CFRunLoopSourceSignal(source),将这个Source作为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

    Source1包含了一个mach_port和一个回调(函数指针)被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop。
    看看源码:

    struct __CFRunLoopSource {
        CFRuntimeBase _base;
        uint32_t _bits;
        pthread_mutex_t _lock;
        CFIndex _order;         /* 优先级,越小,优先级越高。可以是负数。immutable */
        CFMutableBagRef _runLoops;
        union {  // 联合,用于保存source的信息,同时可以区分source是0还是1类型
            CFRunLoopSourceContext version0;    /* immutable, except invalidation */
            CFRunLoopSourceContext1 version1;   /* immutable, except invalidation */
        } _context;
    };
    
    typedef struct {
        CFIndex version;      // 类型:source0
        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);  // call out 
    } CFRunLoopSourceContext;
    
    typedef struct {
        CFIndex version;  // 类型:source1
        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);
    #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
        mach_port_t (*getPort)(void *info);
        void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
    #else
        void *  (*getPort)(void *info);
        void    (*perform)(void *info); // call out
    #endif
    } CFRunLoopSourceContext1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    source0和source1由联合_context来做代码区分。
    Source1 由RunLoop和内核管理,Mach Port驱动。
    Source0 则偏向应用层一些,如Cocoa里面的UIEvent处理,会以source0的形式发送给main RunLoop。

    Mode类型

    • Default mode 是RunLoop默认的mode。当RunLoop被创建时,就会对应创建出一个default mode。其余的mode,则是懒加载的。
    • Event tracking mode 是Cocoa在处理密集传入的事件时所使用的mode(如scrollview的滑动)。
    • “CommonModes”:在Cocoa程序中,默认会包含default,modal,event tracking mode。而在Core Foundation程序中,默认仅有Default mode,一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
      这意味着,当我们将mode作为整体加入到Common modes中时,该mode可以响应common mode item事件,而其本身自带的mode items,在被标记为common的mode中,却不会被响应。

    应用场景NSTimer的使用

    主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
    如果我们想要滑动的时候NSTimer依旧在使用,可以将Timer加入到commonModelItems。

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
    • 1

    CFRunLoopTimerRef && Timer

    CFRunLoopTimerRef是基于时间的触发器,它和 NSTimer是toll-free bridged 的,可以混用。其包含了一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
    还有一些常用的Timer:

    • NSTimer & PerformSelector:afterDelay:(由RunLoop处理,内部结构为CFRunLoopTimerRef)
    • GCD Timer(由GCD自己实现,不通过RunLoop)
      CADisplayLink(通过向RunLoop投递source1 实现回调)
      NSObject perform系列函数中的dealy类型, 其实也是一种Timer事件,可能不那么明显。

    NSTimer & PerformSelector:after Delay:

    NSTimer在CF源码中的结构是这样的:

    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 */ // timer 回调
        CFRunLoopTimerContext _context; /* immutable, except invalidation */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    关于Timer的计时,是通过内核的mach time或GCD time来实现的。

    在RunLoop中,NSTimer在激活时,会将休眠中的RunLoop通过_timerPort唤醒,之后,RunLoop会调用。

    CFRunLoopObserverRef

    CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

    Mode一些接口

    CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
    CFRunLoopRunInMode(CFStringRef modeName, ...);
    
    • 1
    • 2

    管理mode item的接口

    CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
    CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
    CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
    CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
    CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
    CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    你只能通过mode name来操作内部的mode,当你传入一个新的mode name但RunLoop没有对应的mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef。对于一个RunLoop来说,其内部的mode只能增加不能删除。

    四、RunLoop逻辑

    上一张经典图:
    在这里插入图片描述

    主要流程如下:
    1.通知观察者RunLoop已经启动
    2.通知观察者即将要开始的定时器
    3.通知观察者任何即将启动的非基于端口的源
    4.启动任何准备好的非基于端口的源
    5.如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
    6.通知观察者线程进入休眠状态
    7.将线程置于休眠知道任一下面的事件发生:

    • 某一事件到达基于端口的源
    • 定时器启动
    • RunLoop设置的时间已经超时
    • RunLoop被显示唤醒
      8.通知观察者线程将被唤醒
      9.处理未处理的事件
    • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
    • 如果输入源启动,传递相应的消息
    • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
      10。通知观察者RunLoop结束。

    RunLoop源码

    /// 用DefaultMode启动
    void CFRunLoopRun(void) {
        CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
    }
     
    /// 用指定的Mode启动,允许设置RunLoop超时时间
    int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
        return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
    }
     
    /// RunLoop的实现
    int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
        
        /// 首先根据modeName找到对应mode
        CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
        /// 如果mode里没有source/timer/observer, 直接返回。
        if (__CFRunLoopModeIsEmpty(currentMode)) return;
        
        /// 1. 通知 Observers: RunLoop 即将进入 loop。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
        
        /// 内部函数,进入loop
        __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
            
            Boolean sourceHandledThisLoop = NO;
            int retVal = 0;
            do {
     
                /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
                /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
                /// 4. RunLoop 触发 Source0 (非port) 回调。
                sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
     
                /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
                if (__Source0DidDispatchPortLastTime) {
                    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                    if (hasMsg) goto handle_msg;
                }
                
                /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
                if (!sourceHandledThisLoop) {
                    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
                }
                
                /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
                /// • 一个基于 port 的Source 的事件。
                /// • 一个 Timer 到时间了
                /// • RunLoop 自身的超时时间到了
                /// • 被其他什么调用者手动唤醒
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                    mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
                }
     
                /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
                
                /// 收到消息,处理消息。
                handle_msg:
     
                /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
                if (msg_is_timer) {
                    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
                } 
     
                /// 9.2 如果有dispatch到main_queue的block,执行block。
                else if (msg_is_dispatch) {
                    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                } 
     
                /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
                else {
                    CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                    sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                    if (sourceHandledThisLoop) {
                        mach_msg(reply, MACH_SEND_MSG, reply);
                    }
                }
                
                /// 执行加入到Loop的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
     
                if (sourceHandledThisLoop && stopAfterHandle) {
                    /// 进入loop时参数说处理完事件就返回。
                    retVal = kCFRunLoopRunHandledSource;
                } else if (timeout) {
                    /// 超出传入参数标记的超时时间了
                    retVal = kCFRunLoopRunTimedOut;
                } else if (__CFRunLoopIsStopped(runloop)) {
                    /// 被外部调用者强制停止了
                    retVal = kCFRunLoopRunStopped;
                } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                    /// source/timer/observer一个都没有了
                    retVal = kCFRunLoopRunFinished;
                }
                
                /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
            } while (retVal == 0);
        }
        
        /// 10. 通知 Observers: RunLoop 即将退出。
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110

    可以看到,实际上RunLoop就是这样的函数,内部就是个do-while循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

    五、Apple中的RunLoop

    AutoreleasePool

    系统会在APP启动时,向main RunLoop里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

    第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 
    _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
    第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 
    时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 
    释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 
    Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
    在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 
    环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    事件响应

    苹果注册了一个 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 事件都是在这个回调中完成的。

    界面更新

    我们需要界面刷新,如UIView/CALayer 调用了setNeedsLayout/setNeedsDisplay, 或更新了UIView的frame,或UI层次。
    其实,系统并不会立刻就开始刷新界面,而是先提交UI刷新请求,再等到下一次main RunLoop循环时,集中处理(集中处理的好处在于可以合并一些重复或矛盾的UI刷新)。而这个实现方式,则是通过监听main RunLoop的before waitting和Exit通知实现的。
    回调去执行一个很长的函数:
    _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

    NSTimer

    NSTimer在CF层对应的结构如下,它们之间的转换时toll-free的:
    一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
    Timer的触发流程大致是这样的:
    (1)用户添加timer到runloop的某个或几个mode下->
    (2)根据timer是否设置了tolerance,如果没有设置,则调用底层xnu内核的mk_timer注册一个mach-port事件,如果设置了tolerance,则注册一个GCD timer->
    (3)当由XNU内核或GCD管理的timer的 fire time到了,通过对应的mach port唤醒RunLoop(mk_timer对应rlm的_timerPort, GCD timer对应GCD queue port)->
    (4)RunLoop执行__CFRunLoopDoTimers,其中会调用__CFRunLoopDoTimer, DoTimer方法里面会根据当前mach time和Timer的fireTSR属性,判断fireTSR 是否< 当前的mach time,如果小于,则触发timer,同时更新下一次fire时间fireTSR。

    这样一次timer事件从注册到执行大致流程如图所示:
    在这里插入图片描述

    PerformSelector

    当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    dispatch to main queue

    当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

    六、RunLoop实际应用

    我们何时需要RunLoop? 苹果给我们列出了以下情境:

    • 需要使用port或input source和别的线程通信(基本用不到)
    • 在当前线程需要使用NSTimer
    • 在Cocoa程序中需要使用performSelector… 系列函数
    • 需要子线程保活来执行后台任务

    线程保活

    在文章开头我们就提到,RunLoop的一个作用就在于让线程跑圈,不至于退出。如果我们自己创建一个子线程而没有让它的RunLoop一直run的话,线程会立刻退出。

    我们可以用如下代码,让子线程一直等待一个mach port,从而维持RunLoop 不退出循环:

    // 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
    static CFMutableDictionaryRef loopsDic;
    
    @interface ViewController ()<NSPortDelegate>
    @property (nonatomic, strong) NSMessagePort* messagePort;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        @autoreleasepool{
              NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
              self.messagePort = [NSPort port];
              self.messagePort.delegate = self;
              [runLoop addPort:self.messagePort forMode:NSDefaultRunLoopMode];
              [runLoop run]; // 当runloop 在跑圈时,下面的代码永远不会执行。
              NSLog(@"runloop exit!!");  // 仅当子线程即将退出时,才会执行到这里
           }
     
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    之所以要在代码外包一层autoreleasepool,是因为系统不会为子线程自动创建autorelease pool,为了防止内存泄漏,我们在所有的子线程开始都要创建一个autorelease pool
    那么我们如何退出呢?
    我们可以移除刚刚添加的mach port,可以通过定时器,或者完成某些事件后调用:[[NSRunLoop currentRunLoop] removePort:self.messagePort forMode:NSDefaultRunLoopMode];

    RunLoop 性能优化

    tableView的Cell中有多个ImageView,同时加载大图,导致UI卡顿。
    解决思路:使用Runloop每次循环只添加一张图片。
    工具:这里我们需要使用到CFRunloop
    实现过程:

    1、把加载图片等代码保存起来,先不执行 (保存一段代码,block)
    2、监听Runloop循环(CFRunloopObserver)
    3、每次都从任务数组中取出一个加载图片等代码执行(执行block代码)
    底层实现有点懵,这里没看明白,想看的可以看看下面大牛的文章。

    本文参考:
    RunLoop详解

    iOS RunLoop完全指南

    七、一些RunLoop问题

    RunLoop线程阻塞 & 线程休眠?

    RunLoop的线程休眠是真的休眠,它是不会占用任何cpu的资源,完全休息.它和white(1)这种还是有本质的区别,white(1)它是一直在执行,转成汇编会有几条指令,一直在执行,一直占用cpu资源,比如我们想做优化,那RunLoop的这种休眠模式是不是更节约cpu资源,说白了更省电些。
    本质是利用了:mach_msg。
    在这里插入图片描述
    就是用户态和内核态的切换,用户态发消息,内核态休眠,再被唤醒,用户态处理消息.

    所以如果面试官问:RunLoop里面线程阻塞是怎么样的?我们千万不能答,里面是个死循环.就是上面刚刚说的那些.

    在这里插入图片描述
    剩下的答案上面有。

  • 相关阅读:
    自动化运维ansible(role)
    React的 虚拟DOM创建
    MySQL——多版本并发控制(MVCC)
    【java学习】项目: ATM系统
    中国石油大学《高等数学二》第三次在线作业
    数据库字段打标
    一文读懂:低代码和无代码的演进历程、应用范围
    sql sever 数据更改
    计算机毕业设计ssm高校车辆租赁管理系统23qhn系统+程序+源码+lw+远程部署
    微信小程序使用本地存储方法
  • 原文地址:https://blog.csdn.net/chabuduoxs/article/details/126314331