iOS RunLoop漫谈

作者: Neebel | 来源:发表于2016-12-04 13:15 被阅读0次

在iOS开发或者面试中,我们多多少少会听到或者用到RunLoop的相关知识,彻底吃透RunLoop这一块内容,对我们技术能力的提升大有裨益。最近看了一些RunLoop的文章和代码,作为总结写了这篇文章。本篇文章将会从RunLoop的原理和应用两个方面剖析RunLoop,帮助大家更好地理解、应用RunLoop。本篇文章主要内容:

  • RunLoop的概念
  • RunLoop存在的意义
  • RunLoop的构成
  • RunLoop的应用

RunLoop的概念

通常情况下,线程执行完成某个任务后,线程就会退出;但某些情况下,我们需要线程常驻并且随时被唤醒以执行其他任务。线程执行一次就退出和循环多次执行可以用图形表示为:

runloop0.png

线程循环多次执行用伪代码可以简单表示为:

do {
   //获取消息
   //处理消息
} while (消息 != 退出)

上面的这种循环模型被称作 Event Loop,事件循环模型在众多系统里都有实现,具体到iOS和OSX中就是我们要介绍的RunLoop。当然RunLoop的实现绝不会是一个简单的while循环所能搞定的,其内部实现会复杂很多,在后面的内容中会详细介绍。

RunLoop存在的意义

RunLoop的官方定义为:

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

从中我们可以得知RunLoop的主要目的是:当有任务到来时,可以立马唤醒线程让其处理任务,没有任务时保持线程休眠。RunLoop的机制既能有效减少系统资源的浪费,又能使线程随时待命,不至于执行完成后就退出。

RunLoop的构成

在iOS和OSX中,与RunLoop对应的两种对象是NSRunLoopCFRunLoopRef。两者的关系如下图所示:

runloop1.png

CFRunLoopRef是开源的,所以我们研究其源码是最合适不过的。CFRunloopRef整体结构组成如下:

runloop2.png

先通过源码看下RunLoop和线程的关系:

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();//如果线程t为空,将其赋值为主线程
    }
    __CFSpinLock(&loopsLock);//加锁,保证多线程访问的安全性
    if (!__CFRunLoops) {
        __CFSpinUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//如果维护线程和runloop关系的字典不存在,就新建一个
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//主线程的runloop总会被默认创建,且创建一次
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);//将主线程和主runloop存放到dict中维护
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {//比较、交换dict和__CFRunLoops,也就是说dict是临时字典,实际上是由__CFRunLoops全局字典维护线程和runloop的关系
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));//在全局字典__CFRunLoops查找线程t对应的runloop
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);//如果没找到就新建一个
        __CFSpinLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//把新建的runloop和线程t存放到__CFRunLoops字典中
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;//返回线程t对应的runloop
}

从代码可以看出:线程和RunLoop是一一对应的关系,这种对应关系维护在一个全局的字典中,主线程的RunLoop会被系统自动创建,其他线程的RunLoop在第一次获取的时候才会创建,不获取的话其RunLoop是不存在的。

一个CFRunLoop包含多个CFRunLoopMode,一个CFRunLoopMode包含多个CFRunLoopSourceCFRunLoopTimerCFRunLoopObserver。RunLoop在任何时刻只能以一种Mode运行(叫做currentMode),切换Mode时要先退出当前RunLoop。下面我们从源码具体解析:

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;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode; //RunLoop当前运行的Mode
    CFMutableSetRef _modes;        //RunLoop包含的所有Mode
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};RunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

代码中可以看出:

(1)所有的Mode是被RunLoop的一个集合维护的

(2)当前正在运行的Mode就是currentMode

(3)一个普通的Mode可以被标记为commonMode,所有的commonMode也是被RunLoop的一个集合维护的

(4)commonMode里面的Source、Timer、Observer都叫做commonModeItem,所有的commonModeItem也是被RunLoop的一个集合维护的,不同的commonMode之间是可以同步、共享commonModeItem信息的。

(5)苹果为我们提供了两种Mode:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode

CFRunLoopMode结构体:

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;//计时器timer的数组
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    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
    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 */
};

代码中可以看出:
(1)CFRunLoopMode中维护一个管理多个事件源的集合,事件源又可以分为Soure0Source1
Source0: 基于非mach_port的,例如Custom Input Sources 、CFSocket等。
Source1: 基于mach_port的,能够主动触发RunLoop的线程处理消息,并且执行相应的回调。

(2)CFRunLoopMode中维护一个管理多个CFRunLoopTimerRef的数组
CFRunLoopTimerRef:包含一个时间长度和一个回调,在Timer被注册到RunLoop的时刻,再经过这个时间长度后,就会执行这个回调,相当于定时器的概念。

(3)CFRunLoopMode中维护一个管理多个CFRunLoopObserverRef的数组。
CFRunLoopObserverRef:可以理解为实时观察RunLoop的执行状态,在重要的时间点向外报告也就是执行相应的回调,这些重要的时间点包括:

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

关于RunLoop的具体执行流程,可以参照ibireme博客上的流程图帮助理解:

runloop3.png

RunLoop的应用

AutoreleasePool的实现

runloop4.png

主线程上执行的代码被AutoreleasePool包围着,不会出现内存泄漏的情况,所以也不用我们手动创建释放池了。

NSTimer

NSTimer可以看做是CFRunLoopTimerRef的封装,当一个NSTimer被添加到RunLoop后,经过一段时间后会执行相应的回调,具体执行一次还是重复多次执行,由repeats参数决定,具体使用方法简单,这里就不贴代码了

UI刷新

当界面元素的位置、大小发生变化,或者做连续几个动画时,这些变化不会立马体现在界面上,而是暂时保存这些变化,等到RunLoop休眠或者即将退出时,再去执行相应的回调,回调中的代码用于更新界面。

GCD用到RunLoop的情况

GCD中,子线程通过dispatch_async提交任务到主线程时,主线程的RunLoop会收到消息,激活后通过回调处理任务,但这仅限于提交到主线程的任务,如果提交任务到子线程,则还是由GCD本身处理的,就与RunLoop无关了。

参考链接

深入理解RunLoop
Run Loops

相关文章

网友评论

    本文标题:iOS RunLoop漫谈

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