当前位置: 首页 > news >正文

iOS 初识RunLoop

iOS 初识RunLoop

文章目录

  • iOS 初识RunLoop
    • RunLoop的概念
    • RunLoop的功能
    • RunLoop和线程的关系
    • RunLoop的结构
      • Mode
      • Observer
      • Timer 和 source
        • 小结
    • RunLoop的核心
    • RunLoop的流程
    • RunLoop的应用
      • AutoreleasePool
      • 响应触控事件
      • 刷新界面
      • 常驻线程
      • 网络请求
      • NSTimer 和 CADisplayLink
        • NSTimer
        • GCDTimer
        • CADisplayLink

RunLoop的概念

一般来说,一个线程一次只能执行一个任务,执行完成后线程就退出,如果我们需要一个机制,让线程能随时处理事件而不退出.

可以将代码理解成下图:

do {//获取消息//处理消息
} while (消息 != 退出)

这种模型通常被称作Event Loop很多系统和框架都有实现在iOS和macOS中就被称作RunLoop.

简单来说RunLoop就是一种循环,通过这种循环得以让应用程序持续运行,让线程可以随时处理事件,并且让线程需要进行事件处理的时候忙起来,在不需要事件处理的时候闲下来.

  • RunLoop是通过内部维护的事件循环来实现对于事件/消息进行管理的一个对象
  • 没有消息处理时,休眠以避免资源占用,有消息需要处理的,立刻被唤醒

从代码上来看RunLoop是一个对象:

struct __CFRunLoop {CFRuntimeBase _base;pthread_mutex_t _lock;  /* locked for accessing mode list */__CFPort _wakeUpPort;   // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloopBoolean _unused;volatile _per_run_data *_perRunData; // reset for runs of the run looppthread_t _pthread;             //RunLoop对应的线程uint32_t _winthread;CFMutableSetRef _commonModes;    //存储的是字符串,记录所有标记为common的modeCFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)CFRunLoopModeRef _currentMode;   //当前运行的modeCFMutableSetRef _modes;          //存储的是CFRunLoopModeRefstruct _block_item *_blocks_head;//doblocks的时候用到struct _block_item *_blocks_tail;CFTypeRef _counterpart;
};

下面这个图片是一个RunLoop结构的一个概览:

img

RunLoop的功能

  • 保持程序的持续运行
  • 处理app中的各种事件
  • 节省cpu资源,提高程序性能:在需要工作的时候才去工作,在不需要工作的时候就进入休眠状态,在休眠状态是不占用CPU的

RunLoop和线程的关系

RunLoop和线程是一一对应的,app启动之后,程序进入了主线程,apple帮我们在主线程启动了一个RunLoop.如果是我们开辟的子线程,那么需要我们手动开启RunLoop,而且如果你不主动去获取RunLoop的话,那么子线程的RunLoop是不会开启的,他是采用了一个懒加载的形式.

  • 注意:苹果不允许直接创建 RunLoop,只能通过 CFRunLoopGetMain()CFRunLoopGetCurrent() 去获取,其内部会创建一个 RunLoop 并返回给你(子线程),而它的销毁是在线程结束时。

img

RunLoop在线程中的作用主要就是从 input sourcetimer source这里面接受事件,然后在线程中处理事件

输入源 (input source) 异步传递的事件,通常是从其他线程,app发送的消息

计时器源 (Timer source) 同步传递事件,这些事件在计划的时间,间隔触发.

input source 传递异步事件到对应的一个处理程序,并且退出runUntilDate方法.Timer source传递事件到对应处理程序,但是不会退出runLoop.

runUntilDate:方法在指定时间到达前会保持RunLoop 处于运行状态,RunLoop运行时会处理 input source 的各种事件。

这里我们就可以方便理解下面这段代码的意思了

  1. 为什么main函数不会退出
int main(int argc, char * argv[]) {NSString * appDelegateClassName;@autoreleasepool {// Setup code that might create autoreleased objects goes here.appDelegateClassName = NSStringFromClass([AppDelegate class]);}return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

UIApplicationMain内部默认开启了主线程的RunLoop并执行了一段无限循环的代码,UIApplicationMain函数一直没有返回,还在不断的接受处理消息以及等待休眠,所以运行程序之后,会保持持续运行状态.

RunLoop的结构

在 CoreFoundation 里面关于 RunLoop 有5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

他们之间的关系如下图:

RunLoop_0.png

一个RunLoop中有多个Mode,每一个mode有属于自己的Observer,Timer,Source.

Mode

Mode也就是模式,一个RunLoop当前只能处于一种Mode中,就好比当前只能是白天或者当前是夜晚.图中可以看到不同Mode之间是互不干扰的,处于各自的世界里面,AMode做了什么和BMode都是没有关系的,这也就是apple为什么会设置成两个状态来管理我们的滑动和默认状态.

apple给我提供了一下几种Mode:

  • kCFRunLoopDefaultModeapp默认的Mode,通常主线程是在这个Mode下运行的
  • UITrackingRunLoopMode界面追踪Mode,让UISrcollView处于滑动的时候就处于这个Mode
  • UIInitializationRunLoopMode:刚启动app就进入的第一个Mode.启动后就不在使用
  • GSEventReceiveRunLoopMode接受系统事件的内部Mode.通常情况下是用不到的
  • kCFRunLoopCommonModes:不会是一个真正意义上的一个Mode,但是如果你把事件丢到这里来,那么不管你处于什么mode,都会触发你想要执行的事件

当我们的程序运行起来的时候,我们就不用去动他了,它就处于一个kCFRunLoopDefaultMode的状态,当你滚动他力,他就会处于UITrackingRunLoopMode的一个状态,如果你想要在这两个状态都可以响应同一个事件的话,那么就要把事情都方法到kCFRunLoopCommonModes中去执行

大部分情况我们只用使用下面的几种

  • kCFRunLoopDefaultMode
  • UITrackingRunLoopMode
  • kCFRunLoopCommonModes

Observer

Observer,这个就相当于观察你上班的状态的人,这里用一个枚举来表示当前RunLoop的状态:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),           // 即将进入 LoopkCFRunLoopBeforeTimers = (1UL << 1),    // 即将处理 TimerkCFRunLoopBeforeSources = (1UL << 2),   // 即将处理 SourcekCFRunLoopBeforeWaiting = (1UL << 5),   // 即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6),    // 刚从休眠中唤醒kCFRunLoopExit = (1UL << 7),            // 即将退出 LoopkCFRunLoopAllActivities = 0x0FFFFFFFU   // 所有的状态
};

Timer 和 source

从结构的那张图可以看到,Mode 中有一个 Timer 的数组,一个 Mode 中可以有多个 Timer。Timer其实就是计时器,工作原理其实是确定一个计时器,然后给一个任务设置开始事件,和多久执行一次的一个Timer,把它注册到RunLoop中去,然后RunLoop就会根据我们注册的一个时间点来实现对应的内容:

注意,RunLoop中的Timer也不一定是准确的,因为RunLoop的执行事件是有一个顺序的,所以我们要处理玩一个事件才可以处理下一个事件,所以按照逻辑上来讲,尽管事件到了我们还是要把上一个事件执行玩才可以执行这个事件,也就是说我们尽管设置了一个是Timer,也要把上一个事件执行完才可以执行下一个任务,所以按道理来说Timer也不一定准确.

Source

Source 是另外一种 RunLoop 要干的活,看源码的话,Source 其实是 RunLoop 的数据源抽象类.

这里apple给我定义了两种Version的source:

  • source0 : 处理 App 内部事件,App 自己负责管理(触发),如 UIEventCFSocket
  • source1:由 RunLoop 内核管理,Mach port 驱动,如 CFMackPortCFMessagePort

Soucre1可以理解成一种线程之间通信的一种方式,在这ARunLoop中,我想让B线程传递东西给我们,那我们就要通过Port来进行一个传递,然后系统将传输的东西包装成 Source1,在线程 A 中监听 port 是否有东西传输过来,接收到后,唤醒 RunLoop 进行处理

小结

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。(所以在下文中我们实现一个常住线程也是给他添加一个item,保证RunLoop不会被退出)

RunLoop的核心

前面我们一直重复的内容是RunLoop的是一个do - while循环,但是按道理来说do-while循环也是会占用cpu的,我们是怎么实现RunLoop的休眠功能呢?

就是它如何在没有消息处理时休眠,在有消息时又能唤醒。这样可以提高CPU资源使用效率 当然RunLoop它不是简单的while循环,不是用sleep来休眠,毕竟sleep这方法也是会占用cpu资源的。那它是如何实现真正的休眠的呢?

那就是:没有消息需要处理时,就会从用户态切换到内核态,用户态进入内核态后,把当前线程控制器交给内核态,这样的休眠线程是被挂起的,不会再占用cpu资源。

img

这里要注意用户态和内核态 这两个概念,还有mach_msg()方法。 内核态 这个机制是依靠系统内核来完成的。

RunLoop是通过通过内部维护的时间循环来对事件/消息进行管理的一个对象

  • 没有消息需要处理时,休眠避免掉资源占用
    • 用户态 -> 内核态
  • 有消息时候,立刻被唤醒
    • 内核态 -> 用户态

RunLoop的流程

Work.png

具体流程:

  1. 通知Observer已经进入RunLoop
  2. 通知 Observer 即将处理 Timer
  3. 通知Observer即将处理source0
  4. 处理sourece0
  5. 如果有soucre1就通知跳转第九步
  6. 通知Observer即将休眠
  7. 线程休眠状态,除非遇到下面的情况:
    • 有Source0
    • Timer到时间执行
    • 外部手动唤醒
    • 为RunLoop设定时间超时
  8. 通知Observer线程刚被唤醒
  9. 处理等待处理事件
    • 如果是 Timer 事件,处理 Timer 并重新启动循环,跳到 2
    • 如果是source1触发,就处理source
    • RunLoop如果手动被触发但尚未超时,重新启动循环
  10. 通知Observer即将退出

在这里插入图片描述

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

这里不用担心因为一直处于一个while中的时候,我们的线程就一直在运行,会浪费资源,但是实际上并非如此,我们的RunLoop进入休眠调用的函数mach_msg这个函数是内核记别的指令,可以让这个线程进入一个等待的装入,然后需要的时候再唤醒,所以不用担心这个线程的一个占用问题.

RunLoop的应用

AutoreleasePool

首先我们之前讲过主线程创建的时候,会自动生成RunLoop,而主线程注册了两个Observer,来是实现这里的AutoreleasePool的管理:

  • 第一个Observer,这里的Observer是用来监听一个事件Entry这个事件,即将进入 Loop 的时候,创建一个自动释放池,并且给了一个最高的优先级,保证自动释放池的创建发生在其他回调之前,这是为了保证能管理所有的引用计数。
  • 第二个Observer: 监听两个事件,一个 BeforeWaiting,一个 ExitBeforeWaiting 的时候,干两件事,一个释放旧的池,然后创建一个新的池,所以这个时候,自动释放池就会有一次释放的操作,是在 RunLoop 即将进入休眠的时候。Exit的时候,也释放自动释放池,这里也有一次释放的操作。

BeforeWaiting这个含义其实就是指我们当前的一个RunLoop即将休眠了的状态,我们这里就会释放一个旧池子,然后创建一个新的池子,等到RunLoop退出的时候再次销毁池子.

  • 进入RunLoop的时候会创建一个池子
  • 休眠的时候会销毁该池子,在创建一个新池子
  • 退出的时候再次销毁池子

响应触控事件

苹果提前在 App 内注册了一个 Source1 来监听系统事件。

比如,当一个 触摸/锁屏/摇晃 之类的系统事情产生,系统会先包装,包装好了,通过 mach port 传输给需要的 App 进程,传输后,提前注册的 Source1 就会触发回调,然后由 App 内部再进行分发。

  1. 注册一个Souce1来接受事件
  2. 硬件事件的发生
  3. IOKit.framework 生成 IOHIDEvent 事件并由 SpringBoard 接收
  4. SpringBoard 用 mach port 转发给需要的 App
  5. Soucre1触发回调
  6. 回调中 IOHIDEvent 包装成 UIEvent 进行处理或分发

刷新界面

改变Ui的参数他不会立刻更新,他也是通过RunLoop来进行一个更新的

当 UI 需要更新,先标记一个 dirty,然后提交到一个全局容器中去。然后,在 BeforeWaiting 时,会遍历这个容器,执行实际的绘制和调整,并更新 UI 界面。(在休眠和退出)

常驻线程

我们每一次网络请求的时候都会创建一个新的线程,这里为了减小开销,我们其实可以让一个线程常驻,保证线程不被销毁,这样可以让我们的开销减小.

我们在上面说过,一个RunLoop中如果没有Observer/Timer/Source等items,Runloop会自动退出,因此我们创建一个空的port发送消息给Runloop,以至于Runloop不会退出而是一直常驻

这里笔者给出一个案例:

- (void)viewDidLoad {[super viewDidLoad];self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];[self.thread start];self.view.backgroundColor = UIColor.redColor;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event  {[self performSelector:@selector(runTest) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)runTest {NSLog(@"%@", [NSThread currentThread]);
}
- (void)runThread {NSLog(@"开启子线程 %@", self.thread);[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop] run];
}

然后我们点击空白处

image-20250518135616295

网络请求

网络请求在RunLoop中有以下几个层级:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire
  • CFSocket:是最底层的接口,只负责socket通信
  • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
  • 这一次是一个更高级的封装,可以说是新的面向接口的封装
  • NSSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

下面来了解一下有关于NSURLConnection的工作流程

使用NSConnection的时候,会传入一个Delegte,当调用了[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 执行实际的回调。

NSTimer 和 CADisplayLink

NSTimer

前面一直有提到Timer source事件源,从上层来看其实他就相当于我们的一个NSTimer

一个NSTimer注册到RunLoop后,RunLoop会为其重复的时间点注册好事件,比方说10 : 00者几个时间点.RunLoop为了节省资源,不会在非常准确的时间点进行一个回调Timer.Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。由于 NSTimer 的这种机制,因此 NSTimer 的执行必须依赖于 RunLoop,如果没有 RunLoop,NSTimer 是不会执行的。

GCDTimer

使用GCDTimer就不会有这个问题,NSTimer十因为依赖与RunLoop的内容,如果RunLoop的任务繁重,很有可能导致时间不够准确,而采用GCD的计时器就不会有这个问题,因为这里的GCDTimer是依赖于我们的操作系统内核的,所以会更加准确.

CADisplayLink

CADisplayLink是一个执行频率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改变刷新频率)的定时器,它也需要加入到RunLoop才能执行。与NSTimer类似,CADisplayLink同样是基于CFRunloopTimerRef实现,底层使用mk_timer(可以比较加入到RunLoop前后RunLoop中timer的变化)。和NSTimer相比它精度更高(尽管NSTimer也可以修改精度),不过和NStimer类似的是如果遇到大任务它仍然存在丢帧现象。通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅,而NSTimer则有更广泛的用处。

相关文章:

  • 深度学习推理引擎---ONNX Runtime
  • Vue+Go 自定义打字素材的打字网站
  • 海盗王改60帧时有关树木抖动的问题
  • Leetcode 3551. Minimum Swaps to Sort by Digit Sum
  • Protect Your Digital Privacy: Obfuscate, Don’t Hide
  • C语言指针深入详解(二):const修饰指针、野指针、assert断言、指针的使用和传址调用
  • 用 UniApp 构建习惯打卡 App —— HabitLoop 开发记
  • 报告精读:华为2024年知行合一通信行业数据治理实践指南报告【附全文阅读】
  • leetcodehot100刷题——排序算法总结
  • python中http.cookiejar和http.cookie的区别
  • React 19版本refs也支持清理函数了。
  • 【每天一个知识点】湖仓一体(Data Lakehouse)
  • 规则联动引擎GoRules初探
  • 牛客网NC21989:牛牛学取余
  • 新电脑软件配置二:安装python,git, pycharm
  • UnLua源码分析(一)初始化流程
  • 【关联git本地仓库,上传项目到github】
  • 四品种交易策略
  • 模型评估与调优(PyTorch)
  • 02 K8s双主安装
  • 以色列称“将立即允许恢复”人道主义物资进入加沙
  • 看展 | 黄永玉新作展,感受赤子般的生命力
  • 基金经理调仓引发大金融板块拉升?公募新规落地究竟利好哪些板块
  • 中拉论坛部长级会议为何悬挂海地和圣卢西亚的国旗?外交部回应
  • 挖掘机4月销量同比增17.6%,出口增幅创近两年新高
  • 男子发寻母视频被警方批评教育,律师:发寻亲信息是正当行为