iOS RunLoop由浅入深

Event Loop

Event Loop事件循环机制,如javascript的事件循环,以及依赖其的nodejs都是采用的异步事件循环机制。

对于上述两者,都是基于多线程,但是都是单线程执行任务代码,其依赖的就是Event Loop事件循环机制,通过事件队列注册事件及事件的观察者,事件的执行交由其他线程去执行(如I/O操作,网络请求等),nodejs采用的是libuv异步I/O线程池库;对于非异步I/O操作,如setTimeOut setInterval等,都是基于事件循环查询(每次事件处理完成后进入下一次事件循环时都会查看时间是否已到达,并且是任务是插入到任务队列尾部,因此存在误差,不过也可采用process.netxTick会将事件插入到事件循环前解析执行,且可嵌套执行);

image.png

image.png

  • 定时器:本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
  • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检测setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)

JavaScript单线程异步的背后——事件循环机制

《深入浅出Nodejs》

Nodejs Event Loop

JavaScript 运行机制详解:再谈Event Loop

Mach Port

这个在《unix进程间通信》中已阐述,在这不过多阐述;

RunLoop

RunLoop就像其名字一样,就是运行循环,核心就是事件循环+mach port,利用事件循环注册观察相应的事件,若无事件处理,线程就去睡眠等待内核事件触发或者通过手动唤醒,不停地循环处理各种事件(如timer source0 source1事件以及dispatch分发的func block等);

注:以下代码分析基于CF-1151.16源码;

runloop与线程的关系

直接上结论:runloop与线程是一一对应的,且对于主线程是默认开启的,对于其他线程,需要通过手动开启,且只能通过苹果对外的接口获取线程相应的CFRunLoopRef对象:

CFRunLoopRef CFRunLoopGetMain(void);
CFRunLoopRef CFRunLoopGetCurrent(void);

原理就是:苹果维护了一个全局的字典对象,若字典中不存在线程对应的runloop对象就会创建并赋值,并且还利用线程私有数据(数组)存储了指定__CFTSDKeyRunLoop当前线程的CFRunLoopRef对象(同时也关联了runloop对象销毁的回调,用于线程退出销毁);策略是:优先从线程私有数据数组中获取,若获取不到就从全局字典对象中获取,若无则去创建;

runloop对象结构分析

CoreFoundation中关于runloop的五个类:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中CFRunLoopRef对象结构体如下:

//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;//绑定的线程pthread_t
    uint32_t _winthread;
    CFMutableSetRef _commonModes;//通用的mode set集合
    CFMutableSetRef _commonModeItems;//通用mode的itme集合
    CFRunLoopModeRef _currentMode;//当前mode
    CFMutableSetRef _modes;//所有的mode
    struct _block_item *_blocks_head;//添加的block任务,与dispatch分发的block处理不同
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

runloop对象的结构体为__CFRunLoop,其存储着runloop相关的锁保证线程安全(注意下NSRunLoop不是线程安全的),唤醒端口(用于CFRunLoopWakeUp外部接口调用,主要是source0),绑定的线程,mode各种集合(下面会重点阐述),block处理任务(通过CFRunLoopPerformBlock接口注册的)以及记录需要的相关信息(如运行时间_runTime _sleepTime)等;

CFRunLoopModeRef

image.png

CFRunLoopRef对象中包含了若干ModeMode对象的数据结构如下:

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;//定时器
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;//需要监听的所有mach port集合
    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
    //mk_timer由mach port实现<https://opensource.apple.com/source/xnu/xnu-3789.51.2/osfmk/kern/mk_timer.c>
    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 */
};

CFRunLoopModeRef对象未对外暴露,可通过CFRunLoopCopyAllModesCFRunLoopCopyCurrentMode获取所有runloop相关的Mode,通过CFRunLoopAddCommonMode添加Mode

runloop可添加多个Mode,但只能指定一个Mode模式运行(默认是kCFRunLoopDefaultMode),且需要退出当前重新指定运行才能生效;每个Mode中可添加若干sourcetimerobserver

对于CoreFoundation中的CFRunLoop苹果只提供了两种默认模式kCFRunLoopDefaultModekCFRunLoopCommonModes其中kCFRunLoopCommonModes只是操作common标记的字符串,用于向所有现有的Modes中添加相应的观察者,不是一种具体的Mode,不能直接用于CFRunLoopRunInMode调用运行;但上层NSRunLoopMode封装了一些相应的Mode,如

  • NSDefaultRunLoopModekCFRunLoopDefaultMode默认的模式
  • NSEventTrackingRunLoopMode模态跟踪事件时,例如鼠标拖动循环,应将运行循环设置为此模式;
  • NSModalPanelRunLoopMode运行等待模态窗口(如NSSavePanel NSOpenPanel)的输入时指定;
  • UITrackingRunLoopMode运行控件追踪时指定,如UIScrollView滑动时(这个系统会默认自动切换到此模式)

iOS应用启动时系统默认注册了5个Mode:

  • kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode下运行的;
  • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响;
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用;
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到;
  • kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用;

CFRunLoopSourceRef

CFRunLoopSourceRef对象的数据结构如下:

struct __CFRunLoopSource
{
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order; /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;  /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;
};

typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    mach_port_t (*getPort)(void *info);
    void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
} CFRunLoopSourceContext1;

其中联合体union中的versionx.version字段信息用于区分source0或者source1

  • source0,结构体中context上下文中包含了各种回调函数,主要是perform回调函数(用于执行添加到source0中的任务),当调用CFRunLoopSourceSignal时会标记__CFRunLoopSource中的_bits标记位,然后调用CFRunLoopWakeUp来唤醒runloop再下一个循环中处理此回调;

    主要用于APP内部事件,由APP负责管理触发,如UIEvent事件;

  • source1,不同于source0执行回调函数,source1还需要指定mach port,用于监听系统内核事件或其他线程发来的事件;

CFRunLoopTimer

struct __CFRunLoopTimer
{
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;        /* immutable */
    CFTimeInterval _tolerance;       /* mutable */
    uint64_t _fireTSR;               /* TSR units */
    CFIndex _order;                  /* immutable */
    CFRunLoopTimerCallBack _callout; /* immutable */
    CFRunLoopTimerContext _context;  /* immutable, except invalidation */
};

CFRunLoopTimer结构中包含了时间相关的变量,runlooptimer事件触发都会去检查当前所有的timer时间点是否达到,若达到则处理事件任务;具体触发时间事件主要包含两种mk_timerdispatch source形式,两者都是基于mach port但是触发runloop并处理回调的处理方式不同;

mk_timer是通过__CFRunLoopDoTimers来处理,依赖于runloop来触发时间回调函数,因此基于此的NStimerperformSelector:withObject:afterDelay:(是对NSTimer的包装),都需要runloop运行;

dispatch source(针对主队列,其他队列不是通过runloop来触发)是通过__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__中指定的外部函数_dispatch_main_queue_callback_4CF来处理,调用堆栈如下

image.png

dispatch_after指定主队列的时间任务,是对dispatch_source的包装,对于存在UI的应用默认主队列的runloop是开启的,若是其他工具类应用,则需要手动开启主队列runloop,否则指定主队列的dispatch source是无法生效的;

Timer有两种实现方式分别是MK_Timer和GCD Timer,在runloop中Timer被转为了一个存了触发时间的列表,这个触发时间是一个绝对时间,会按时间大小升序排序,在最小的时间被触发后,Runloop会更新列表保证时间始终是升序排列。如果Runloop在某次运行中阻塞了很长时间,Timer的触发会受到影响。过期的时间点会被移除而不会去触发。

具体的NSTimerGCD Timer实现剖析可参考从NSTimer的失效性谈起(二):关于GCD Timer和libdispatch

不过在源码中的USE_DISPATCH_SOURCE_FOR_TIMERS未生效,暂时未搞清问题,待后续补充;

CFRunLoopObserver

struct __CFRunLoopObserver
{
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;          /* immutable */
    CFIndex _order;                     /* immutable */
    CFRunLoopObserverCallBack _callout; /* immutable */
    CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};

CFRunLoopObserver观察者对象指定了runloop相应状态变化(_activities指定需要观察的类型)及状态变化的回调指针_callout,具体观察的选项包括:

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
};

RunLoop事件循环

image.png

image.png

runloop事件循环的逻辑如上图,对于黄色的Block是通过CFRunLoopPerformBlock添加的block任务,若runloop任务处理完成后就会休眠等待source1timer、或者手动被唤醒来继续下一次循环处理任务;

RunLoop实践

AutoReleasePool

主要是利用runloop observer观察注册的事件:kCFRunLoopEntrykCFRunLoopBeforeWaitingkCFRunLoopExit,分别用于autoreleasepool push/pop操作来创建/释放内存池,并且保证自动内存池创建优先级其他回调之前,释放内存池在其他回调之后,进而不会导致内存泄露;

事件响应/手势识别

对于IOKit.framework生成的IOHIDEvent(如触摸/锁屏/静音/传感器加速等)会发送给SpringBoard接收,并通过mach port发送给注册了相应端口的source1应用进程,进而触发事件回调__IOHIDEventSystemClientQueueCallback,并通过_UIApplicationHandleEventQueue内部注册source0事件进行事件应用内部分发;

手势识别就是将上面识别的手势UIGestureRecognizer标记为待处理,并注册了observer监测BeforeWaiting事件,触发回调来处理待处理的手势;

界面更新

苹果注册了observer监测BeforeWaiting/Eixt事件,当事件发生时会将已提交到全局容器待界面绘制的任务执行并更新UI,如果中间执行大量逻辑计算的任务导致runloop迟迟不触发ui更新的话,就会导致绘制ui的帧被丢弃即“丢帧”,进而引发ui卡顿,FaceBook推出的开源项目AsyncDisplayKit 就是防止主线程存在大量与ui不相关的任务处理(通过后台线程处理)阻塞ui更新,来避免“丢帧”提升界面流畅度;

GCD

对于提交至主队列的任务,如dispatch_source timerdispatch_async,都是主队列runloop中监听相对应的mach port事件,当事件发生时(timer到期或dispatch_async添加到主队列任务),libdispatch就会通过mach port端口向监听该端口的runloop发送唤醒消息,被唤醒的runloop触发回调函数__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__,该回调函数内部的_dispatch_main_queue_callback_4CF实际是由libdispatch定义处理的,即处理相应的任务;

网络请求

对于NSURLConnection实现原理如下图,具体为:

image.png

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

AFNetworking使用的常驻线程用于后台接收delegate回调,当有任务需要处理时,通过performSelector:onThread:将任务提交给该线程的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;
}

最重要的就是在runloop中添加了的未接收任何消息的NSMachPort进去,防止runloop退出进而线程退出;

小知识

宏定义 do{ }while(0)

  • 帮助定义复杂的宏以避免错误
#define DO_SOMETHING() foo1();foo2();

if (a>0)
  DO_SOMETHING();
//展开后如下
if (a>0)
  foo1();
  foo2();
  • 避免使用goto跳转

    int foo() {
      if (error1) {
          do_something();
          goto END:
      }
      if (error2) {
          do_something2();
          goto END;
      }
    END:
      xxx;
    }
    
    //使用do{}while(0)
    int foo() {
      do {
          if (error1) 
              do_something();
      if (error2) 
          do_something2();
      } while(0)
      
      xxx;
    }
    
  • 控制代码块

  • 避免由空宏定义造成的警告

    内核中由于不同架构的限制,很多时候会用到空宏,。在编译的时候,这些空宏会给出warning,为了避免这样的warning,我们可以使用do{...}while(0)来定义空宏: #define EMPTYMICRO do{}while(0); 这种情况不太常见,因为有很多编译器,已经支持空宏。

do{...}while(0)的妙用

CHECK_FOR_FORK()宏定义用途

主要对于非移动端平台,如Mac OSX,进程调用fork生成子进程,一般是直接调用exec或类似的函数执行新的程序,而对于依赖Core Founadtion / Cocoa / Core Data 框架的应用,必须调用 exec 函数,否则这些框架也许不能正确的工作。

Warning: When launching separate processes using the fork function, you must always follow a call to fork with a call to exec or a similar function. Applications that depend on the Core Foundation, Cocoa, or Core Data frameworks (either explicitly or implicitly) must make a subsequent call to an exec function or those frameworks may behave improperly.

-- 摘自Threading Programming Guide

理解:应该是避免进程使用vfork系统调用继续使用父进程的数据,导致影响父进程,因此要求立即调用exec去执行新的程序;

Reference

RunLoop --- CHECK_FOR_FORK()

RunLoop 源码阅读

Dispatch Sources

深入浅出 GCD 之 dispatch_queue

Threading Programming Guide -- Runloop

iOS刨根问底-深入理解RunLoop

CFRunLoopRef

深入理解RunLoop

iOS线下分享《RunLoop》by 孙源@sunnyxx

demo

https://github.com/FengyunSky/notes/blob/master/local/code/runloop.tar

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

推荐阅读更多精彩内容

  • Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。 RunLoop的概念 -般说,一个线程一...
    小猫仔阅读 971评论 0 1
  • 转自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飘金阅读 969评论 0 4
  • 原文地址:http://blog.ibireme.com/2015/05/18/runloop/ RunLoop ...
    大饼炒鸡蛋阅读 1,138评论 0 6
  • RunLoop 的概念 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线...
    Mirsiter_魏阅读 612评论 0 2
  • 宝妈创业好项目,品牌童装童品母婴用品,一件代发! 招代理招加盟一手货源一件代发,支持退换零囤货,零风险,售后服务质...
    美丽心情88阅读 115评论 0 0