洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

    • Runloop系列

      • Runloop系列
        • RunLoop是什么?
          • Runloop是个对象,怎么获取呢?
          • 再说说RunLoop的实现机制是什么?
          • 关于RunLoop的source1和source0
        • RunLoop和线程的关系
        • RunLoop的有几种Mode, RunLoop设置Mode作用是什么?
        • RunLoop的工作原理
        • RunLoop的事件源
        • RunLoop的应用场景
          • 1. 定时器
          • 2. 网络请求
          • 3. 异步任务
          • 4. 触摸事件
        • 手势识别
        • 界面更新
        • runloop内部逻辑
        • RunLoop的实际应用
          • CADispalyTimer和Timer哪个更精确
          • 2.AutoreleasePool
          • 3.事件响应
          • 4.手势识别
          • 5.界面更新
          • 6.PerformSelecter
          • 7.NSURLConnection
          • GCD 在Runloop中的使用?
          • AFNetworking 中如何运用 Runloop?
        • 总结
        • 常见面试题
          • runloop 是怎么响应用户操作的, 具体流程是什么样的?
          • 说说runLoop的几种状态
          • timer 与 runloop 的关系?
          • runloop内部实现逻辑?
          • 讲讲 RunLoop,项目中有用到吗?
          • 如何实现线程保活/控制线程生命周期
          • RunLoop的休眠是如何实现的?
          • Runloop的作用
          • 为什么只有主线程的Runloop是自动开启的?
        • 如何使线程保活
        • 如何检测卡顿
      • CADisplayLink 与 NSTimer 有什么不同
      • 监控runloop卡顿
    • Runtime系列

    • 内存管理系列

    • Block系列

    • 线程系列

    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • Runloop系列
yangyang.wen
2023-08-11
目录

Runloop系列

# RunLoop是什么?

一句话总结: Runloop是通过内部维护一个事件循环来对事件、消息进行管理的一个对象。

首先,一个线程一次只能执行一个任务,执行完成后线程就会销毁。但是我们需要一个机制,让线程随时处理事件并不销毁,那就要考虑设立一个线程观察者对象,这个对象在接收到消息以后就去处理事情,没有事情的时候又不占用资源进入休眠状态,在消息到来时再次被唤醒。其实在各个系统中都有这样的机制,可能叫法不一,但意思相似,而iOS中,它就叫做RunLoop。

所以,RunLoop实际上就是一个对象。在iOS中,程序启动,便自动创建一个和主线程对应的RunLoop。这个对象管理了其需要处理的事件和消息,并提供了一个入口函数,线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束,函数返回。

# Runloop是个对象,怎么获取呢?

  • Foundation [NSRunloop currentRunLoop];获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop];获得主线程的Runloop对象

  • Core Foundation CFRunLoopGetCurrent();获得当前线程的RunLoop对象 CFRunLoopGetMain();获得主线程的Runloop对象

# 再说说RunLoop的实现机制是什么?

为了方便Runloop机制的理解,下面写一段伪代码来表示一下RunLoop循环。

function runloop() {
    initialize();
    do {
        var message = get_next_message();//从队列获取消息
        process_message(message);//处理消息
    } while (message != quit);//当触发quit条件时,Runloop退出
}
1
2
3
4
5
6
7

从代码代码可以看出,Runloop的处理机制是 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息)

RunLoop的核心是什么? 就是它如何在没有消息处理时休眠,在有消息时又能唤醒。这样可以提高CPU资源使用效率 当然RunLoop它不是简单的while循环,不是用sleep来休眠,毕竟sleep这方法也是会占用cpu资源的。那它是如何实现真正的休眠的呢?那就是:没有消息需要处理时,就会从用户态切换到内核态,用户态进入内核态后,把当前线程控制器交给内核态,这样的休眠线程是被挂起的,不会再占用cpu资源。

Screenshot-2023-08-19-at-19

意用户态和内核态 这两个概念,还有mach_msg()方法。 内核态 这个机制是依靠系统内核来完成的(苹果操作系统核心组件 Darwin 中的 Mach )。

下面是RunLoop实现的流程源码:

点击查看

/// RunLoop的实现 int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;

/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

    Boolean sourceHandledThisLoop = NO;
    int retVal = 0;
    do {

        /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
        /// 执行被加入的block
        __CFRunLoopDoBlocks(runloop, currentMode);

        /// 4. RunLoop 触发 Source0 (非port) 回调。
        sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
        /// 执行被加入的block
        __CFRunLoopDoBlocks(runloop, currentMode);

        /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
            if (hasMsg) goto handle_msg;
        }

        ///6. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }

        /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
        /// • 一个基于 port 的Source 的事件。
        /// • 一个 Timer 到时间了
        /// • RunLoop 自身的超时时间到了
        /// • 被其他什么调用者手动唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
            mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
        }

        /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

        /// 收到消息,处理消息。
        handle_msg:

        /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
        if (msg_is_timer) {
            __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
        } 

        /// 9.2 如果有dispatch到main_queue的block,执行block。
        else if (msg_is_dispatch) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } 


        /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
    } while (retVal == 0);
}

/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

}

源码我删减了很多,看源码里的注释,可以了解个Runloop运行的流程。 咱们还是围绕RunLoop的核心来理解, 既然上面提到休眠是通过内核来完成的,那唤醒条件呢? 下面几个就是主要的唤醒Runloop的事件:

收到基于 port 的 Source1 的事件Timer到时间执行RunLoop自身的超时时间到了被其他调用者手动唤醒

# 关于RunLoop的source1和source0

上面介绍了source1包括系统事件捕捉和基于port的线程间通信。

什么是系统事件捕捉?又如何理解基于port的线程间通信?

其实,我们手指点击屏幕,首先产生的是一个系统事件,通过source1来接受捕捉,然后由Springboard程序包装成source0分发给应用去处理,因此我们在App内部接受到触摸事件,就是source0,这一前一后的关系。source1 通过程序包装是会变成 source0的

Screenshot-2023-08-19-at-19

# RunLoop和线程的关系

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程除外)。

苹果不允许直接创建RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 小结:

  1. 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop会在线程结束时销毁

  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建([NSRunLoop currentRunLoop])

  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value

  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

# RunLoop的有几种Mode, RunLoop设置Mode作用是什么?

RunLoop的运行模式共有5种,RunLoop只会运行在一个模式下,要切换模式,就要暂停当前模式,重写启动一个运行模式

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

RunLoop设置Mode作用 设置Mode作用是指定事件在运行循环(Loop)中的优先级。 线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。(比如可以优化tableview的时候可以设置UITrackingRunLoopMode下不进行一些操作)

# RunLoop的工作原理

RunLoop的工作可以简单概括为以下几个步骤:

  1. 准备阶段:设置RunLoop需要监听的事件源(如触摸事件、定时器事件等)。
  2. 启动阶段:进入事件循环,等待事件的发生。
  3. 执行阶段:当事件发生时,RunLoop将事件分发给对应的事件处理器进行处理。
  4. 退出阶段:当所有事件处理完毕或接收到退出指令时,RunLoop结束当前循环,线程进入休眠状态,等待下一次启动。

# RunLoop的事件源

RunLoop的事件源主要有以下几种:

  1. NSMachPort:用于进程间通信(IPC)。
  2. CFSocket:用于Socket网络编程。
  3. CFRunLoopSource:自定义的事件源。
  4. NSTimer:定时器事件。
  5. NSRunLoopTimer:用于非精确时间触发的定时器事件。
  6. NSAutoreleasePool:自动释放池,用于内存管理。

# RunLoop的应用场景

# 1. 定时器

RunLoop常用于实现定时器功能,通过设置NSTimer对象,可以在指定时间间隔后触发相应的回调方法。

# 2. 网络请求

在进行网络请求时,RunLoop可以确保请求在主线程之外执行,避免阻塞主线程,影响用户体验。

# 3. 异步任务

通过在后台线程创建并启动RunLoop,可以实现异步任务的执行,如数据加载、图片处理等。

# 4. 触摸事件

苹果注册了一个 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 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
 QuartzCore:CA::Transaction::observer_callback:
 CA::Transaction::commit();
 CA::Context::commit_transaction();
 CA::Layer::layout_and_display_if_needed();
 CA::Layer::layout_if_needed();
 [CALayer layoutSublayers];
 [UIView layoutSubviews];
 CA::Layer::display_if_needed();
 [CALayer display];
 [UIView drawRect];
1
2
3
4
5
6
7
8
9
10
11

# runloop内部逻辑

实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

提示

  1. 通知 Observer 已经进入了 RunLoop

  2. 通知 Observer 即将处理 Timer

  3. 通知 Observer 即将处理非基于端口的输入源(即将处理 Source0)

  4. 处理那些准备好的非基于端口的输入源(处理 Source0)

  5. 如果基于端口的输入源准备就绪并等待处理,请立刻处理该事件。转到第 9 步(处理 Source1)

  6. 通知 Observer 线程即将休眠

  7. 将线程置于休眠状态,直到发生以下事件之一

    • 事件到达基于端口的输入源(port-based input sources)(也就是 Source0)
    • Timer 到时间执行
    • 外部手动唤醒
    • 为 RunLoop 设定的时间超时
  8. 通知 Observer 线程刚被唤醒(还没处理事件)

  9. 处理待处理事件

    • 如果是 Timer 事件,处理 Timer 并重新启动循环,跳到第 2 步
    • 如果输入源被触发,处理该事件(文档上是 deliver the event)
    • 如果 RunLoop 被手动唤醒但尚未超时,重新启动循环,跳到第 2 步

Source0被添加到RunLoop上时并不会主动唤醒线程,需要手动去唤醒。Source0负责对触摸事件的处理以及performSeletor:onThread:。

Source1具备唤醒线程的能力,使用的是基于Port的线程间通信。Source1负责捕获系统事件,并将事件交由Source0处理。

Source0 作用

  1. 触摸事件处理
  2. performSelector:onThread:

Source1 作用

  1. 基于Port的线程间通信
  2. 系统事件捕捉

Timers 作用

  1. NSTimer
  2. performSelector:withObject:afterDelay:

Observers 作用

  1. 用于监听RunLoop的状态

根据runloop的状态来做相应的事情 如:UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)等

流程

  1. 通知Observers:进入Loop
  2. 通知Observers:即将处理Timers
  3. 通知Observers:即将处理Sources
  4. 处理Blocks
  5. 处理Source0(可能会再次处理Blocks)
  6. 如果存在Source1,就跳转到第8步
  7. 通知Observers:开始休眠(等待消息唤醒)
  8. 通知Observers:结束休眠(被某个消息唤醒)
    1. 处理Timer
    2. 处理GCD Async To Main Queue
    3. 处理Source1
  9. 处理Blocks
  10. 根据前面的执行结果,决定如何操作
    1. 回到第02步
    2. 退出Loop
  11. 通知Observers:退出Loop

# RunLoop的实际应用

# CADispalyTimer和Timer哪个更精确

提示

iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。

NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。

# 2.AutoreleasePool

提示

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

# RunLoop和AutoreleasePool的关系

RunLoop进行处理事件的时候会自动创建一个AutoreleasePool,在处理事件过程中会将发送autorelease消息的对象添加到AutoreleasePool中。等待RunLoop处理事件结束,就释放当前的AutoreleasePool。AutoreleasePool则会将所有的对象进行release-1操作。

# 3.事件响应

提示

苹果注册了一个 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 事件都是在这个回调中完成的。

# 4.手势识别

提示

当 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

# 5.界面更新

提示

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

# 6.PerformSelecter

提示

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

# PerformSelector:afterDelay:这个方法在子线程中是否起作用?

不起作用,子线程默认没有 Runloop,也就没有 Timer。可以使用 GCD的dispatch_after来实现

# 7.NSURLConnection

提示

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取

CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 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 执行实际的回调。

# GCD 在Runloop中的使用?

GCD由 子线程 返回到 主线程,只有在这种情况下才会触发 RunLoop。会触发 RunLoop 的 Source 1 事件。

# AFNetworking 中如何运用 Runloop?

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 runLoop run 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
1
2
3
4
5
6
7
8
9
10

当需要这个后台线程执行任务时,AFNetworking 通过调用 NSObject performSelector:onThread:.. 将这个任务扔到了后台线程的 RunLoop 中。

# 总结

  • RunLoop是一个对象,他与线程有着一一对应的关系。

  • RunLoop包含了一系列Mode,Mode又包含了一系列ModeItems,ModeItems又是由Source/Timer/Observer组成。

  • Mode常用的有5种,分别处理不同的事物。其中Commons比较特殊,是一个集合,你可以将其他的Mode通过方法添加进Commons,这样就可以减少重复关联到Mode。

  • RunLoop运行时只能以一种固定的模式运行,如果需要切换Mode,只能退出当前的循环,再重新指定一个Mode进入

  • Soure有2类,分别是Source1和Source0,Source1处理来自系统内核或者其他进程或线程的事件,Source0处理不是其他进程或者内核直接发送给你的事件。

  • APP启动的时候主线程RunLoop跟着启动,并且默认包含kCFRunLoopDefaultMode和UITrackingRunLoopMode,Observer能监听定时器和各种交互事件以及界面刷新,当事件到来的时候会通过发送通知的方式触发事件处理,将事件block丢到block处理队列。事情处理完毕以后会进入休眠,等待下一次的唤醒,如果有符合退出RunLoop的条件,退出runloop,不符合继续runloop循环。

  • RunLoop的底层是通过基于mach port在内核和线程间进行消息发送实现的,为了实现消息的发送和接收,mach_msg() 函数实际上是调用函数mach_msg_trap()。

  • RunLoop在定时器,自动释放池,线程间通信,事件的响应和UI的刷新等方面都有应用。

# 常见面试题

# runloop 是怎么响应用户操作的, 具体流程是什么样的?

首先由Source1捕捉系统事件 然后Source1又将事件存放在 事件队列中交给Source0来处理。

# 说说runLoop的几种状态

一共有6中状态:

  1. kCFRunLoopEntry = (1UL << 0) , //即将进入Loop

  2. kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer

  3. kCFRunLoopBeforeSources = (1UL << 2) , //即将处理Source

  4. kCFRunLoopBeforeWaiting = (1UL << 5) , // 即将进入休眠 ,

  5. kCFRunLoopAfterWaiting = (1UL << 6) , / /刚从休眠中唤醒

  6. kCFRunLoopExit = (1UL << 7), //即将退出Loop

# timer 与 runloop 的关系?

一个runloop下会包含很多个model,每个model下又会包含很多的timer/source/observe,同一时刻runloop只能在一种模式下运行,处理一种模式下的状态

所以层次关系是 runloop 包含 model 包含 timer/source/observe

# runloop内部实现逻辑?

第一步:首先通知Observers进入Loop 然后处理一些 定时器、事件、block

第二步:事件处理完成之后通知Observers进入休眠状态开始休眠 等待消息唤醒

第三步:通知Observers结束休眠处理一些 定时器、事件、block

# 讲讲 RunLoop,项目中有用到吗?

答:肯定是有用到的 例如:

  1. 控制线程生命周期(线程保活)
  2. 解决NSTimer在滑动时停止工作的问题
  3. 监控应用卡顿
  4. 性能优化

# 如何实现线程保活/控制线程生命周期

首先需要知道线程一般执行完任务后,就会被销毁; 为什么说使用了Runloop就可以实现线程保活。添加runloop并运行起来,实际上是添加了一个循环,这样这个线程的程序一直卡在这个循环上,这样相当于线程的任务一直没有执行完,所以线程一直不会销毁; 如何销毁这个线程? 停止runloop,可以使用CFRunLoopStop函数。

在AFNetworking2.x中便是利用runloop实现线程保活的。代码如下:

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

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# RunLoop的休眠是如何实现的?

当RunLoop一旦休眠意味着CPU不会分配任何资源,那线程也进入休眠。RunLoop休眠内部是调用了mach_msg()函数。操作系统中有内核层面的API和应用层面的API。mach_msg()可以理解为是应用层面的API,告诉内核休眠该线程休眠。一旦接受到系统事件,也会转化成内核API,告诉内核需要唤醒该线程,那么又可以执行应用层API了。所以RunLoop的休眠可以看成是用户状态到内核状态的切换,而唤醒RunLoop就是内核状态到用户状态的切换。

# Runloop的作用

保持应用的持续运行

处理App的各种事件

节省CPU资源,提升性能。

负责渲染界面的UI

# 为什么只有主线程的Runloop是自动开启的?

看iOS的main函数代码, 代码自动生成了autoreleasepool,这里就是调用了runloop。app启动时main函数就自动开启了主线程的runloop。

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

# 如何使线程保活

  1. 为当前线程开启一个RunLoop, 可以通过[CFRunLoop getCurrent]或者 [NSRunLoop currentRunLoop]来创建,因为获取RunLoop这个方法本身会查找,如果当前线程没有runloop,会在系统内部为我们创建

  2. 向该RunLoop中添加一个port / Source等维护RunLoop的事件循环,RunLoop如果没有事件需要处理的话,默认情况下,是不能自己维持事件循环,会直接退出,所以需要添加port / Source来维持事件循环机制

  3. 启动该RunLoop

  4. 调用run方法

实现一:

NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
   NSLog(@"timer 定时任务");
}];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addTimer:timer forMode:NSDefaultRunLoopMode];
[runloop run];
1
2
3
4
5
6

实现二:

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
1
2
3

# 如何检测卡顿

这里重点介绍2种与runloop相关的2种检测卡顿方式

1.FPS检测方案
一般来说,我们约定60FPS即为流畅。那么反过来,如果App在运行期间出现了掉帧,即可认为出现了卡顿。

监控FPS的方案几乎都是基于CADisplayLink实现的。简单介绍一下CADisplayLink:CADisplayLink是一个和屏幕刷新率保持一致的定时器,一但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector。
可以通过向RunLoop中添加CADisplayLink,根据其回调来计算出当前画面的帧数

FPS的好处就是直观,小手一划后FPS下降了,说明页面的某处有性能问题。坏处就是只知道这是页面的某处,不能准确定位到具体的堆栈。

- (void)startFpsMonitoring {
    self.fpsDisplay = [CADisplayLink displayLinkWithTarget: proxy selector: @selector(displayFps:)];
    [self.fpsDisplay addToRunLoop: [NSRunLoop mainRunLoop] forMode: NSRunLoopCommonModes];
}

- (void)perframe:(CADisplayLink *)displayLink {
    if (_lastTime == 0) {
        _lastTime = displayLink.timestamp;
        return;
    }
    _count++;
    NSTimeInterval deltaTime = displayLink.timestamp - _lastTime;
    if (deltaTime < 1) return;
    _lastTime = displayLink.timestamp;
    float fps = _count / deltaTime;
    _count = 0;

    CGFloat progress = fps / 60.0;
    UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];

    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d fps", (int)round(fps)]];
    [text addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, [text length])];

    self.attributedText = text;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

2.runloop
在进入runloop之后,当前runloop会不断按照在kCFRunLoopBeforeTimers-> kCFRunLoopBeforeSources-> kCFRunLoopBeforeWaiting-> kCFRunLoopAfterWaiting的顺序循环,直到接收到退出runloop的消息,那么我们只要知道线程任务在哪个状态区间执行的并且抓取这个时间间隔,如果时间间隔超过阈值,则说明卡顿,上报堆栈信息。

- (void)beginMonitor {
    self.dispatchSemaphore = dispatch_semaphore_create(0);
    // 第一个监控,监控是否处于 运行状态
    CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
    self.runLoopBeginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                        kCFRunLoopAllActivities,
                                                        YES,
                                                        LONG_MIN,
                                                        &myRunLoopBeginCallback,
                                                        &context);
    //  第二个监控,监控是否处于 睡眠状态
    self.runLoopEndObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                      kCFRunLoopAllActivities,
                                                      YES,
                                                      LONG_MAX,
                                                      &myRunLoopEndCallback,
                                                      &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopBeginObserver, kCFRunLoopCommonModes);
    CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopEndObserver, kCFRunLoopCommonModes);

    // 创建子线程监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子线程开启一个持续的loop用来进行监控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 17 * NSEC_PER_MSEC));
            if (semaphoreWait != 0) {
                if (!self.runLoopBeginObserver || !self.runLoopEndObserver) {
                    self.timeoutCount = 0;
                    self.dispatchSemaphore = 0;
                    self.runLoopBeginActivity = 0;
                    self.runLoopEndActivity = 0;
                    return;
                }
                // 两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
                if ((self.runLoopBeginActivity == kCFRunLoopBeforeSources || self.runLoopBeginActivity == kCFRunLoopAfterWaiting) ||
                    (self.runLoopEndActivity == kCFRunLoopBeforeSources || self.runLoopEndActivity == kCFRunLoopAfterWaiting)) {
                    // 出现三次出结果
                    if (++self.timeoutCount < 2) {
                        continue;
                    }
                    NSLog(@"调试:监测到卡顿");
                } // end activity
            }// end semaphore wait
            self.timeoutCount = 0;
        }// end while
    });
}

// 第一个监控,监控是否处于 运行状态
void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;
    lagMonitor.runLoopBeginActivity = activity;
    dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

//  第二个监控,监控是否处于 睡眠状态
void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;
    lagMonitor.runLoopEndActivity = activity;
    dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

方案三:Ping主线程

Ping主线程的核心思想是向主线程发送一个信号,一定时间内收到了主线程的回复,即表示当前主线程流畅运行。没有收到主线程的回复,即表示当前主线程在做耗时运算,发生了卡顿。

self.semaphore = dispatch_semaphore_create(0);
- (void)main {
    //判断是否需要上报
    __weak typeof(self) weakSelf = self;
    void (^ verifyReport)(void) = ^() {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf.reportInfo.length > 0) {
            if (strongSelf.handler) {
                double responseTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);
                double duration = responseTimeValue - strongSelf.startTimeValue;
                if (DEBUG) {
                    NSLog(@"卡了%f,堆栈为--%@", duration, strongSelf.reportInfo);
                }
                strongSelf.handler(@{
                    @"title": [InsectUtil dateFormatNow].length > 0 ? [InsectUtil dateFormatNow] : @"",
                    @"duration": [NSString stringWithFormat:@"%.2f",duration],
                    @"content": strongSelf.reportInfo
                                   });
            }
            strongSelf.reportInfo = @"";
        }
    };

    while (!self.cancelled) {
        if (_isApplicationInActive) {
            self.mainThreadBlock = YES;
            self.reportInfo = @"";
            self.startTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);
            dispatch_async(dispatch_get_main_queue(), ^{
                self.mainThreadBlock = NO;
                dispatch_semaphore_signal(self.semaphore);
            });
            [NSThread sleepForTimeInterval:(self.threshold/1000)];
            if (self.isMainThreadBlock) {
                self.reportInfo = [InsectBacktraceLogger insect_backtraceOfMainThread];
            }
            dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
            //卡顿超时情况;
            verifyReport();
        } else {
            [NSThread sleepForTimeInterval:(self.threshold/1000)];
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
方案 优点 缺点 实现复杂性
FPS 直观 无法准确定位卡顿堆栈 简单
RunLoop Observer 能定位卡顿堆栈 不能记录卡顿时间,定义卡顿的阈值不好控制 复杂
Ping Main Thread 能定位卡顿堆栈,能记录卡顿时间 一直ping主线程,费电 中等
编辑 (opens new window)
上次更新: 2024/10/23, 23:26:17
字典系列
CADisplayLink 与 NSTimer 有什么不同

← 字典系列 CADisplayLink 与 NSTimer 有什么不同→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式