iOS开发runloop运行循环机制学习
作者:沧海一笑-梦中的瞬间 发布时间:2024-01-24 19:03:04
引言
RunLoop:又叫运行循环机制,在iOS中的两大机制之一。并不是只有iOS有Runloop其他语言也有,他们的方式不太一样,但是核心都是为了解决性能和良好的运行,例如:webJs里Runloop也称作eventLoop,由于js没有多线程,在这样的情况做了一种调用栈来配合主线程运行。而在iOS里面runloop就不太一样,因为有多线程的原因,runloop是配合多线程使用的。每一个线程都对应一个runloop。
Runloop最核心的事情就是保证程序的持续运行让线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。这个机制是依靠系统内核来完成的(苹果操作系统核心组件 Darwin 中的 Mach)
概念:RunLoop 是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。
1、没有消息处理时,休眠已避免资源占用,由用户态切换到内核态(CPU-内核态和用户态)
2、有消息需要处理时,立刻被唤醒,由内核态切换到用户态
main函数是不会退出的,为什么呢?这个时候就是 UIApplicationMain 内部默认开启了主线程的 RunLoop,并执行了一段无限循环的代码(不是简单的 for 循 环或 while 循环)
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain 函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保 持持续运行状态
// 伪代码
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
//执行的事件
} while(running)
return 0;
}
一、Runloop的实现机制
RunLoop 通过 mach_msg()函数接收、发送消息。它的本质是调用函数 mach_msg_trap(),相当于是一 个系统调用,会触发内核状态切换。
在用户态调用 时会切换到内核态;内核态中内核 实现的 mach_msg()函数会完成实际的工作。
二、Runloop 数据结构
是 CFRunLoop(CoreFoundation)的封装,提供了面向对象的 API 相关的主要涉及五个类:
CFRunLoop:Runloop对象 (由 pthread(线程对象,说明 和线程是一一对应的)、currentMode(当前所处的运行模式)、 modes(多个运行模式的集合)、 (模式名称字符串集合)、,Timer,Source 集合)构成)
CFRunLoopMode:运行模式(由 name、source0、source1、observers、timers 构成)
CFRunLoopSource:输入源/事件源 (分为 source0 和 source1 两种)
CFRunLoopTimer:定时源(基于时间的触发器,基本上说的就是 NStimer。在预设的时间点唤醒 执行回调。因为它是基于 RunLoop 的,因此它不是实时的(就是 NStimer 是不准确的。 因为只负责分发源的消息。如果 线程当前正在处理繁重的任务,就有可能导致 Timer 本次延时,或者少执行一次))
CFRunLoopObserver:观察者(对相关事件runloop的状态进行监听)
CFRunLoopSource分为两种source0和source1详解:
source0:
即非基于 port 的,也就是用户触发的事件。需要手动唤醒线程,将当前线程从内核态切换到用户 态source1:
基于 port 的,包含一个mach_port和一个回调,可监听系统端口和通过内核和其他线程发送的消息能主动唤醒Runloop接受分发系统事件 具备唤醒事件的能力
CFRunLoopObserver监听时间点详细事件
kCFRunLoopEntry RunLoop 准备启动
kCFRunLoopBeforeTimers RunLoop 将要处理一些 Timer 相关事件
kCFRunLoopBeforeSources RunLoop 将要处理一些 Source 事件
kCFRunLoopBeforeWaiting RunLoop 将要进行休眠状态,即将由用户态切换到内核态
kCFRunLoopAfterWaiting RunLoop 被唤醒,即从内核态切换到用户态后
kCFRunLoopExit RunLoop 退出
kCFRunLoopAllActivities 监听所有状态
线程和 RunLoop 一一对应, RunLoop 和 Mode 是一对多的,Mode 和 source、timer、observer 也是一对多 的
三、实现机制
Runloop运行的大致逻辑是:
通知观察者 RunLoop 即将启动。
通知观察者即将要处理 Timer 事件。
通知观察者即将要处理 source0 事件。
处理 source0 事件。
如果基于端口的源(Source1)准备好并处于等待状态,进入步骤 9。
通知观察者线程即将进入休眠状态。
将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程。
一个基于 port 的 Source1 的事件。
一个 Timer 到时间了。
RunLoop 自身的超时时间到了。
被其他调用者手动唤醒。
通知观察者线程将被唤醒。
处理唤醒时收到的事件
如果用户定义的定时器启动,处理定时器事件并重启 RunLoop。进入步骤 2。
如果输入源启动,传递相应的消息。
如果 RunLoop 被显示唤醒而且时间还没超时,重启 RunLoop。进入步骤 2
通知观察者 RunLoop 结束。
观察者observer 怎么监听Runloop,监听的状态
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 即将进入runloop
kCFRunLoopEntry = (1UL << 0),
// 即将处理timer
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理source
kCFRunLoopBeforeSources = (1UL << 2),
// 即将进入休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 休眠后唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出runloop
kCFRunLoopExit = (1UL << 7),
// runloop所有活动
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
四、runloop 和 线程
1、怎么创建一个常驻线程?
为当前线程开启一个 RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一 个 RunLoop)
向当前 RunLoop 中添加一个 Port/Source 等维持 RunLoop 的事件循环(如果 RunLoop 的 mode 中一个 item 都没有, runloop会退出)
启动该RunLoop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
2、如果我们开辟一个新的线程 加入了定时器的 这个时候定时器是不会执行的,我们看下下面的代码
- (void)viewDidLoad {
[super viewDidLoad];
NSLogMeth(@"1")
ygweakify(self);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
ygstrongify(self);
NSLogMeth(@"2")
[self performSelector: @selector(test) afterDelay:10];
NSLogMeth(@"3")
});
NSLogMeth(@"4")
}
- (void)test {
NSLogMeth(@"5")
}
答案是 1423,test 方法并不会执行。
原因是如果是带 afterDelay 的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的 RunLoop 中。 也就是如果当前线程没有开启 RunLoop,该方法会失效。
我们再看另一个
dispatch_async(dispatch_get_global_queue(0, 0), ^{
ygstrongify(self);
NSLogMeth(@"2")
[NSRunLoop currentRunLoop] run];
[self performSelector: @selector(test) afterDelay:10];
NSLogMeth(@"3")
});
NSLogMeth(@"4")
}
答案依然是 1423,test 方法并不会执行。
原因是如果 RunLoop 的 mode 中一个 item 都没有,RunLoop 会退出。即在调用 RunLoop 的 run 方法后,由 于其 mode 中没有添加任何 item 去维持 RunLoop 的时间循环,RunLoop 随即还是会退出。 所以我们自己启动 RunLoop,一定要在添加 item 后 所以我们把 开启runloop的代码 放在 延时方法之后 就好了
dispatch_async(dispatch_get_global_queue(0, 0), ^{
ygstrongify(self);
NSLogMeth(@"2")
[self performSelector: @selector(test) afterDelay:10];
[NSRunLoop currentRunLoop] run];
NSLogMeth(@"3")
});
NSLogMeth(@"4")
}
这个时候test的方法就执行了
3、怎样保证子线程数据回来更新 UI 的时候不打断用户的滑动操作?
当我们在子请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新 UI,那么就会影响当 前正在滑动的体验。
我们就可以将更新 UI 事件放在主线程的 上执行即可,这样就会等用户不再滑动页 面,主线程 RunLoop 由 切换到 时再去更新 UI
[self performSelectorOnMainThread: @selector(readload) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
4、NSTimer 在runloop中的关系
NSTimer其实就是 CFRunLoopTimerRef(基于时间的触发器) ,他们之间是tool-free bridged 的。一个 NSTimer 注册到RunLoop后, RunLoop会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。 RunLoop为了节省资源,并不会在非常准确的时间点回调这个 Timer。Timer 有个属性叫做Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样, 其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被 跳过去(和NSTimer相似),造成界面卡顿的感觉。在快速滑动 TableView 时,即使一帧的卡顿也会 让用户有所察觉。 FaceBook开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用 到了 RunLoop
五、异步绘制
异步绘制,就是可以在子线程把需要绘制的图形,提前在子线程处理好。将准备好图像数据直接返给主线程使用,这样可以降低主线程的压力。(一般情况下我们都是在主线程绘制的大家可以作为了解,特殊情况下在处理研究)
异步绘制的过程:
要通过系统的 [view.delegate displayLayer:] 这个入口来实现异步绘制。
代理负责生成对应的 Bitmap
设置该 Bitmap 为 layer.contents 属性的值。
来源:https://juejin.cn/post/7122380690439536653
猜你喜欢
- 类的参数定义将conda环境设置为ai,conda activate ai这个文件的由来:由于在yolov1的pytorch实现的损失函数中
- 在SQL Server数据库的维护或者Web开发中,有时需要在存储过程或者作业等其他数据库操作中调用其它的存储过程,下面介绍其调用的方法一、
- jieba 库是优秀的中文分词第三方库,中文文本需要通过分词获得单个的词语1、jieba库安装管理员身份运行cmd窗口输入命令:pip in
- SQL Server 2016 CTP2.2 安装配置教程下载一个iso文件,解压出来(大约2.8G左右),在该路径下双击Setup.exe
- 我就废话不多说了,大家还是直接看代码吧~clf=KMeans(n_clusters=5) #创建分类器对象fit_clf=clf.fit(X
- 如何用ASP发送带附件的邮件?请问如何用CDONTS组件发送带附件的邮件? 见下列代码:<%&nb
- 一:关于MySQL5 MySQL5系列数据库是MySQL的最新版本的数据库,比较流行的发行版是mysql-5.0.18。MySQL 英文官方
- 简介Button(按钮)组件用于实现各种各样的按钮。Button 组件可以包含文本或图像,你可以将一个 Python 的函数或方法与之相关联
- 目录1.程序结构2.选择语句2.1最简单的if语句2.2.if …… else 语句2.3.if…elif…else语句2.4 if 语句的
- 本文主要给大家介绍了关于Python中getpass模块的相关内容,分享出来供大家参考学习,话不多说了,来一起看看详细的介绍:getpass
- 如下所示:class bcolors: HEADER = '\033[95m' OKBLUE = &
- 一、前言这几天宅在家里网上冲浪,无意间看到了一个比较有趣的项目,就是使用 Python 语言实现对视频中的人物的眨眼进行计数并描绘在图表中。
- 引言继上一篇 《Blender Python 编程:快速入门》 我们已经了解了 Blender Python 脚本的基本概念。接下来让我们了
- Mr.Think是一个喜欢简洁的人,喜欢如诗一样的代码.不喜欢.NET的代码,就是因为经常看到同事用VS后生成出来的一段段冗长的代码.在我的
- 1.首先注册应用,获取 appkey、appsecretapi_url = "https://oapi.dingtalk.com/
- #!/usr/bin/env python# -*- coding: utf-8 -*-# @File : 自实现一个线性回归.py# @A
- ImageDataGenerator位于keras.preprocessing.image模块当中,可用于做数据增强,或者仅仅用于一个批次一
- 开篇语本文主要是回顾下从项目创建到生成数据到数据库(代码优先)的全部过程。采用EFCore作为ORM框架。本次示例环境:vs2019、net
- 然后我们在Interactive Python prompt中测试了一下:>>> import subprocess &n
- optim.Adam()解读torch.optim是一个实现了多种优化算法的包,大多数通用的方法都已支持,提供了丰富的接口调用,未来更多精炼