RunLoop之底层探究

概述

RunLoop顾名思义就是运行循环,来保证程序一直处于程序运行状态。

在iOS中,RunLoop有很多应用,比如:

  • 定时器(Timer)、PerformSelector
  • GCD、Async Main Quene
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • AutoreleasePool

这些技术底层都会用到RunLoop。

在iOS项目中main函数中都会这样写道:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

UIApplicationMain()函数中内部就会为主线程创建一个RunLoop。对于iOS程序,RunLoop也是非常重要的,比如:

  • 保持程序的持续运行
  • 处理App中各种事件(比如触摸事件、定时器事件)
  • 节省CPU资源,提高程序性能:该处理事件时唤醒,不处理时wait。

RunLoop

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

  • Foundation: NSRunLoop
  • Core Foundation: CFRunLoopRef

NSRunLoop是基于CFRunLoopRef的一层OC包装

//获取当前线程的RunLoop对象
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
//获取主线程的RunLoop对象
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];

//获取当前线程的RunLoop对象
CFRunLoopGetCurrent();
//获取主线程的RunLoop对象
CFRunLoopGetMain();

RunLoop与线程

RunLoop和线程有这密切的关系

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key, RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。

对于上述的这些关系,我们可以在开源的Core Foundation中窥见一二:

// CFRunLoop.c
// 代码已经简化

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { 
  // 在全局RunLoop字典 __CFRunLoops 中,以线程作为key,获取RunLoop
  CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
  // 如果loop为空
  if (!loop) {
    //则新创建loop
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
    //以线程作为key,存入全局RunLoop字典中
    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
    loop = newLoop;
    CFRelease(newLoop);
  }
}

RunLoop相关的类

Core Foundation中关于RunLoop的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其中RunLoop的底层结构为:

typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
  pthread_t _pthread;               //与该Runloop对应的线程
  CFMutableSetRef _commonModes;     //被标记为commonModes的Mode
  CFMutableSetRef _commonModeItems; //被添加到commonModes的事件源
  CFRunLoopModeRef _currentMode;    //表示该RunLoop中正在运行的Modes
  CFMutableSetRef _modes;           //CFRunLoopModeRef类型,表示该Runloop中包含的Modes
}
Runloop示意图.png

CFRunLoopModeRef

CFRunLoopModeRef的底层结构为:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
  CFStringRef _name;
  CFMutableSetRef _source0;
  CFMutableSetRef _source1;
  CFMutableArrayRef _observers;
  CFMutableArrayRef _timers;
}
  • CFRunLoopModeRef代表RunLoop的运行模式
  • 一个RunLoop包含若干个Mode,每个Mode有包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个Mode,作为currentMode
  • 如果需要切换Mode,只能退出当Loop
  • 不同Mode的Source0/Source1/Timer/Observer能分隔开,互不影响
  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

常见的2种Mode

系统默认提供的Run Loop Modes有:

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode)

  • UITrackingRunLoopMode

需要切换到对应的Mode时只需要传入对应的名称即可。前者是系统默认的Runloop Mode,例如进入iOS程序默认不做任何操作就处于这种Mode中,此时滑动UIScrollView,主线程就切换Runloop到到UITrackingRunLoopMode,不再接受其他事件操作(除非你将其他Source/Timer设置到UITrackingRunLoopMode下)。

但是对于开发者而言经常用到的Mode还有一个kCFRunLoopCommonModes(NSRunLoopCommonModes),其实这个并不是某种具体的Mode,而是一种模式组合,在iOS系统中默认包含了NSDefaultRunLoopModeUITrackingRunLoopMode

注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了NSDefaultRunLoopModeUITrackingRunLoopMode。当然你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到kCFRunLoopCommonModes组合。

注意:我们常常还会碰到一些系统框架自定义Mode,例如Foundation中NSConnectionReplyMode。还有一些系统私有Mode,例如:GSEventReceiveRunLoopMode接受系统事件,UIInitializationRunLoopMode App启动过程中初始化Mode。

Source0/1 & Timers & Observer

  • Source0
    • 触摸事件的处理
    • performSelector:onThread:
  • Source1
    • 基于Port的线程间通信
    • 系统事件捕捉,比如点击事件,捕捉后封装成Source0
  • Timers
    • NSTimer
    • performSelector:withObject:aferDelay
      • 内部是用定时器实现
  • Observer
    • 用于监听RunLoop的状态
    • UI刷新(BeforeWaiting,睡眠之前)
    • Autorelease pool(BeforeWaiting,睡眠之前)

CFRunLoopObserverRef

struct __CFRunLoopObserver {
      CFRuntimeBase _base;
      pthread_mutex_t _lock;
      CFRunLoopRef _runLoop;              /* 监听的RunLoop */
      CFIndex _rlCount;
      CFOptionFlags _activities;          /* 监听的RunLoop状态 */
      CFIndex _order;                     /* 创建时传入的排序 */
      CFRunLoopObserverCallBack _callout; /* 回调 */
      CFRunLoopObserverContext _context;  /* 回调参数 */
};

CFRunLoopObserverRef可以用来监听RunLoop的状态,RunLoop的状态分为以下几种:

Runloop_Activity.png

创建自定义Observer

创建自定义Observer有两种方式

  • 第一种:使用回调函数创建

    /// 1. 创建observer
    // 第一个参数:分配存储空间,使用默认的即可:kCFAllocatorDefault
    // 第二个参数:要监听的状态(kCFRunLoopAllActivities)所有的状态
    // 第三个参数:是否持续监听
    // 第四个参数:优先级,填0即可
    // 第五个参数:回调,CFRunLoopObserverCallBack类型,
    // typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
    // 第六个参数:context,回调传参,可以NULL
     CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                                kCFRunLoopAllActivities,
                                                                YES,
                                                                0,
                                                                observerCallBack,
                                                                NULL);
        
    /// 2.将observer添加到RunLoop中,
    // kCFRunLoopCommonModes默认包括kCFRunLoopDeaultMode、UITrackingRunLoopMode
    CFRunLoopAddObserver(CFRunLoopGetMain(),
                         observerRef,
                         kCFRunLoopCommonModes);
        
    /// 3. 释放observer
    CFRelease(observerRef);
    
    /// 回调函数
    /// @param observer observer
    /// @param activity Loop activity
    /// @param info 创建observer时,传入的context
    void observerCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry,即将进入Loop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"kCFRunLoopBeforeTimers,即将处理Timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"kCFRunLoopBeforeSources,即将处理Source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"kCFRunLoopBeforeWaiting,即将进入Loop");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"kCFRunLoopAfterWaiting,刚从睡眠中唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"kCFRunLoopExit,即将退出Loop");
                break;
            default:
                break;
        }
    }
    
  • 第二中:使用Block回调创建

    /// 1. 创建observer
    // 第一个参数:分配存储空间,使用默认的即可:kCFAllocatorDefault
    // 第二个参数:要监听的状态(kCFRunLoopAllActivities)所有的状态
    // 第三个参数:是否持续监听
    // 第四个参数:优先级,填0即可
    // 第五个参数:block回调
    CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
                                                                          kCFRunLoopAllActivities,
                                                                          YES,
                                                                          0,
                                                                          ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"kCFRunLoopEntry,即将进入Loop");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"kCFRunLoopBeforeTimers,即将处理Timer");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"kCFRunLoopBeforeSources,即将处理Source");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"kCFRunLoopBeforeWaiting,即将进入Loop");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"kCFRunLoopAfterWaiting,刚从睡眠中唤醒");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"kCFRunLoopExit,即将退出Loop");
                    break;
                    
                default:
                    break;
            }
        });
        
    /// 2.将observer添加到RunLoop中,
    // kCFRunLoopCommonModes默认包括kCFRunLoopDeaultMode、UITrackingRunLoopMode
    CFRunLoopAddObserver(CFRunLoopGetMain(),
                         observerRef,
                         kCFRunLoopCommonModes);
        
    /// 3. 释放observer
    CFRelease(observerRef);
    

RunLoop的运行逻辑

Runloop流程图.png

源码分析

我们可以在ViewControllerviewDidLoad打一个断点,利用lldb指令:bt查看程序调用栈

lldb_bt_demo.png

可以看出,进入RunLoop逻辑的是CFRunLoopRunSpecific函数。下面是伪代码开始分析:

//  CFRunLoopRunSpecific
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
  //通过Mode名称 ,获取RunLoop当前的Mode
  CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
      
  //通知observer: 进入Loop
  __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
  //具体要做的事情
  result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
      
  //通知observer: 退出Loop
  __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
  return result;
}

__CFRunLoopRun()

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

  int32_t retVal = 0;
  do {
    // 通知observer: 即将处理Timer
    __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
    
    // 通知observer: 即将处理Sources
    __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
    
    // 通知observer: 即将处理block
    __CFRunLoopDoBlocks(rl, rlm);

    // 通知observer: 处理Source0
    if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
      // 通知observer: 即将处理block
      __CFRunLoopDoBlocks(rl, rlm);
    }
    
    // 判断有无Source1
    if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
      //如果有就跳转到 handle_msg
      goto handle_msg;
    }
    
    // 通知observer: 即将休眠
    __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    __CFRunLoopSetSleeping(rl);
        
    //等待别的消息来唤醒当前线程,线程阻塞在这里
   __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
    // 如果被唤醒,继续执行
    __CFRunLoopUnsetSleeping(rl);
    // 通知observer: 结束休眠
    __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

handle_msg:;
    if (/* 被timer唤醒 */) {
      //处理timer
      __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()
    } else if (/* 被timer唤醒 */) {
      // 处理GCD
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
    } else { /* 被Source1唤醒 */
      // 处理Source1
      __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
    }

    // 设置返回值
    if (sourceHandledThisLoop && stopAfterHandle) {
      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)) {
      retVal = kCFRunLoopRunFinished;
    }
} while (0 == retVal);
    return retVal;
}

RunLoop 休眠

其实对于Event Loop而言RunLoop最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。RunLoop的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件Darwin中的Mach来完成的(Darwin是开源的)。

Mach是Darwin的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在Mach中,进程、线程间的通信是以消息的方式来完成的,消息在两个Port之间进行传递(这也正是Source1之所以称之为Port-based Source的原因,因为它就是依靠系统发送消息到指定的Port来触发的)。消息的发送和接收使用<mach/message.h>中的mach_msg()函数(事实上苹果提供的Mach API很少,并不鼓励我们直接调用这些API)。

/*
   *  Routine:  mach_msg
   *  Purpose:
   *    Send and/or receive a message.  If the message operation
   *    is interrupted, and the user did not request an indication
   *    of that fact, then restart the appropriate parts of the
   *    operation silently (trap version does not restart).
   */
  __WATCHOS_PROHIBITED __TVOS_PROHIBITED
  extern mach_msg_return_t  mach_msg(
            mach_msg_header_t *msg,
            mach_msg_option_t option,
            mach_msg_size_t send_size,
            mach_msg_size_t rcv_size,
            mach_port_name_t rcv_name,
            mach_msg_timeout_t timeout,
            mach_port_name_t notify);

mach_msg()的本质是一个调用mach_msg_trap(),这相当于一个系统调用,会触发内核状态切换。当程序静止时,RunLoop停留在

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)

而这个函数内部就是调用了mach_msg()让程序处于休眠状态。

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

推荐阅读更多精彩内容

  • 一、概念 runloop 程序在运行过程中循环的做一些事情; 二、 作用 处理下面的事件: 定时器 ...
    code_牧轩阅读 649评论 0 0
  • runloop的理解借助了MJ视频以及开源文档 https://github.com/apple/swift-co...
    tigerfly66阅读 476评论 0 0
  • 前言: main() { initialize(); do { varmessage = get_...
    一川烟草i蓑衣阅读 75评论 0 0
  • 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退...
    这是个方块阅读 267评论 0 1
  • ##RunLoop浅析 参考文章:`//www.greatytc.com/p/c38b5741919b`...
    星空梦想阅读 373评论 0 0