美文网首页
RunLoop 运行机制原理逻辑与GCD及线程关系剖析(续)

RunLoop 运行机制原理逻辑与GCD及线程关系剖析(续)

作者: MoShengLive | 来源:发表于2018-09-26 16:04 被阅读0次

    由于字数限制 接上一遍https://www.jianshu.com/p/a5c442cb9a7f继续讲解下去

    7),增加记录的睡眠时间

     // 根据 poll 的值,记录休眠时间,休眠时间差
            rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
    

    8),到这里表示runloop正在运行中

    // Must remove the local-to-this-activation ports in on every loop
            // iteration, as this mode could be run re-entrantly and we don't
            // want these ports to get serviced. Also, we don't want them left
            // in there if this function returns.
    
            // 将 GCD 端口移除
            //对 waitSet 里的 dispatchPort 端口做移除
            __CFPortSetRemove(dispatchPort, waitSet);
            // 设置 runloop 不可被唤醒
            //让 Run Loop 忽略唤醒消息,因为已经重新在运行了
            __CFRunLoopSetIgnoreWakeUps(rl);
    
            //  取消runloop的休眠状态
            // user callouts now OK again
        __CFRunLoopUnsetSleeping(rl);
    

    八,通知 observers: kCFRunLoopAfterWaiting, 即停止等待(被唤醒)

         // 注意实际处理过 source 0 或者已经超时的话,不会通知(因为没有睡)
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
    
            // 11. 被什么唤醒就处理什么: 处理通过端口收到的消息
            handle_msg:;
    

    9),对唤醒线程做处理

     if (MACH_PORT_NULL == livePort) {// 不知道哪个端口唤醒的(或者根本没睡),啥也不干  livePort 为空,什么事都不做
                CFRUNLOOP_WAKEUP_FOR_NOTHING();
                // handle nothing
            } else if (livePort == rl->_wakeUpPort) {// 被 CFRunLoopWakeUp 函数弄醒的,啥也不干 跳回2重新循环 // livePort 等于 run loop 的 _wakeUpPort
                // 被 CFRunLoopWakeUp 函数唤醒的
                CFRUNLOOP_WAKEUP_FOR_WAKEUP();
    }
    

    CFRunLoopWakeUp唤醒函数与前面runloop使用GCD定时器超时机制__CFRunLoopTimeout里触发的, 所以回到第二大步 通知处理timer

    被 模mk timers 唤醒,处理 timers

    #if USE_MK_TIMER_TOO
            else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {// 被 模mk timers 唤醒,处理 timers
                //livePort 等于 run loop mode 的 _timerPort
                // 9.1-2 被 timers 唤醒,处理 timers
                CFRUNLOOP_WAKEUP_FOR_TIMER();
                // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
                // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
                if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                    // Re-arm the next timer
                    __CFArmNextTimerInMode(rlm, rl);
                }
            }
    #endif
    

    有dispatch到main_queue的block,执行block。

     else if (livePort == dispatchPort) {//9.2 如果有dispatch到main_queue的block,执行block。
                // 被 GCD 唤醒或者从第 7 步跳转过来的话,处理 GCD
                CFRUNLOOP_WAKEUP_FOR_DISPATCH();
                __CFRunLoopModeUnlock(rlm);
                __CFRunLoopUnlock(rl);
                //设置 CFTSDKeyIsInGCDMainQ 位置的 TSD 为 6 .
                _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
                // 执行block
                // 处理 msg
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                //设置 CFTSDKeyIsInGCDMainQ 位置的 TSD 为 0.
                _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
                __CFRunLoopLock(rl);
                __CFRunLoopModeLock(rlm);
                //设置变量
                sourceHandledThisLoop = true;
                didDispatchPortLastTime = true;
    }
    

    被 source (基于 mach port) 唤醒

     else {
                // source1 事件 
                //被 source (基于 mach port) 唤醒
                CFRUNLOOP_WAKEUP_FOR_SOURCE();
                
                // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again.
               // 假如我们 从这个 mach_msg 中接收到一个 voucher,然后在 TSD 中放置一个复制的新的 voucher.
                // CFMachPortBoost 会在 TSD 中去查找这个 voucher. 
                // 通过使用 TSD 中的值,我们将 CFMachPortBoost 绑定到这个接收到的 mach_msg 中,在这两段代码之间没有任何机会再次设置凭证
                voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
    
                // 被 sources 1 唤醒,处理 sources 1
                //9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
                // Despite the name, this works for windows handles as well
                 // 根据接收消息的 port 寻找 source1 事件
                CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
                if (rls) {// 有 source1 事件待处理  //如果 rls 存在
    #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            mach_msg_header_t *reply = NULL;
                    // 处理 source1 事件  //处理 Source ,并返回执行结果
            sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
            if (NULL != reply) {//发送reply消息(假如 reply 不为空)
                (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                //释放 reply 变量
                CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
            }
            }
                
                // Restore the previous voucher
                _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
                
            } 
    

    __CFRunLoopDoSource1方法核心代码调用系统方法

     if (__CFIsValid(rls)) {
        __CFRunLoopSourceUnsetSignaled(rls);
        __CFRunLoopSourceUnlock(rls);
            __CFRunLoopDebugInfoForRunLoopSource(rls);
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(rls->_context.version1.perform,
    #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
                msg, size, reply,
    #endif
                rls->_context.version1.info);
        CHECK_FOR_FORK();
        sourceHandled = true;
        } else {
            if (_LogCFRunLoop) { CFLog(kCFLogLevelDebug, CFSTR("%p (%s) __CFRunLoopDoSource1 rls %p is invalid"), CFRunLoopGetCurrent(), *_CFGetProgname(), rls); }
        __CFRunLoopSourceUnlock(rls);
        }
    
      //12. 再一次处理 blocks
        __CFRunLoopDoBlocks(rl, rlm);
            
    
            // 13. 判断是否退出,不需要退出则跳转回第 2 步  //根据一次循环后的状态,给 retVal 赋值 。状态不变则继续循环
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
                __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }
            
    #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            // 循环一次后收尾处理
            voucher_mach_msg_revert(voucherState);
            os_release(voucherCopy);
    #endif
    
        } while (0 == retVal);
    

    可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
    当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去 (call out), 当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数。下面是这几个函数的整理版本,如果你在调用栈中看到这些长函数名,在这里查找一下就能定位到具体的调用地点了:

    {
        /// 1. 通知Observers,即将进入RunLoop
        /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
        do {
     
            /// 2. 通知 Observers: 即将触发 Timer 回调。
            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
     
            /// 4. 触发 Source0 (非基于port的) 回调。
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
     
            /// 6. 通知Observers,即将进入休眠
            /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
     
            /// 7. sleep to wait msg.
            mach_msg() -> mach_msg_trap();
            
     
            /// 8. 通知Observers,线程被唤醒
            __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
     
            /// 9. 如果是被Timer唤醒的,回调Timer
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
     
            /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
     
            /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
     
     
        } while (...);
     
        /// 10. 通知Observers,即将退出RunLoop
        /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
    }
    

    AutoreleasePool

    App启动后,苹果在主线程 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 了。
    点击手机屏幕时


    image.png
    image.png

    上面的代码,有几个地方的定义可能需要结合其它地方才能理解:

    - voucher_mach_msg_state_t 在 [mach.h](https://opensource.apple.com/source/xnu/xnu-2782.30.5/libsyscall/mach/mach/mach.h) 中:
    
     ```objc
     /*!
     * @typedef voucher_mach_msg_state_t
     *
     * @abstract
     * Opaque object encapsulating state changed by voucher_mach_msg_adopt().
     */
    typedef struct voucher_mach_msg_state_s *voucher_mach_msg_state_t;
    

    不透明的对象封装状态由 voucher_mach_msg_adopt() 改变,它代表一种 mach_msg 通信时的状态。
    memset :作用是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法。

    HANDLE:

    DISPATCH_EXPORT HANDLE _dispatch_get_main_queue_handle_4CF(void);
    _dispatch_get_main_queue_port_4CF
    #define _dispatch_get_main_queue_port_4CF _dispatch_get_main_queue_handle_4CF
    

    它是 dispatch_get_main_queue_handle_4CF 的宏,存在 libdispatch 中,里面对它的实现为:

    #pragma mark -
    #pragma mark dispatch_main_queue
    
    dispatch_runloop_handle_t
    _dispatch_get_main_queue_handle_4CF(void)
    {
        dispatch_queue_t dq = &_dispatch_main_q;
        dispatch_once_f(&_dispatch_main_q_handle_pred, dq,
                _dispatch_runloop_queue_handle_init);
        return _dispatch_runloop_queue_get_handle(dq);
    }
    

    返回的是主线程 runloop 所关联的的端口。

    CFGetTSD / CFSetTSD

    看完代码相信大家对有些代码还有一些比较迷惑地方,里面多次用到这两句代码
    _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);和CFGetTSD
    下面来了解一下
    首先了解一下:Thread-specific data
    为了帮助理解,还需要先说 Thread-specific data (TSD),它可以叫作 线程私有数据 , 这个概念来自于 unix 之中。
    它是存储和查询与某个线程相关数据的一种机制:
    进程内的所有线程,共享进程的数据空间,因此全局变量为所有线程所共有。
    而有时线程也需要保存自己的私有数据,这时可以创建线程私有数据(Thread-specific Data)TSD来解决。
    在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量。
    Pthreads 里,把它叫做 Thread-local storage (线程私有存储) , 有以下几个相关的操作函数:

    - pthread_key_create(): 分配用于标识进程中线程特定数据的pthread_key_t类型的键
    - pthread_key_delete(): 销毁现有线程特定数据键
    - pthread_setspecific(): 为指定线程的特定数据键设置绑定的值
    - pthread_getspecific(): 获取调用线程的键绑定值,并将该绑定存储在 value 指向的位置中
    

    在苹果的平台上,基本就是利用上面操作实现 TSD 。
    RunLoop 实际上就属于 TSD 的里存储的一种数据。所以我们讲, RunLoop 和线程是一一对应的。而 RunLoop 会在线程销毁时,跟着一起清理,也是由于线程私有数据的机制。
    TSD 对应存储的 key 有相关的析构函数,线程退出时,析构函数函数就会按照操作系统,实现定义的顺序被调用。所以在 CFSetTSD 会有一个析构函数的参数位置。

    关于 CFGetTSD / CFSetTSD , 在 ForFoundationOnly.h 找到定义:

    // ---- Thread-specific data --------------------------------------------
    
    // Get some thread specific data from a pre-assigned slot.
    CF_EXPORT void *_CFGetTSD(uint32_t slot);
    
    // Set some thread specific data in a pre-assigned slot. Don't pick a random value. Make sure you're using a slot that is unique. Pass in a destructor to free this data, or NULL if none is needed. Unlike pthread TSD, the destructor is per-thread.
    CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, void (*destructor)(void *));
    
    //线程数据处理
    // For the use of CF and Foundation only
    CF_EXPORT void *_CFGetTSD(uint32_t slot) {
        if (slot > CF_TSD_MAX_SLOTS) {
            _CFLogSimple(kCFLogLevelError, "Error: TSD slot %d out of range (get)", slot);
            HALT;
        }
        __CFTSDTable *table = __CFTSDGetTable();
        if (!table) {
            // Someone is getting TSD during thread destruction. The table is gone, so we can't get any data anymore.
            _CFLogSimple(kCFLogLevelWarning, "Warning: TSD slot %d retrieved but the thread data has already been torn down.", slot);
            return NULL;
        }
        uintptr_t *slots = (uintptr_t *)(table->data);
        return (void *)slots[slot];
    }
    

    方法实现在CFPlatform.c类中

    // For the use of CF and Foundation only
    CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, tsdDestructor destructor) {
        if (slot > CF_TSD_MAX_SLOTS) {
            _CFLogSimple(kCFLogLevelError, "Error: TSD slot %d out of range (set)", slot);
            HALT;
        }
        __CFTSDTable *table = __CFTSDGetTable();
        if (!table) {
            // Someone is setting TSD during thread destruction. The table is gone, so we can't get any data anymore.
            _CFLogSimple(kCFLogLevelWarning, "Warning: TSD slot %d set but the thread data has already been torn down.", slot);
            return NULL;
        }
    
        void *oldVal = (void *)table->data[slot];
        
        table->data[slot] = (uintptr_t)newVal;
        table->destructors[slot] = destructor;
        
        return oldVal;
    }
    

    TSD 也就是 thread specific data 的缩写了。
    按照注释,说明 CFGetTSD 的作用是 – 从预先赋值的位置,得到 TSD。
    上面也说明了 CFSetTSD 的作用 – 在预先位置设置 TSD 。 这个数据不可以是随机的值,并保证你使用的位置有唯一性。如果需要释放这个数据,就传入析构函数;如果不需要释放,则传入NULL
    在上面的CFRunLoopGetCurrent里,是这么使用 _CFGetTSD 的:

    CFRunLoopRef CFRunLoopGetCurrent(void) {
        CHECK_FOR_FORK();
        CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
        if (rl) return rl;
        return _CFRunLoopGet0(pthread_self());
    }
    

    这里的 slot 值,一般对应的,都应该是类似 __CFTSDKeyRunLoop 的枚举类型的关键字。

    CFTSDTable

    CFPlatform.c 找到 CFGetTSD / CFSetTSD 具体的实现,发现两者其实都依靠了 CFTSDTable 类型的一个 table 实现。

    CFTSDTable 是一个保存 TSD 数据的结构体:

    // Data structure to hold TSD data, cleanup functions for each
    typedef struct __CFTSDTable {
        uint32_t destructorCount;
        uintptr_t data[CF_TSD_MAX_SLOTS];
        tsdDestructor destructors[CF_TSD_MAX_SLOTS];
    } __CFTSDTable;
    

    它拥有两个数组: data 存储私有数据, destructors 存储释放函数 . 还有一个 destructorCount ,它顾名思义就是 destructors 数组的数量。
    CFGetTSD 主要是取了 table ,获取 table 的 data 数组,按 slot 索引取值。

    CFSetTSD 的作用,就是根据 CFTSDTable 的结构,分别是往 table 里设置 data 数组 slot 位置的值,以及 destructors 数组 slot 位置的值:

    void *oldVal = (void *)table->data[slot];
    
    table->data[slot] = (uintptr_t)newVal;
    table->destructors[slot] = destructor;
    

    介绍完了CFSetTSD或CFGetTSD后 回到上一篇CFRunLoopGet0部分代码

    if (pthread_equal(t, pthread_self())) {
            _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
            if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                   // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
                _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
            }
        }
    

    判断 t 是否为当前线程。如果是当前线程,就会利用 CFSetTSD 在 CFTSDKeyRunLoop/CFTSDKeyRunLoopCntr 的位置做设置。
    在了解完 CFSetTSD 的作用, CFTSDKeyRunLoop 设置的意思就很清楚: 在 CFTSDKeyRunLoop 位置,存储 loop , 但不对 loop 设置析构函数。
    而在 CFTSDKeyRunLoopCntr 设置了清理 loop 的回调。
    细回想一下:
    在 CFTSDKeyRunLoopCntr 位置,给出的参数是 PTHREAD_DESTRUCTOR_ITERATIONS - 1
    PTHREAD_DESTRUCTOR_ITERATIONS 表示的,是线程退出时,操作系统实现试图销毁线程私有数据的最大次数。
    试图销毁次数,和 CFFinalizeRunLoop 这个析构函数,是怎么关联起来的?又是怎么被调用的?
    在前面,说过了 CFTSDTable ,实际上在 CFTSDGetTable() 里面,就做了相关 TSD 的设置:

    // Get or initialize a thread local storage. It is created on demand.
    static __CFTSDTable *__CFTSDGetTable() {
       ...
            pthread_key_init_np(CF_TSD_KEY, __CFTSDFinalize);
       ...
    

    有关pthread_key_init_np链接 https://github.com/apple/darwin-libpthread/blob/master/src/pthread_tsd.c
    通过 CF_TSD_KEY ,指定了对应的析构函数 CFTSDFinalize 。
    而 CFTSDFinalize 的代码如下:

    static void __CFTSDFinalize(void *arg) {
        // Set our TSD so we're called again by pthreads. It will call the destructor PTHREAD_DESTRUCTOR_ITERATIONS times as long as a value is set in the thread specific data. We handle each case below.
        //设置TSD中我们又称为pthreads。它将调用析构函数PTHREAD_DESTRUCTOR_ITERATIONS倍长线程特定数据的值设置。下面我们处理每个案例。
        __CFTSDSetSpecific(arg);
    
        if (!arg || arg == CF_TSD_BAD_PTR) {
            // We've already been destroyed. The call above set the bad pointer again. Now we just return.
            return;
        }
        
        __CFTSDTable *table = (__CFTSDTable *)arg;
        table->destructorCount++;
            
        // On first calls invoke destructor. Later we destroy the data.
        // Note that invocation of the destructor may cause a value to be set again in the per-thread data slots. The destructor count and destructors are preserved.  
        // This logic is basically the same as what pthreads does. We just skip the 'created' flag.
        for (int32_t i = 0; i < CF_TSD_MAX_SLOTS; i++) {
            if (table->data[i] && table->destructors[i]) {
                uintptr_t old = table->data[I];
                table->data[i] = (uintptr_t)NULL;
                table->destructors[i]((void *)(old));
            }
        }
        
        if (table->destructorCount == PTHREAD_DESTRUCTOR_ITERATIONS - 1) {    // On PTHREAD_DESTRUCTOR_ITERATIONS-1 call, destroy our data
            free(table);
            
            // Now if the destructor is called again we will take the shortcut at the beginning of this function.
            __CFTSDSetSpecific(CF_TSD_BAD_PTR);
            return;
        }
    }
    

    我们可以看到,table 会循环遍历 data 和 destructors 的数据,并且把 old 变量作为 destructors 里函数的参数。也就是回到之前注册函数__CFFinalizeRunLoop回调
    __CFFinalizeRunLoop函数:

    // Called for each thread as it exits
    CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data) {
        CFRunLoopRef rl = NULL;
        if (data <= 1) {
        __CFLock(&loopsLock);
        if (__CFRunLoops) {
            rl = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(pthread_self()));
            if (rl) CFRetain(rl);
           //移除 __CFRunLoops
            CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self()));
        }
        __CFUnlock(&loopsLock);
        } else {//递归移除
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void (*)(void *))__CFFinalizeRunLoop);
        }
        if (rl && CFRunLoopGetMain() != rl) { // protect against cooperative threads
            if (NULL != rl->_counterpart) {
                CFRelease(rl->_counterpart);
            rl->_counterpart = NULL;
            }
        // purge all sources before deallocation
            CFArrayRef array = CFRunLoopCopyAllModes(rl);
            for (CFIndex idx = CFArrayGetCount(array); idx--;) {
                CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx);
                //移除runloop对应modeName
                __CFRunLoopRemoveAllSources(rl, modeName);
            }
            //移除runloop里所有models
            __CFRunLoopRemoveAllSources(rl, kCFRunLoopCommonModes);
            CFRelease(array);
        }
        if (rl) CFRelease(rl);
    }
    

    这就是线程退出时,会调用到 Run Loop 销毁函数的原因。
    同时也由于 table 是从 0 开始遍历,所以会根据枚举值的大小,来决定销毁调用的顺序的。
    我们可以在 CFInternal.h 中找到相关枚举定义:

    // Foundation uses 20-40
    // Foundation knows about the value of __CFTSDKeyAutoreleaseData1
    enum {
        __CFTSDKeyAllocator = 1,
        __CFTSDKeyIsInCFLog = 2,
        __CFTSDKeyIsInNSCache = 3,
        __CFTSDKeyIsInGCDMainQ = 4,
        __CFTSDKeyICUConverter = 7,
        __CFTSDKeyCollatorLocale = 8,
        __CFTSDKeyCollatorUCollator = 9,
        __CFTSDKeyRunLoop = 10,
        __CFTSDKeyRunLoopCntr = 11,
            __CFTSDKeyMachMessageBoost = 12, // valid only in the context of a CFMachPort callout
            __CFTSDKeyMachMessageHasVoucher = 13,
        // autorelease pool stuff must be higher than run loop constants
        __CFTSDKeyAutoreleaseData2 = 61,
        __CFTSDKeyAutoreleaseData1 = 62,
        __CFTSDKeyExceptionData = 63,
    };
    

    注释里有一句 autorelease pool stuff must be higher than run loop constants 的说明,这一点就其实关系到 Run Loop 和 autorelease pool 释放的顺序了。

    RunLoop使用场景

    1.保证线程的长时间存活
    在iOS开发过程中,有时候我们不希望一些花费时间比较长的操作阻塞主线程,导致界面卡顿,那么我们就会创建一个子线程,然后把这些花费时间比较长的操作放在子线程中来处理。可是当子线程中的任务执行完毕后,子线程就会被销毁掉。
    当子线程中的任务执行完毕后,线程就被立刻销毁了。


    image.png

    如果程序中,需要经常在子线程中执行任务,频繁的创建和销毁线程,会造成资源的浪费。这时候我们就可以使用RunLoop来让该线程长时间存活而不被销毁
    我们将上面的示例代码修改一下,修改后的代码过程为,创建一个子线程,当子线程启动后,启动runloop,点击视图,会在子线程中执行一个耗时3秒的任务(其实就是让线程睡眠3秒)。


    image.png
    image.png

    注意点:

    关于AutoReleasePool的官方文档中有提到:

    *   f you spawn a secondary thread.
        You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects. (See [Autorelease Pool Blocks and Threads]
    

    子线程使用RunLoop需要使用自动释放池来保证内存不泄露
    AFNetworking中的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; }
    

    线程私有数据:http://zdyi.com/books/apue/s11/11.9.html
    事件驱动:https://godbasin.github.io/2017/09/29/data-driven-or-event-driven/#%E4%BA%8B%E4%BB%B6%E9%A9%B1%E5%8A%A8%E7%BC%96%E7%A8%8B
    CFRunLoop.c 源码: https://opensource.apple.com/source/CF/CF-1153.18/CFRunLoop.c.auto.html
    XNU 源码
    UNIX环境高级编程——线程私有数据

    相关文章

      网友评论

          本文标题:RunLoop 运行机制原理逻辑与GCD及线程关系剖析(续)

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