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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

  • ======================= 前言 RunLoop 是 iOS 和 OSX 开发中非常基础的一个...
    i憬铭阅读 861评论 0 4
  • Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。 RunLoop的概念 -般说,一个线程一...
    小猫仔阅读 973评论 0 1
  • 转自bireme,原地址:https://blog.ibireme.com/2015/05/18/runloop/...
    乜_啊_阅读 1,299评论 0 5
  • 转载:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling阅读 1,430评论 0 13
  • RunLoop 的概念 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线...
    Mirsiter_魏阅读 612评论 0 2