请稍侯

深入理解iOS 中的 Runloop

12 December 2020

深入理解iOS 中的 Runloop

我们知道Runloop本质上是一个执行死循环的对象,它跑起来后大多数时候是处于休眠等待状态,是不占用CPU资源的。我们先来看一下它是怎么做到的:

闲等待:在死循环里是如何不占用CPU资源的 所谓闲等待是在一个循环里,CPU不是一直在空转,没有任务处理时,就不占用CPU计算资源,让CPU就去干别的事。这一般都是通 过中断来实现的:

  • 中断是CPU停止正在运行的程序并转入处理新情况的程序,处理完毕后才返回原被暂停的程序继续运行;
  • 比如在阻塞IO中IO设备向CPU发一条IO中断信号,CPU就会暂停当前程序的处理去服务该I/O设备的程序;
  • 可知sleep函数本质上是通过时钟中断让CPU暂停对当前线程的处理的,时钟中断可以用来切换运行中的线程(抢占式调度);
  • 阻塞机制是循环+中断+特定的数据结构实现,本质上是由中断来实现CPU调度切换,中断会导致内核的任务调度器挂起当前进程,当有中断返回或有其他中断信号时,再恢复进程;
  • 非阻塞一般是借助缓冲区满/空,或事件循环通过注册信号处理函数来实现高效且节省CPU资源的。

关于阻塞就有必要看一下IO的几种模型

  • 阻塞IO:进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程;
  • 非阻塞IO:进程发起IO系统调用后,如果内核缓冲区没有数据,需要到IO设备中读取,进程返回一个错误而不会被阻塞;进程发起IO系统调用后,如果内核缓冲区有数据,内核就会把数据返回进程。应用上一般借助缓冲区减少轮循消耗CPU资源;
  • IO复用模型:是多个进程IO可以注册到一个复用器(select)上,让该select监听所有注册进来的IO,然后再用一个进程调用该select,如果select所有监听的IO在内核缓冲区都没有可读数据,select调用进程会被阻塞,当任一IO在内核缓冲区中有可数据时,select调用就会返回;
  • 信号驱动IO:当进程发起一个IO操作,会向内核注册一个信号处理函数,然后进程返回不阻塞;当内核数据就绪时会发送一个信号给进程,进程便在信号处理函数中调用IO读取数据。具体应用有linux下的epoll、macOS下的runloop的实现机制。 异步IO:同步IO是一种IO处理方式。

Runloop的一些特性总结

  • Runloop在iOS中被封装成了一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;
  • 在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能;
  • 主线程一开起来就会跑一个对应的RunLoop,而RunLoop保证主线程不会被销毁,保证了程序的持续运行;
  • 所以总结起来Runloop主要有3个作用:1.保持线程持续运行;2.处理App中的各种事件;3.节省CPU资源,提高程序性能;

QQ20201212-142851

  • RunLoop在跑圈过程中,当接收到Input sources 或者 Timer sources时就会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了。

  • NSRunLoop对象是基于CFRunLoopRef的封装,所以NSRunLoopmainRunLoopcurrentRunLoop就相当于CFRunLoopGetMainCFRunLoopGetCurrent

  • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建,RunLoop在第一次获取时创建,在线程结束时销毁。
  • RunLoop的5个重要的类:
      CFRunLoopRef  //获得当前RunLoop和主RunLoop
      CFRunLoopModeRef  //运行模式,只能选择一种,在不同模式中做不同的操作
      CFRunLoopSourceRef  //事件源,输入源
      CFRunLoopTimerRef //定时器时间
      CFRunLoopObserverRef //观察者
    
  • 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
  • RunLoop Mode的类型:
      kCFRunLoopDefaultMode 
      //App的默认Mode,通常主线程是在这个Mode下运行
      UITrackingRunLoopMode 
      //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
      UIInitializationRunLoopMode 
      //在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
      GSEventReceiveRunLoopMode 
      //接受系统事件的内部 Mode,通常用不到
      kCFRunLoopCommonModes 
      //这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
    
  • Runloop中与Mode相关的三个函数:
    CFRunLoopAddSource()  //把runloop设置mode后,添加到Source的runloop列表里,对应addPort:forMode:
    CFRunLoopAddObserver() //向mode 的observer 数组中添加observer
    CFRunLoopAddTimer()  //添加mode 的timer数组中添加Timer,对应 addTimer:forMode:
    
  • Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
  • 为了保证用户界面保持平滑动画流畅,会给用户界面相关任务设置的较高优先级的 Runloop Mode,如UITrackingRunLoopModeNSRunLoopCommonModes,这样当UI很活跃的时候会就暂停一些别的任务。比如当UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,以标准的NSTimer和网络请求就不会启动。
  • 当同时对CADisplayLink指定多个run loop模式,如加入NSDefaultRunLoopModeUITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能。和CADisplayLink类似,NSTimer同样也可以使用不同的run loop模式配置。

  • 分析源码CFRunLoopc.c知道,在执行RunLoop的循环中使用了GCD的dispatch_source_t来实现其超时机制;另外GCD中将任务提交到主线程的主队列即dispatch_get_main_queue()时,是由RunLoop负责执行。

  • 只要RunLoop ModeSource/Observer/Timer中的有一个不为空, RunLoop就不会退出循环,即能够常驻内存。可以通过使用CFRunLoopStop(_:)或者从RunLoop Mode移除所有的Source/Observer/Timer,来停止runloop

参考: iOS RunLoop详解
poll/select/epoll 对比
阻塞io和非阻塞io在发生相应的系统调用的时候,操作系统到底发生了什么
编程语言中阻塞机制在操作系统最底层是如何实现的?
Epoll的本质(内部实现原理)
epoll原理详解及epoll反应堆模型
RunLoop 原理和核心机制
对iOS中runloop使用场景的一次总结