RunLoop总结

RunLoop,从字面意思看叫做运行循环。RunLoop基本作用:

  • 保持程序的持续运行
  • 处理APP中的各种事件(触摸事件,定时器事件...)
  • 节省CPU资源,提高程序性能:有事做事,没事休息
    等等......

RunLoop内部其实就是一个do-while循环,在这个循环内部不断的处理各种事件,因此保证了程序不会退出一直运行;

runloop示意图

程序的main函数中,UIApplicationMain函数内部自动启动了一个RunLoop,所以UIApplicationMain一直没有返回,保持程序持续运行。这个默认启动的RunLoop是跟主线程相关联的;

RunLoop对象:

iOS中有2套API访问和使用RunLoop:

  • Foundation框架下的:NSRunLoop
  • CoreFoundation框架下的:CFRunLoopRef;
    CFRunLoopRef和NSRunLoop都代表RunLoop对象;NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层的API;

获取RunLoop对象

// Foundation:
[NSRunLoop currentRunLoop];// 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
//Core Foundation:
CFRunLoopGetCurrent();// 获得当前线程的RunLoop对象
CFRunLoopGetMain();// 获得主线程的RunLoop对象

获取RunLoop的源码如下

// 存放Runloop的全局变量__CFRunLoops,key是pthread_t,value是Runloop
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) { // t为0或为空表示主线程
    t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    //__CFRunLoops为空,则初始化__CFRunLoops,并创建主线程的Runloop,保存到__CFRunLoops中
    if (!__CFRunLoops) { 
        __CFSpinUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    // 通过pthread_t获取loop,如果没获取到,则新建一个loop保存到__CFRunLoops中,返回该loop
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        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;
}

可参考苹果官方文档:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html
苹果开源代码:
http://opensource.apple.com/source/CF/

CFRunLoopRef

源码是这样定义CFRunLoopRef的:

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __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; // runloop对应的线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes; //被标记为commonMode的模式
    CFMutableSetRef _commonModeItems;//添加到commonMode的item
    CFRunLoopModeRef _currentMode; //Runloop当前的运行的模式
    CFMutableSetRef _modes; 
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

从代码中可以看出Runloop主要包含线程pthread_t和模式CFRunLoopModeRef。_commonModes存放已被标记为“common”的mode。主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,这两个 Mode 都已经被标记为”Common”属性。_commonModeItem是哪些被添加到commonMode的source/timer/observer。

RunLoop与线程的关系
  • 每条线程都有唯一一个与之对应的RunLoop对象;
  • Runloop保存在一个全局的dictionary中,线程为key,Runloop为value;
  • 主线程的RunLoop程序启动后自动创建好了,而子线程的RunLoop在第一次获取时创建;
  • RunLoop在线程结束时销毁;
RunLoop与RunLoopMode的关系
  • RunLoopMode代表RunLoop的运行模式,每个RunLoop包含若干个Mode
  • 每次RunLoop启动时,只能指定其中一个Mode,这个Mode称作CurrentMode,Runloop循环处理CurrentMode的所有事件;
  • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入;这样可以分隔开不同mode的source、timer、observer,互不影响;
  • 如果Mode里没有任何source、timer、observer,Runloop立刻退出;
CFRunLoopModeRef(模式)

源码中对CFRunLoopModeRef的定义:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ....
};

从源码中可以看到每个Mode包含若干个source0、source1、timer、observer;

系统默认注册了5个Mode:

  • KCFRunLoopDefaultMode:APP的默认Mode,通常主线程在这个Mode下运行;

  • UITrackingRunLoopMode:界面跟踪Mode,用于scrollView追踪触摸滑动,保证界面滑动不受其他Mode影响;

  • UIInitializationRunLoopMode:在刚启动APP时进入的第一个Mode,启动完成后就不再使用;

  • GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到;

  • KCFRunloopCommonModes:这是一个占位用的Mode,不是真正的Mode;事件被标记为KCFRunloopCommonMode后,Runloop运行在UITrackingRunLoopMode和kCFRunLoopDefaultMode,事件都能执行;

CFRunLoopSourceRef(事件源)

Source0:触摸事件、performSelector:onThread:
Source1:基于Port的线程间通信、系统事件捕捉,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

RunLoopTimer
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 */
};

RunLoopTimer是基于时间的触发器,包含一个时间长度和回调。NSTimer注册到Runloop后,Runloop会为重复的时间点注册好事件,时间间隔_interval,时间点到时Runloop会被唤醒执行回调;Runloop为了节省资源,不会准确按照时间点回调timer,_tolerance表示到了时间点后允许的最大误差。如果某个时间点被错过了,则这个时间点的回调会跳过,不会延后执行。

NSTimer的处理,会受到RunLoop的Mode影响。举个例子:

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];   
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];

上面这样创建的处理的time只能在NSDefaultRunLoopMode下工作,当滚动页面时,Runloop切换到UITrackingRunLoopMode,timer失效;若想在NSDefaultRunLoopMode和UITrackingRunLoopMode模式下,timer都有效,应将timer事件添加到KCFRunloopCommonModes;如下:

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];   
[[NSRunLoop currentRunLoop]addTimer:timer forMode: KCFRunloopCommonModes];
RunLoopObserver
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 */
};

RunLoopObserver是用来观察Runloop的状态的。当Runloop状态发生改变,观察者能通过回调接收到这个变化。可检测到Runloop以下状态:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Runloop的运行逻辑
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

从源码可以看到,Runloop做着do...while循环,CFRunLoopRunSpecific函数返回值为kCFRunLoopRunStopped或kCFRunLoopRunFinished时退出循环。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
        // 通知observer即将进入Runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // Runloop循环处理事件,__CFRunLoopRun函数只有在被打断或没有source/timer/observer时,才会返回
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知observer,Runloop即将退出
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    return result;
}

进入CFRunLoopRunSpecific函数,说明Runloop将要开始工作了,于是通知observer即将进入Runloop。然后调用__CFRunLoopRun函数循环处理事件,函数返回,通知observer,Runloop即将退出。直到Runloop被中断或事件处理完毕,整个Runloop循环结束;

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
          //   通知 Observers: RunLoop 即将触发 Timer 回调。
          if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
          // 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
          if (rlm->_observerMask & kCFRunLoopBeforeSources)   __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
          // 执行被加入的block
          __CFRunLoopDoBlocks(rl, rlm);
          //  RunLoop 触发 Source0 (非port) 回调。
          Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

          if (sourceHandledThisLoop) {
            // 执行被加入的block
              __CFRunLoopDoBlocks(rl, rlm);
          }
          // 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
          if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
              goto handle_msg;
          }

          // 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
          __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
          __CFRunLoopSetSleeping(rl);
        /*
        调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒
        • 一个基于 port 的Source 的事件。
        • 一个 Timer 到时间了
        • RunLoop 自身的超时时间到了
        • 被其他什么调用者手动唤醒
        */
          __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);

          // 通知 Observers: RunLoop 的线程刚刚被唤醒了。
          __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

          // 收到消息,处理消息。
          handle_msg:;
          __CFRunLoopSetIgnoreWakeUps(rl);

          //   Timer 到时间了,触发Timer的回调。
          if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
              CFRUNLOOP_WAKEUP_FOR_TIMER();
              __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) 
          }
          //有dispatch回到main_queue的block,执行block。
          else if (livePort == dispatchPort) {
              CFRUNLOOP_WAKEUP_FOR_DISPATCH();
              __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
          } else {
          //  Source1 (基于port) 发出事件,处理这个事件
              CFRUNLOOP_WAKEUP_FOR_SOURCE();
              // Despite the name, this works for windows handles as well
              __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
          }
      }
      // 执行加入到Loop的block        
      __CFRunLoopDoBlocks(rl, rlm);
        
      if (sourceHandledThisLoop && stopAfterHandle) {
          // 进入loop时参数说处理完事件就返回。
          retVal = kCFRunLoopRunHandledSource;
       } else if (timeout_context->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);

    return retVal;
}

__CFRunLoopRun函数是进入Runloop后的处理逻辑。

RunLoop整个运行过程如下:

  • 1.通知Observer,将要进入RunLoop

  • 2.通知Observer,将要处理timer

  • 3.通知Observer,即将触发非基于端口的输入源(source0事件)。

  • 4.处理block

  • 5.触发的source0事件(可能会再次处理)

  • 6.如果存在source1,调转到第8步

  • 7.通知Observer开始休眠

  • 8.通知Observer结束休眠
    1、处理timer
    2、处理GCD Async To Main Queue
    3、处理source1

  • 9.处理Blocks

  • 10.根据前面执行的结果,决定如何操作
    1、回到第2步
    2、退出RunLoop

  • 11.通知Observer退出RunLoop

  • 9.处理待处理事件。
    如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步。
    如果输入源被触发,则传递事件。
    如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。

  • 10.通知观察者运行循环已退出。

参考博客:
深入理解RunLoop

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

推荐阅读更多精彩内容

  • 前言 最近离职了,可以尽情熬夜写点总结,不用担心第二天上班爽并蛋疼着,这篇的主角 RunLoop 一座大山,涵盖的...
    zerocc2014阅读 12,368评论 13 67
  • 参考资料:ibireme :http://blog.ibireme.com/2015/05/18/runloop/...
    LiYaoPeng阅读 12,912评论 2 19
  • RunLoop简介 RunLoop 是一个运行循环,内部类似do-while循环,线程进入并使用它来运行响应输入事...
    伶俐ll阅读 378评论 0 4
  • 发现自己对于找女盆友的真实想法,,其实一直对找女朋友没兴趣的,只是因为这段时间的空白让我觉得遗憾,然后才稍微的想找...
    我有个帅哥梦阅读 169评论 0 0
  • 漫天的迷雾遮住了我的双眼, 眺望你要离去的身影,坚决,狠心。 你的离去,我的眼里有些许的迷离,差一点点潸然泪下,我...
    心是甜甜的阅读 347评论 0 1