iOS RunLoop解析

RunLoop

RunLoop概念

RunLoop理解为运行循环。其本质就是一个do-while,这里的do-while和普通的do-while循环不一样,一般的 while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

image.png

图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。

RunLoop 作用

  1. 保持程序持续运行。程序启动就会自动开启一个runloop在主线程
  2. 处理App中的各种事件
  3. 节省CPU资源,提高程序性能 有事情则做事,没事情则休眠

RunLoop 和线程之间的关系

  • Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。
  • 主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。

底层RunLoop的存储使用的是字典结构,线程是key,对应的runloop是value。

  1. 主线程是默认开启RunLoop的
  2. 子线程是默认不开启RunLoop的
  3. 子线程开启RunLoop需要我们手动开启,手动开启时会先在全局的存储字典里根据传入的线程key,查看value是否存在,不存在的话就基于线程为key创建一个新的runloop并存储进字典里。

RunLoop Mode

RunLoop Mode可以理解为RunLoop的运行模式,在苹果文档里定义了有五种运行模式。

- kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
- UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
- kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到

在源码里搜索__CFRunLoopMode可以看到里面的内部结构,提取出关键的成员变量。

  1. _name:Mode的名字
  2. _sources0:
    1. App内部事件,由App自己管理的,像UIEvent、CFSocket、以及performSelector不带afterDelay参数的方法都是source0
    2. source0不能主动触发,需要调用CFRunLoopWakeUp(runloop) 来唤醒 RunLoop
  3. _sources1:
    1. 由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort。
    2. source1包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息
    3. 能主动唤醒 RunLoop 的线程
  4. _observers:添加的观察者
  5. _timers:定时器,包括NSTimer、CADisplayLink以及performSelector带afterDelay参数的方法。
struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    __CFPortSet _portSet;
}

RunLoop的本质

RunLoop底层其实就是一个结构体,通过源码搜索__CFRunLoop。
提取出关键的成员变量_pthread、_currentMode以及_modes,可以发现

  1. RunLoop和线程是绑定的,每个线程会有对应的RunLop,只是主线程是默认开启的,子线程不是默认开启的,需要手动开启,然后底层就会根据传入的线程创建新的RunLoop
  2. _currentMode:当前运行的Mode
  3. _modes:多个mode的集合
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;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

通过上面的整理可以发现

  1. RunLoop管理着多个mode
  2. 每次RunLoop运行的的时候都必须指定一个mode作为_currentMode,如果需要执行其他mode的事务,那么就要先退出当前mode,然后切换另一个mode
  3. mode里面管理着source0、source1、timers、observers以及port端口的事务。

引用来自掘金博主的图片🌟


image.png

RunLoop的核心方法

通过查找源码,我们可以发现三个关键的方法

//通知观察者即将进入RunLoop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //进入RunLoop的关键方法
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知观察者即将退出RunLoop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

__CFRunLoopRun

通过下面的源代码解析,可以将RunLoop的处理逻辑理为

  1. 通知观察者,即将进入runloop
  2. 通知观察者,即将处理Timers
  3. 通知观察者,即将处理Sources
  4. 执行加入到runloop的blocks
  5. 处理Source0,处理之后可能会再次处理blocks
  6. 如果存在Source1,直接跳转到第9步
  7. 通知观察者,即将进入休眠
  8. 通知观察者,结束休眠
  9. 执行唤醒RunLoop的事件
    1. 处理timer事件
    2. 执行gcd通过异步函数提交任务到主线程的Block
    3. 处理Source1事件
    4. 被手动唤醒,不需要执行事件,单纯起到唤醒RunLoop的功能
  10. 执行加入到runloop的blocks
  11. 根据以下情况来确定是否要退出当前RunLoop
    1. 进入run方法时参数表明处理完事件就返回。
    2. 超出run方法参数中的超时时间
    3. 被外部调用者强制停止了
    4. 调用_CFRunLoopStopMode将mode停止了
    5. 检测还有没有待处理的sources/timer/observer
    6. 以上🈚五种情况均无的话 那么就跳转到第2步,继续循环
  12. 通知观察者,退出RunLoop

以下代码引用掘金博主的源码解析


do {
// 消息缓冲区,用户缓存内核发的消息
uint8_t msg_buffer[3 * 1024]; 
//取所有需要监听的port
__CFPortSet waitSet = rlm->_portSet;
//设置RunLoop为可以被唤醒状态
__CFRunLoopUnsetIgnoreWakeUps(rl);

//1.通知 Observers: RunLoop 即将处理 Timer 回调。
if (rlm->_observerMask & kCFRunLoopBeforeTimers) 
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

//2。通知 Observers: RunLoop 即将触发 Source(非port) 回调
 if (rlm->_observerMask & kCFRunLoopBeforeSources) 
 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

// 执行被加入的block
//外部通过调用CFRunLoopPerformBlock函数向当前runloop增加block。新增加的block保存咋runloop.blocks_head链表里。
//__CFRunLoopDoBlocks会遍历链表取出每一个block,如果block被指定执行的mode和当前的mode一致,则调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__执行
 __CFRunLoopDoBlocks(rl, rlm);

//RunLoop 触发 Source0 (非port) 回调
// __CFRunLoopDoSources0函数内部会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数会调用source0的perform回调函数,即rls->context.version0.perform
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

//处理了source0后再次处理blocks
if (sourceHandledThisLoop) {
     __CFRunLoopDoBlocks(rl, rlm);
 }
//
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
didDispatchPortLastTime = false;

//如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
     goto handle_msg;
}

//通知oberver即将进入休眠状态
if(!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);

//接收waitSet端口的消息
//等待接受 mach_port 的消息。线程将进入休眠
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

 // 计算线程沉睡的时长
 rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
 
 __CFPortSetRemove(dispatchPort, waitSet);
 __CFRunLoopSetIgnoreWakeUps(rl);
  // runloop置为唤醒状态
 __CFRunLoopUnsetSleeping(rl);
  // 8. 通知 Observers: RunLoop对应的线程刚被唤醒。
 if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

//收到处理的消息进行处理
handle_msg:;
// 忽略端口唤醒runloop,避免在处理source1时通过其他线程或进程唤醒runloop(保证线程安全)
__CFRunLoopSetIgnoreWakeUps(rl);

if(MACH_PORT_NULL == livePort){
// livePort为null则什么也不做

}else if(livePort == rl->_wakeUpPort){
// livePort为wakeUpPort则只需要简单的唤醒runloop(rl->_wakeUpPort是专门用来唤醒runloop的)
 CFRUNLOOP_WAKEUP_FOR_WAKEUP();

}else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
    //如果是一个timerPort
// 如果一个 Timer 到时间了,触发这个Timer的回调
// __CFRunLoopDoTimers返回值代表是否处理了这个timer
 if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
     __CFArmNextTimerInMode(rlm, rl);
 }
}else if(livePort == dispatchPort){
   //如果是GCD port
   //处理GCD通过port提交到主线程的事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}else{
    // 处理source1事件(触发source1的回调)
    //// runloop 触发source1的回调,__CFRunLoopDoSource1内部会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
   __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}


/// 执行加入到Loop的block
 __CFRunLoopDoBlocks(rl, rlm);
        
if (sourceHandledThisLoop && stopAfterHandle) {
     /// 进入loop时参数说处理完事件就返回。
     retVal = kCFRunLoopRunHandledSource; // 4
} else if (timeout_context->termTSR < mach_absolute_time()) {
            /// 超出传入参数标记的超时时间了
            retVal = kCFRunLoopRunTimedOut; // 3
        } else if (__CFRunLoopIsStopped(rl)) {
            /// 被外部调用者强制停止了
            __CFRunLoopUnsetStopped(rl); // 2
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            // 调用了_CFRunLoopStopMode将mode停止了
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped; // 2
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            // source0/source1/timers/blocks一个都没有了
            retVal = kCFRunLoopRunFinished; // 1
        }
}while(0 == retVal)
__CFRunLoopDoBlocks

解析关于runloop调用的blocks,而Source0、Source1、timers以及Observers 在前面已经解析过了。
这个方法主要处理Blocks回调。

  • runloop对象有一个_block_item结构的链表,里面存储着当前runloop待处理的blocks
  • 执行__CFRunLoopDoBlocks方法就是遍历runloop的链表,取出blocks,然后执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);方法
  • KVO回调方法,以及在主线程调用的block;这两种情况回调由__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);方法调用执行
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg);
  • 在调用GCD的异步函数dispatch_async,往主线程里添加任务时会触发该方法
  • 由于子线程默认是没有开启Runloop的,子线程下执行GCD的任务是不会被添加到RunLoop上的,子线程blocks的执行是由lidbdispatch驱动完成的。

RunLoop的应用

AutoreleasePool

App启动后,苹果在主线RunLoop里注册了两个观察者,分别观察RunLoop的即将进入loop事件、即将进入休眠事件、即将退出事件。

  • 即将进入loop事件触发时会调用_objc_autoreleasePoolPush()方法创建自动释放池以及插入哨兵对象
  • 即将进入休眠事件触发时会分别调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()方法,释放旧的自动释放池并release里面的对象以及创建新的自动释放池
  • 即将退出事件触发时会调用_objc_autoreleasePoolPop() 方法,释放自动释放池。

NSTimer

NSTimer就是上文提到的timer

  • 底层Runloop会在每个时间点都注册一个事件,到了事件点进行回调,但是并不是每次都是很准确地在指定时间点进行回调的,timer中有一个属性Tolerance标识可以容忍多大的误差。
  • 如果RunLoop有很多要处理的事,错过了timer指定的时间点,那么是会错过此次回调的。
  • 默认在主线创建的timer都会自动加入到RunLoop中,而如果是在子线程中创建的timer,在没有开启RunLoop的情况下,其实是无效的。

CADisplayLink

  • CADisplayLink是一个执行频率(fps)和屏幕刷新相同的定时器,需要加入到RunLoop才能执行。但是我们也可以通过调用API来实现更改执行频率。
  • 它与NSTimer都是定时器,区别在于CADisplayLink的精度更高,而NSTimer的使用范围更加地广泛。

事件响应

  • 苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()
  • 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue()进行应用内部的分发。
  • _UIApplicationHandleEventQueue() 会把 IOHIDEvent处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别

  • 当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
  • 苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
  • 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

UI更新

当修改了View的frame等导致View的图层发生了变化或者手动调用了setNeedsDisplay/setNeedsLayout方法之后就会将这个View标记放进一个全局容器里面,当RunLoop的即将进入休眠或者退出事件回调时,就会遍历这个全局容器将UI进行绘制更新。

PerformSelector 的实现原理

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

推荐阅读更多精彩内容