深入理解iOS 中的 Runloop
深入理解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资源,提高程序性能;
-
RunLoop在跑圈过程中,当接收到
Input sources
或者Timer sources
时就会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了。 -
NSRunLoop对象是基于CFRunLoopRef的封装,所以
NSRunLoop
的mainRunLoop
与currentRunLoop
就相当于CFRunLoopGetMain
与CFRunLoopGetCurrent
。 - 主线程的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,如
UITrackingRunLoopMode
或NSRunLoopCommonModes
,这样当UI很活跃的时候会就暂停一些别的任务。比如当UIScrollview
滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,以标准的NSTimer和网络请求就不会启动。 -
当同时对
CADisplayLink
指定多个run loop
模式,如加入NSDefaultRunLoopMode
和UITrackingRunLoopMode
来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能。和CADisplayLink
类似,NSTimer
同样也可以使用不同的run loop
模式配置。 -
分析源码
CFRunLoopc.c
知道,在执行RunLoop的循环中使用了GCD的dispatch_source_t
来实现其超时机制;另外GCD中将任务提交到主线程的主队列即dispatch_get_main_queue()
时,是由RunLoop负责执行。 - 只要
RunLoop Mode
的Source/Observer/Timer
中的有一个不为空,RunLoop
就不会退出循环,即能够常驻内存。可以通过使用CFRunLoopStop(_:)
或者从RunLoop Mode
移除所有的Source/Observer/Timer
,来停止runloop
。
参考: iOS RunLoop详解
poll/select/epoll 对比
阻塞io和非阻塞io在发生相应的系统调用的时候,操作系统到底发生了什么
编程语言中阻塞机制在操作系统最底层是如何实现的?
Epoll的本质(内部实现原理)
epoll原理详解及epoll反应堆模型
RunLoop 原理和核心机制
对iOS中runloop使用场景的一次总结