浅析RunLoop原理及其应用
yuyutoo 2025-01-03 19:49 3 浏览 0 评论
转载本文需注明出处:微信公众号EAWorld,违者必究。
引言:
一个APP的启动与结束都是伴随着RunLoop循环往复的,不断的循环、不断的往复。当线程被杀掉、APP退出后被系统以占用内存为由杀掉,RunLoop就消失了。但平时开发中很少见到RunLoop,为何它如此神秘?本文跟大家分享一下RunLoop的相关知识。
目录:
1、RunLoop的概念
2、RunLoop与线程的关系
3、RunLoop的常用模式
4、RunLoop的应用
1.RunLoop的概念
将英文拆解不难理解其实RunLoop表示一直在运行着的循环或者从上面的定义源码中可以看出就是一个do..while..循环。当启动一个iOS APP时主线程启动与其对应的RunLoop也已经开启。如果不杀掉APP则APP一直运行,就是因为RunLoop循环一直为开启状态保证主线程不会被摧毁。这也是RunLoop的作用之一保证线程不退出。RunLoop在循环过程中监听事件,当前线程有任务时,唤醒当当线程去执行任务,任务执行完成以后,使当前线程进入休眠状态。当然这里的休眠不同于我们自己写的死循环(while(1);),它在休眠时几乎不会占用系统资源,当然这是由操作系统内核去负责实现的。
UIApplicationMain()函数方法会默认为主线程设置一个NSRunLoop对象,这个循环会随时监听屏幕上由用户触摸所带来的底层消息并将其传递给主线程去处理,当点击一个button事件的传递从图上的调用栈可以看出。(监听的范围还包含时钟/网络)RunLoop循环与While循环的区别在于,RunLoop会在没有事件发生时进入休眠状态从而不占用CPU消耗,有事件发生才会去找对应的 Handler 处理事件,而While则会一直占用。在 Cocoa 程序的线程中都可以通过代码NSRunLoop *runloop = [NSRunLoop currentRunLoop];来获取到当前线程的Runloop对象。
RunLoop共有两套API接口 :1. Foundation框架NSRunLoop 2. Core Foundation框架CFRunLoopRef。NSRunLoop和CFRunLoopRef都代表着RunLoop对象,它们是等价的,可以互相转换。
NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)。
2.RunLoop与线程之间的关系
RunLoop和线程是相辅相成的,一个Runloop对应着一条唯一的线程,可以这样说RunLoop是为了线程而生,没有线程,它也没有存在的必要。RunLoop是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了RunLoop对象方便配置和管理线程的 RunLoop。每个线程,包括程序的主线程( main thread )都有与之相对应的 RunLoop对象。上图从 input source 和 timer source 接受事件,然后在线程中处理事件都是由RunLoop推动完成。
注意:开一个子线程创建runloop,不是通过alloc init方法创建,而是直接通过调用currentRunLoop方法来创建,它本身是一个懒加载的。在子线程中,如果不主动获取Runloop的话,那么子线程内部是不会创建Runloop的。
3.RunLoop的常用模式
RunLoop 的模式有五种。图上列出了其中两种分别是 NSDefaultRunLoopMode(默认模式) 和 UITRackingRunLoopMode(UI模式) 、NSRunLoopCommonModes(占位模式)。其实占位模式不是一个真正的模式,它相当于上面两种模式之和。苹果公开提供的 Mode 有两个NSDefaultRunLoopMode(kCFRunLoopDefaultMode) NSRunLoopCommonModes(kCFRunLoopCommonModes)。
4.RunLoop的应用
例如创建一个比较常见的注册页面,里面用NSTimer来自处理常见的验证码倒计时,每秒处理一下,如果NSTimer添加到的是默认模式的RunLoop这时候注册页面有一个展示注册协议的UITextView当用户滑动UITextView时验证码的倒计时是停止的,这是因为主线程的RunLoop模式是UI模式这个时候RunLoop循环是优先处理UI模式的任务而忽略了默认模式的计时器。此时解决上面的问题就需要用到NSRunLoopCommonModes(占位模式),这个模式相当于把NSTimer在两种模式下都添加了,这就不难理解为什么NSRunLoopCommonModes是一个复数形式了。这个模式下滑动UITextView或停止的时候RunLoop是在UITRacking和default模式下切换的(从打印日志中可以看出)。如果觉得NSTimer设置RunLoop模式很复杂可以尝试用GCD的Timer用法很简便。
RunLoop在TableView中的应用(解决滑动卡顿问题)。
如图代码展示,当加载高清大图渲染屏幕,而此时不得不在主线程操作,会引起滑动的卡顿。
tableview 在加载 cell 时如果遇到多个耗时操作会有点卡顿。将耗时操作放到 DefaultMode 里只能解决滑动时流畅,但是停止时需要加载耗时,仍然会有卡顿的感觉。正确方法是采用 RunLoop 监听,将多个耗时操作分开执行,在每次 RunLoop 唤醒时去做一个耗时任务。
阻塞原因:kCFRunLoopDefaultMode时候 多张图片(特别是高清大图)一起加载(耗时)loop不结束无法BeforeWaiting(即将进入休眠) 切换至UITrackingRunLoopMode来处理等候的UI刷新事件造成阻塞。
解决办法:每次RunLoop循环只加载一张图片 这样loop就会很快进入到BeforeWaiting处理后面的UI刷新(UITrackingRunLoopMode 优先处理)或者没有UI刷新事件继续处理下一张图片。
RunLoop 监听添加Observer (监听RunLoop的beforeWaiting)当处理完一张图片即将进入到beforeWaiting时处理数组里的tasks,这些任务就在callback里面做处理。
callBack拿到task处理了一部分就进入到了休眠 比如拿到18个任务只处理了7个就不处理了。
此处添加Timer是让RunLoop一直处于活跃状态 保证即使处理完所有task还是一直活跃状态。
注意:当CFRunLoopAddObserver(runloop, observer , kCFRunLoopDefaultMode); 添加到观察者时模式为kCFRunLoopDefaultMode 这样的的话只能监听到一般模式的BeforeWaiting,即不滑动的时候。所以图上的加载只在拖动结束时,而拖动UI时无任何加载。如下图:
所以这里可以再次优化,将模式改为kCFRunLoopCommonModes,这样的话滑动或者不滑动都可以加载图片渲染屏幕,而且是在不影响屏幕流畅性的基础上。如以下GIF:
源码:
#import "ViewController.h" ? @interface ViewController ()<UITableViewDelegate, UITableViewDataSource> @property (weak, nonatomic) IBOutlet UITableView *tableView; ? @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, strong) NSMutableArray *tasks; @property (nonatomic, assign) NSInteger maxTaskNumber; ? @end ? void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ //C语言与OC的交换用到桥接 __bridge //处理控制器加载图片的事情 ViewController *VC = (__bridge ViewController *)(info); if (VC.tasks.count == 0) { return; } void(^task)() = [VC.tasks firstObject]; task(); [VC.tasks removeObject:task]; NSLog(@"COUNT:%ld",VC.tasks.count); } ? @implementation ViewController ? - (void)viewDidLoad { [super viewDidLoad]; [self addRunloopOvserver]; self.maxTaskNumber = 18; self.tasks = [NSMutableArray array]; [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES]; } -(void)timerMethod{ } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ ViewController2 *vc2 = [ViewController2 new]; [self presentViewController:vc2 animated:YES completion:^{ }]; } ? - (void)addRunloopOvserver{ //获取当前的RunLoop CFRunLoopRef runloop = CFRunLoopGetCurrent(); //上下文 (此处为C语言 对OC的操作需要上下文)将(__bridge void *)self 传入到Callback CFRunLoopObserverContext context = {0, (__bridge void *)self, &CFRetain, &CFRelease}; //创建观察者 监听BeforeWaiting 监听到就调用回调callBack CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context); //添加观察者到当前runloop kCFRunLoopDefaultMode可以改为kCFRunLoopCommonModes CFRunLoopAddObserver(runloop, observer , kCFRunLoopCommonModes); //C语言中 有create就需要release CFRelease(observer); } ? ? - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return 30000; } ? - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identity" forIndexPath:indexPath]; NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode); //以下两个循环的UI操作在必须放在主线程,但是弊端就是太多图片的处理会阻塞tableview的滑动流畅性 for (int i = 1; i < 4; i++) { UIImageView *imageView = [cell.contentView viewWithTag:i]; [imageView removeFromSuperview]; } for (int i = 1; i < 4; i++) { /* 阻塞模式 */ // CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15; // UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)]; // [cell.contentView addSubview:imageView]; // imageView.tag = i; // imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]]; ? //阻塞原因:kCFRunLoopDefaultMode时候 多张图片一起加载(耗时)loop不结束无法BeforeWaiting(即将进入休眠) 切换至UITrackingRunLoopMode来处理等候的UI刷新事件造成阻塞 //解决办法:每次RunLoop循环只加载一张图片 这样loop就会很快进入到BeforeWaiting处理后面的UI刷新(UITrackingRunLoopMode 优先处理)或者没有UI刷新事件继续处理下一张图片 /* 流畅模式 */ //下面只是把任务放到数组 不消耗性能 void(^task)() = ^{ CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15; UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)]; [cell.contentView addSubview:imageView]; imageView.tag = i; imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]]; }; [self.tasks addObject:task]; //保证只拿最新的18个任务处理 if (self.tasks.count > self.maxTaskNumber) { [self.tasks removeObjectAtIndex:0]; } } return cell; } ? ? ? - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; }
关于作者:热河,普元移动端开发工程师,互联网技术爱好者,专注于iOS开发。目前参与Mobile 8.0项目的开发,主要接触RN技术的应用,黏合前端代码与iOS底层之间的交互。
关于EAWorld:微服务,DevOps,数据治理,移动架构原创技术分享。
相关推荐
- 全局和隐式 using 指令详解(全局命令)
-
1.什么是全局和隐式using?在.NET6及更高版本中,Microsoft引入了...
- 请停止微服务,做好单体的模块化才是王道:Spring Modulith介绍
-
1、介绍模块化单体是一种架构风格,代码是根据模块的概念构成的。对于许多组织而言,模块化单体可能是一个很好的选择。它有助于保持一定程度的独立性,这有助于我们在需要的时候轻松过渡到微服务架构。Spri...
- ASP.NET程序集引用之痛:版本冲突、依赖地狱等解析与实战
-
我是一位多年后端经验的工程师,其中前几年用ASP.NET...
- .NET AOT 详解(.net 6 aot)
-
简介AOT(Ahead-Of-TimeCompilation)是一种将代码直接编译为机器码的技术,与传统的...
- 一款基于Yii2开发的免费商城系统(一款基于yii2开发的免费商城系统是什么)
-
哈喽,我是老鱼,一名致力于在技术道路上的终身学习者、实践者、分享者!...
- asar归档解包(游戏arc文件解包)
-
要学习Electron逆向,首先要有一个Electron开发的程序的发布的包,这里就以其官方的electron-quick-start作为例子来进行一下逆向的过程。...
- 在PyCharm 中免费集成Amazon CodeWhisperer
-
CodeWhisperer是Amazon发布的一款免费的AI编程辅助小工具,可在你的集成开发环境(IDE)中生成实时单行或全函数代码建议,帮助你快速构建软件。简单来说,AmazonCodeWhi...
- 2014年最优秀JavaScript编辑器大盘点
-
1.WebstormWebStorm是一种轻量级的、功能强大的IDE,为Node.js复杂的客户端开发和服务器端开发提供完美的解决方案。WebStorm的智能代码编辑器支持JavaScript,...
- 基于springboot、tio、oauth2.0前端vuede 超轻量级聊天软件分享
-
项目简介:基于JS的超轻量级聊天软件。前端:vue、iview、electron实现的PC桌面版聊天程序,主要适用于私有云项目内部聊天,企业内部管理通讯等功能,主要通讯协议websocket。支持...
- JetBrains Toolbox推出全新产品订阅授权模式
-
捷克知名软件开发公司JetBrains最为人所熟知的产品是Java编程语言开发撰写时所用的集成开发环境IntelliJIDEA,相信很多开发者都有所了解。而近期自2015年11月2日起,JetBr...
- idea最新激活jetbrains-agent.jar包,亲测有效
-
这里分享一个2019.3.3版本的jetbrains-agent.jar,亲测有效,在网上找了很多都不能使用,终于找到一个可以使用的了,这里分享一下具体激活步骤,此方法适用于Jebrains家所有产品...
- CountDownTimer的理解(countdowntomars)
-
CountDownTimer是android开发常用的计时类,按照注释中的说明使用方法如下:kotlin:object:CountDownTimer(30000,1000){...
- 反射为什么性能会很慢?(反射时为什么会越来越长)
-
1.背景前段时间维护一个5、6年前的项目,项目总是在某些功能使用上不尽人意,性能上总是差一些,仔细过了一下代码发现使用了不少封装好的工具类,工具类里面用了好多的反射,反射会影响到执行效率吗?盲猜了一...
- btrace 开源!基于 Systrace 高性能 Trace 工具
-
介绍btrace(又名RheaTrace)是抖音基础技术团队自研的一款高性能AndroidTrace工具,它基于Systrace实现,并针对Systrace不足之处加以改进,核心改进...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- .NET 奇葩问题调试经历之3——使用了grpc通讯类库后,内存一直增长......
- 全局和隐式 using 指令详解(全局命令)
- 请停止微服务,做好单体的模块化才是王道:Spring Modulith介绍
- ASP.NET程序集引用之痛:版本冲突、依赖地狱等解析与实战
- .NET AOT 详解(.net 6 aot)
- 一款基于Yii2开发的免费商城系统(一款基于yii2开发的免费商城系统是什么)
- asar归档解包(游戏arc文件解包)
- 在PyCharm 中免费集成Amazon CodeWhisperer
- 2014年最优秀JavaScript编辑器大盘点
- 基于springboot、tio、oauth2.0前端vuede 超轻量级聊天软件分享
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)