iOS进阶-详细介绍Runloop

参考:https://blog.ibireme.com/2015/05/18/runloop/

目录:

1、概念

2、作用

3、源码分析 得出 runlopp 和线程是 一一对应

4、Runloop的内部逻辑

5、工作流程

6、RunLoop 的底层实现

7、使用场景

1、概念

在传统情况下,一个线程从创建只能执行一次任务,执行完就会被销毁,如果我们想让一个线程从创建开始可以满足随时处理任务的需求,我们通常会在当前线程的外部加上一个循环,循环条件是 接收到停止通知才会被销毁,代码通常如下:

- (void) loop {

   do {
        var message = get_next_message();
        process_message(message);
      } 
      while (message != quit);
}

上面的实现方式,仅仅是一种事件循环,依然存在弊端就是线程会不断进行调用,销毁资源;参考nodejs的事件循环、windows的消息总线,iOS、os X 系统的 runloop ,他们的实际理念都是: 如何管理事件循环和消息循环的协调性,从而保证线程在没有任务需要执行的时候进入休眠等待状态,在消息来临时,立即被唤醒。

2、作用

Runloop这个对象的作用就是管理,对外提供一个入口函数,线程一旦执行完入口函数,就会一直出入当前入口函数内部,一直保持“接受消息->等待->处理”的循环状态 ,一直到外部传来停止消息结束循环,比如传入quit消息。

3、源码分析 得出 runlopp 和线程是 一一对应

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

我一直在解决要说明runloop和线程是一一对应的用NSRunLoop还是 CFRunLoopRef ,有与iOS本身的封闭性NSRunLoop源码看不到,而CFRunLoopRef 刚好是开源的 ,所以就通过分析CFRunLoopRef吧
注: CFRunLoopRef是位于CoreFoundation 框架,它提供了纯 C 函数的 API,
NSRunLoop位于Foundation框架,NSRunLoop是在CFRunLoopRef的基础之上封装的;

源码前需要了解:
CFRunLoopRef是基于p_thread管理的,p_thread和NSThread都是iOS线程对象,现在都是基于操作系统内核drawin层内核内层math thread 封装的性能查不到,只是p_thread是c的API,NSThread是oc的API.

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。我们看下他们的源码分析下线程和RunLoop的关系:

/// 全局的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());
}

从上面的源码可以看出,当我们调用CFRunLoopGetMain或者CFRunLoopGetCurrent获取runloop对象他们的走的都是一个函数_CFRunLoopGet,_CFRunLoopGet函数内部可以看出 线程和对应的runloop对象存放在一个可变字典里面,首先如果是第一次进入存放容器为空,它会创建一个runloop,并且将主线程为key,runloop为value放进字典内部,如果不是第一次进入会判断字典里是否存在当前线程对应的runloop,有的话直接返回出去,没有的话跟主线程对应runloop创建方式一样创建完加入字段内部,然后返回对应的loop对象;
从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

4、Runloop的内部逻辑

2017-03-31-RunLoop结构.png

这一张图我们可以看出 RunLoop 的组织结构关系。

RunLoop : thread = 1:1
RunLoop : RunLoopMode = 1:n
RunLoopMode : CFRunLoop* = 1:n

下面对它的内部结构进行说明:

CFRunLoopRef :

typedef struct __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; // 字符串,记录所有标记为common的mode
    CFMutableSetRef _commonModeItems; // 所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes; // CFRunLoopModeRef set
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};
  1. CFRunLoopRef 指向 __CFRunLoop
  2. CFRunLoop 里面包含了线程,若干个 mode。 每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
  3. CFRunLoop 和线程是一一对应的。
  4. _blocks_head 是 perform block 加入到里面的

CFRunLoopModeRef :

// 定义 CFRunLoopModeRef 为指向 __CFRunLoopMode 结构体的指针
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0; // source0 set ,非基于Port的,接收点击事件,触摸事件等APP 内部事件
    CFMutableSetRef _sources1; // source1 set,基于Port的,通过内核和其他线程通信,接收,分发系统事件
    CFMutableArrayRef _observers; // observer 数组
    CFMutableArrayRef _timers; // timer 数组
    CFMutableDictionaryRef _portToV1SourceMap;// source1 对应的端口号
    __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. CFRunLoopModeRef 指向 __CFRunLoopMode
  2. CFRunLoopModeRef 内部包含 source0(非基于port,主要是接受点击事件,触摸事件等APP内部事件)、source1 (基于post,主要接受、处理系统事件)、

RunLoopSource: 分为source0 source1

source0 是非基于 port 的事件,主要是 APP 内部事件,如点击事件,触摸事件等。
source1 是基于Port的,通过内核和其他线程通信,接收,分发系统事件。
CFRunLoopSource 里面包含一个 _runLoops,也就意味着一个 CFRunLoopSource 可以被添加到多个 runloop mode 中去。

RunLoopObserver: 观察runloop的状态,监听的状态如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即将进入run loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
    kCFRunLoopBeforeSources = (1UL << 2), //即将处理source
    kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), //被唤醒但是还没开始处理事件
    kCFRunLoopExit = (1UL << 7), //run loop已经退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

RunLoopTimer:

CFRunLoopTimer 是定时器,可以在设定的时间点抛出回调
CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互转换。

5、工作流程

image.png

cong shang

从上图可以看出:

RunLoop 从两个不同的事件源中接收消息: 一类是timer事件;一类是input source事件,
input source分3小类,第一类runloop对外提供input source ,输入源包括两种一种是底层系统基于port事件,第二类输入源是非基于port事件,主要是app内部的点击、触摸事件,第三类是自定义事件;
观察者监控source 是否需要有需要执行的任务,除此之外,还可以用来监控runloop的事件,监控事件如下:
1.The entrance to the run loop. // runloop进入

  1. When the run loop is about to process a timer. // runloop 即将处理定时器
  2. When the run loop is about to process an input source. // runloop即将处理输入源
  3. When the run loop is about to go to sleep. // runloop 进入休眠
  4. When the run loop has woken up, but before it has processed the event that woke it up. // runloop被唤醒
  5. The exit from the run loop. //runloop退出

6、

7、使用场景

RunLoop 与 GCD

RunLoop 与 GCD 是互相协作的关系,RunLoop 的最开始部分使用了 GCD 的 timer 做超时的回调;通过 GCD 调用带有 RunLoop 的线程的 block,会通过 dispatch port CFRunLoopServiceMachPort 把事件发送到该线程的 RunLoop 里面。

比如:

|

<pre style="margin: 0px; padding: 0px; border: none; outline: 0px; font-weight: inherit; font-style: inherit; font-family: "Source Code Pro", Consolas, Monaco, Menlo, Consolas, monospace; font-size: 14px; vertical-align: baseline; background: rgb(39, 40, 34); overflow: auto; color: rgb(248, 248, 242); line-height: 22.4px;">dispatch_async(dispatch_get_main_queue(), ^{});
</pre>

|

主线程存在 runloop,那么 GCD 会通过 dispatch port CFRunLoopServiceMachPort,把事件发送给 RunLoop,RunLoop 接收到时间之后,会执行这个 block。

NSTimer 与 GCD Timer

NSTimer 是通过 RunLoop 的 RunLoopTimer 把时间加入到 RunLoopMode 里面。官方文档里面也有说 CFRunLoopTimer 和 NSTimer 是可以互相转换的。由于 NSTimer 的这种机制,因此 NSTimer 的执行必须依赖于 RunLoop,如果没有 RunLoop,NSTimer 是不会执行的。

GCD 则不同,GCD 的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。

至于这两个 Timer 的准确性问题,如果不再 RunLoop 的线程里面执行,那么只能使用 GCD Timer,由于 GCD Timer 是基于 MKTimer(mach kernel timer),已经很底层了,因此是很准确的。

如果在 RunLoop 的线程里面执行,由于 GCD Timer 和 NSTimer 都是通过 port 发送消息的机制来触发 RunLoop 的,因此准确性差别应该不是很大。如果线程 RunLoop 阻塞了,不管是 GCD Timer 还是 NSTimer 都会存在延迟问题。

应用

  • 异步的回调如果存在延时操作,那么就要放到有 RunLoop 的线程里面,否则回调没有着陆点无法执行
  • NSTimer 必须得在有 RunLoop 的线程里面才能执行,另外,使用 NSTimer 的时候会出现滑动 TableView,Timer 停止的问题,是由于 RunLoopMode 切换的问题,只要把 NSTimer 加到 common mode 就好了。
  • 滚动过程中延迟加载,可以利用滚动时 RunLoopMode 切换到 NSEventTrackingRunLoopMode 模式下这个机制,在 Default mode 下添加加载图片的方法,在滚动时就不会触发。
  • 崩溃后处理 DSSignalHandlerDemo
全部评论

相关推荐

点赞 评论 收藏
分享
10-21 23:48
蚌埠坦克学院
csgq:可能没hc了 昨天一面完秒挂
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务