【iOS底层分析】RunLoop

213 阅读10分钟

基本概念

定义

通常来讲,一个线程在执行完任务后,会直接退出。但在我们日常的App使用中,即使什么都不做,App也不会退出。这其中的原理,便是运行时在线程中构建一个消息循环,使得这个线程中一直有任务执行。在iOS和OSX中,这个消息循环的机制便被称为RunLoop。

所以Runloop就是通过内部维护的事件循环来对事件/消息进行管理的对象

Swift中的RunLoop对应OC中的NSRunLoop,本文以Swift的RunLoop作为主要讲解名词。

作用

  1. 保持程序的正常运行

  2. 处理APP中的各种事件

    1. 事件响应、手势识别、界面刷新

    2. 网络请求

    3. GCD Async Main Queue

    4. Timer定时器、performSelector

    5. AutoReleasePool

    6. ···

  1. 节省cpu资源、提供程序的性能:该做事就做事,该休息就休息

CFRunloop

  1. CFRunLoop对象监视任务的输入源,并在它们准备好进行处理时分派控制。

  2. 运行循环可以监视三种类型的对象:CFRunLoopSourceCFRunLoopTimerCFRunLoopObserver

  3. 添加到运行循环中的每个源、计时器和观察者必须与一个或多个运行循环模式相关联。

  4. Core Foundation定义了一种特殊的伪模式,称为common modes,它允许您将多个模式与给定的source、timer或observer关联起来。

  5. 每个线程只有一个运行循环。你既不创建也不销毁线程的运行循环。Core Foundation会根据需要自动为您创建它。

  6. 运行循环可以递归地运行。您可以在任何运行循环调用中调用CFRunLoopRunCFRunLoopRunInMode,并在当前线程的调用堆栈上创建嵌套的运行循环激活。

  7. Cocoa应用程序构建在CFRunLoop之上,实现它们自己的高级事件循环。在编写应用程序时,可以将源代码、计时器和观察者添加到它们的运行循环对象和模式中。然后,您的对象将作为常规应用程序事件循环的一部分被监视。使用RunLoopGetCFRunLoop方法可以得到对应的CFRunLoopRef类型。

RunLoop

  • RunLoop是对Core Fundation中的CFRunloop的封装

  • RunLoop对象处理:

    - 来自窗口系统的鼠标和键盘事件

    - NSPort对象和NSConnection对象等源的输入。

    - NSTimer事件。

  • RunLoop类通常被认为是线程不安全的,它的方法应该只在当前线程的上下文中被调用。永远不要尝试调用运行在不同线程中的RunLoop对象的方法,因为这样做可能会导致意想不到的结果。

CFRunLoop结构分析


struct __CFRunLoop {

    ···

    _CFThreadRef _pthread;

  


    CFMutableSetRef _commonModes;

    CFMutableSetRef _commonModeItems;

    CFRunLoopModeRef _currentMode;

    CFMutableSetRef _modes;

    ···

};

  


// Mode

struct __CFRunLoopMode {

    ···

    CFMutableSetRef _sources0;

    CFMutableSetRef _sources1;

    CFMutableArrayRef _observers;

    CFMutableArrayRef _timers;

    ···

};

  


// Source

struct __CFRunLoopSource {

    ···

    union {

        // 用以区分Source0和Source1

        CFRunLoopSourceContext version0;

        CFRunLoopSourceContext1 version1;

    } _context;

};

  


// Observer

struct __CFRunLoopObserver {

    ···

    CFIndex _rlCount;

    CFOptionFlags _activities; /* immutable */

    CFIndex _order; /* immutable */

    CFRunLoopObserverCallBack _callout; /* immutable */

    ···

};

  


// Timer

struct __CFRunLoopTimer {

    ···

    uint16_t _bits;

    CFAbsoluteTime _nextFireDate;

    CFTimeInterval _interval; /* immutable */

    CFTimeInterval _tolerance;          /* mutable */

    uint64_t _fireTSR; /* TSR units */

    CFIndex _order; /* immutable */

    CFRunLoopTimerCallBack _callout; /* immutable */

    ···

};

  


如图所示

Untitled

__CFRunLoopModeRef

每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作CurrentMode,如果需要切换 Mode,只能退出Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

代表RunLoop的运行模式

  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer

  • 如果Mode里面没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

  • 切换mode是在循环里面切换的,所以不会导致程序退出

  • 常见mode

    1. KCFRunLoopDefaultMode (对应NSDefaultRunLoopMode):App的默认Mode,通常是主线程是在这个Mode下运行

    2. UITrackingRunLoopMode : 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响

__CFRunLoopSource

事件产生的地方

  • Source0:**一般处理触摸事件和performSelector:onThread:。 只包含一个回调函数指针 (代表不能主动触发) **,需要给一个待处理的signal,再调用wakeup唤醒runLoop处理事件。

  • Source1:**基于Port的线程间通信,系统事件的捕捉(包括触摸事件,捕捉到后传递给Source0)。 **基于包含mach_port(端口通讯)和回调函数指针。被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

__CFRunLoopObserver

是观察者,每个 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

    kCFRunLoopAllActivities = 0x0FFFFFFFU

};

__CFRunLoopTimer

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

RunLoop实现逻辑

下图来自苹果官方文档,帮助理解RunLoop的实现逻辑。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3f5f27801764dbea9d0334856df9779~tplv-k3u1fbpfcp-zoom-1.image

下面我们从CFRunLoopRun的源码着手来梳理一下流程。源码有重点删减,篇幅较长,可以跳过,直接看结论


void CFRunLoopRun(void) {

    int32_t result;

    do {

        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);

        ···

    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);

}

  


可以看到主要调用的是CFRunLoopRunSpecific方法,当返回的Mode不为kCFRunLoopRunStopped/kCFRunLoopRunFinished时,就一直执行


SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, 

CFStringRef modeName,

CFTimeInterval seconds,

Boolean returnAfterSourceHandled) {

    // 基本状态判断

    ···

    // 根据modeName找到本次运行的mode

    CFRunLoopModeRef currentMode = __CFRunLoopCopyMode(rl, modeName, false);

    // 如果没找到,或mode中没有注册任何事件,则就此停止

    ···

  


    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);

    // 取上一次运行的mode

    CFRunLoopModeRef previousMode = rl->_currentMode;

    // 将本次运行的mode赋值给runloop中存储

    rl->_currentMode = currentMode;

    int32_t result = kCFRunLoopRunFinished;

  


    // 【mode判断】若当前为即将进入entry,通知对应观察者

if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_START, rl, currentMode, seconds, previousMode);

        // 执行runloop

        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_END, rl, currentMode, seconds, previousMode);

        // 【mode判断】若当前即将退出runloop,通知对应观察者

        if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

  


    ···

    return result;

}

  


// __CFRunLoopRun执行runloop

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

  


    //获取系统启动后的CPU运行时间,用于控制超时时间

    uint64_t startTSR = mach_absolute_time();

  


    // 基本mode状态判断

    ···

  


    // mach端口,在内核中,消息在端口之间传递。 初始为0

    __CFPort dispatchPort = CFPORT_NULL;

    // 判断是否为主线程

    Boolean libdispatchQSafe = (pthread_main_np() == 1) && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));

    // 如果在主线程 ,并且 runloop是主线程的runloop ,并且 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口

    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();


    uint64_t termTSR = 0ULL;

    dispatch_source_t timeout_timer = NULL;

 
    // 基本超时时间判断

    ···

    // 超时时间设置永不超时

    seconds = 9999999999.0;

    termTSR = UINT64_MAX;

  


    Boolean didDispatchPortLastTime = true;

    int32_t retVal = 0;

    do {

        voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;

        voucher_t voucherCopy = NULL;

        //初始化一个存放内核消息的缓冲池

        uint8_t msg_buffer[3 * 1024];

        mach_msg_header_t *msg = NULL;

        mach_port_t livePort = MACH_PORT_NULL;


        // 取所有需要监听的port

    __CFPortSet waitSet = rlm->_portSet;


        // 设置RunLoop为可以被唤醒状态

        __CFRunLoopUnsetIgnoreWakeUps(rl);


        // 【mode判断】若即将触发 Timer 回调,通知对应观察者

        if (rlm->_observerMask & kCFRunLoopBeforeTimers) {

            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

        }


        // 【mode判断】若即将触发 Source0(非port) 回调,通知对应观察者

        if (rlm->_observerMask & kCFRunLoopBeforeSources) {

            __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        }

        // 执行被加入的block

    __CFRunLoopDoBlocks(rl, rlm);


        // 触发 Source0 (非port) 回调。

        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

        if (sourceHandledThisLoop) {

            __CFRunLoopDoBlocks(rl, rlm);

        }


        //如果没有Sources0事件处理 并且 没有超时,poll为false

        //如果有Sources0事件处理 或者 超时,poll都为true

        Boolean poll = sourceHandledThisLoop || (0ULL == termTSR);

         //第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true

        if (CFPORT_NULL != dispatchPort && !didDispatchPortLastTime) {

            //从缓冲区读取消息

            msg = (mach_msg_header_t *)msg_buffer;

            // 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。

            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL, rl, rlm)) {

                //如果接收到了消息的话,处理msg

                goto handle_msg;

            }

        }

  


        didDispatchPortLastTime = false;

  


        // 【mode判断】若runloop即将休眠,通知对应观察者

if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

        // 使runloop休眠

__CFRunLoopSetSleeping(rl);

        ···

        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();


        msg = (mach_msg_header_t *)msg_buffer;

        /// 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。

        /// • 一个基于 port 的Source 的事件。

        /// • 一个 Timer 到时间了

        /// • RunLoop 自身的超时时间到了

        /// • 被其他什么调用者手动唤醒

        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy, rl, rlm);

  


        // 设置当前runloop的sleepTime为当前时间减去开始时间

        rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

  


        // 移除端口

        __CFPortSetRemove(dispatchPort, waitSet);

  


        // 设置RunLoop为不可以被唤醒状态

        __CFRunLoopSetIgnoreWakeUps(rl);

  


        // 若刚从休眠中唤醒,通知对应观察者

if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

  


        // 若当前不为主线程端口

        if (rlm->_timerPort != CFPORT_NULL && livePort == rlm->_timerPort) {

            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {

                // 重新设置下一个计时器

                rlm->_timerSoftDeadline = UINT64_MAX;

                rlm->_timerHardDeadline = UINT64_MAX;

                __CFArmNextTimerInMode(rlm, rl);

            }

        }

        /* --- DISPATCHES  --- */

        else if (livePort == dispatchPort) {

            // 如果有dispatch到main_queue的block,执行block

            ···

            sourceHandledThisLoop = true;

            didDispatchPortLastTime = true;

        }

  


        /* --- SOURCE1S  --- */

        else {

            /// 如果一个 Source1 (基于port) 发出事件了,处理这个事件

            CFRUNLOOP_WAKEUP_FOR_SOURCE();

            cf_trace(KDEBUG_EVENT_CFRL_DID_WAKEUP_FOR_SOURCE, rl, rlm, 0, 0);

            // Despite the name, this works for windows handles as well

            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);

            if (rls) {

                mach_msg_header_t *reply = NULL;

                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;

                if (NULL != reply) {

                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);

                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);

                }

            } else {

                os_log_error(_CFOSLog(), "__CFRunLoopModeFindSourceForMachPort returned NULL for mode '%@' livePort: %u", rlm->_name, livePort);

            }

        }

        /* --- BLOCKS --- */

        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);

        /// 执行加入到Loop的block

__CFRunLoopDoBlocks(rl, rlm);

  


if (sourceHandledThisLoop && stopAfterHandle) {

        /// 进入loop时参数说处理完事件就返回。

    retVal = kCFRunLoopRunHandledSource;

        } else if (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)) {

        /// source/timer/observer一个都没有了

    retVal = kCFRunLoopRunFinished;

}

  


    } while (0 == retVal);

  


    if (timeout_timer) {

        dispatch_source_cancel(timeout_timer);

        dispatch_release(timeout_timer);

    }

    return retVal;

}

  


总结:

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

RunLoop & 线程

  • Cocoa和Core Foundation都提供了运行循环对象(RunLoop/NSRunLoopCFRunloop)来帮助配置和管理线程的运行循环

  • iOS 开发中能遇到两个线程对象: pthread_tNSThread。两者是一一对应的。CFRunLoop 是基于 pthread 来管理的。线程和RunLoop在Core Foundation中通过key-value的形式一一对应

  • 作为应用程序启动过程的一部分,应用程序框架自动在主线程上设置并运行运行循环。而子线程需要显式地运行它们的运行循环。

  • 线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。下述源码可以验证这一点

源码探索

  1. 我们从CFRunLoopGetMainCFRunLoopGetCurrent入手,可以看到其实现中都是调用了_CFRunLoopGet0函数,分别传入的参数是pthread_main_thread_np()(主RunLoop线程)和pthread_self()(当前线程)


CFRunLoopRef CFRunLoopGetMain(void) {

    ···

    static CFRunLoopRef __main = NULL;

    // 调用_CFRunLoopGet0,传入参数为0

    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np());

    return __main;

}

  


CFRunLoopRef CFRunLoopGetCurrent(void) {

    ···

    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);

    if (rl) return rl;

    // 调用_CFRunLoopGet0,传入参数为当前线程的线程ID

    return _CFRunLoopGet0(pthread_self());

}

  


// CFRunLoopGetMain调用_CFRunLoopGet0的参数解析

CF_EXPORT _CFThreadRef _CFMainPThread;

_CFThreadRef _CFMainPThread = kNilPthreadT;

#define kNilPthreadT  (_CFThreadRef)0

  


  1. _CFRunLoopGet0源码分析如下


CF_EXPORT CFRunLoopRef _CFRunLoopGet0(_CFThreadRef t) {

    if (pthread_equal(t, kNilPthreadT)) {

t = pthread_main_thread_np();

    }

  


    // static CFMutableDictionaryRef __CFRunLoops = NULL;

    // 如上所示,__CFRunLoops为一个全局的字典

    if (!__CFRunLoops) {

        // 若字典为空,则创建一个空字典并将主Runloop存入,key为线程指针

CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);

CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

        ···

CFRelease(dict);

}

CFRelease(mainLoop);

    }

  


    CFRunLoopRef newLoop = NULL;

    // 根据传入的线程ID从全局字典中获取runloop

    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

    if (!loop) {

    // 若runloop为空,则创建并存入全局字典

newLoop = __CFRunLoopCreate(t);

    ···

        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);

        loop = newLoop;

    }

    ···

    // 注:注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。

    _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);

  


    return loop;

}

  


总结:

  1. 先有线程,再有runLoop

  2. 子线程刚创建时并没有RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 会在第一次获取时创建,在线程结束时销毁。

  3. 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。

RunLoop & AutoReleasePool

App启动后,苹果在主线程创建了其关联的RunLoop,并在该RunLoop中注册两个Observer

  • 第一个Observer(优先级最高)监控的事件:Entry(即将进入Loop),回调内会调用_objc_autoreleasePoolPush()创建自动释放池

  • 第二个Observer(优先级最低)监控两个事件:

    - 第一个事件:BeforeWaiting(准备进入休眠),回调内会调用_objc_autoreleasePoolPop()释放旧的池并调用_objc_autoreleasePoolPush()创建新的池

    - 第二个事件:Exit(即将退出Loop),回调内会调用_objc_autoreleasePoolPop()销毁自动释放池

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,**每当一个运行循环结束的时候,它都会释放一次autorelease pool,同时pool中的所有自动释放类型变量都会被释放掉。 **所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

常见问题

Timer

  1. 【主线程问题】以scheduledTimerWithTimeInterval的方式开启的Timer,是以kCFRunLoopDefaultMode的mode添加到当前Runloop的,如下所示


CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer._timer!, kCFRunLoopDefaultMode)

所以在主线程开启的Timer会被手势事件影响。解决方案:将Timer添加到kCFRunLoopCommonModes 中,如下所示


NSTimer *timer = [NSTimer timerWithTimeInterval:1.0

                                         target:self

                                       selector:@selector(timerTick:)

                                       userInfo:nil

                                        repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:timer

                             forMode:NSRunLoopCommonModes];

  1. 【子线程问题】子线程默认不开启Runloop,所以如果在子线程添加timer会无法执行

解决方案一:

NSThread 思路:在子线程中将NSTimer以默认方式加到该线程的runloop中,启动子线程。


NSThread *t = [[NSThread alloc] initWithTarget:self

                                      selector:@selector(privatePrint)

                                        object:nil];

[t start];

  


- (void)privatePrint {

    [NSTimer scheduledTimerWithTimeInterval:3

                                    repeats:YES

                                      block:^(NSTimer * _Nonnull timer) {

        NSLog(@"--%@",[[NSRunLoop currentRunLoop] currentMode]);

        NSLog(@"--%@",[[NSRunLoop mainRunLoop] isEqual:[NSRunLoop currentRunLoop]] ? @"main" : @"not main");

    }];

    [[NSRunLoop currentRunLoop] run];

}

解决方案二:GCD


// 声明属性

@property(nonatomic, strong) dispatch_source_t timers;

  


- (void)runloopTimer {

    uint64_t interval = 0.01 * NSEC_PER_SEC;

    dispatch_queue_t queue = dispatch_queue_create("my queue", 0);

    _timers = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

    dispatch_source_set_timer(_timers,

                              dispatch_time(DISPATCH_TIME_NOW, 0),

                              interval,

                              0);

    __weak ViewController *blockSelf = self;

    

    dispatch_source_set_event_handler(_timers, ^() {

        [blockSelf addTime];

    });

    dispatch_resume(_timers);

}

  


- (void)addTime {

    _timeCount += 100;

    dispatch_async(dispatch_get_main_queue(), ^{

        // 回到主线程执行UI操作

    });

}


参考文档:

深入理解RunLoop