浅析RunLoop原理及其应用
yuyutoo 2025-01-03 19:49 1 浏览 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,数据治理,移动架构原创技术分享。
相关推荐
- 如何在HTML中使用JavaScript:从基础到高级的全面指南!
-
“这里是云端源想IT,帮你...
- 推荐9个Github上热门的CSS开源框架
-
大家好,我是Echa。...
- 硬核!知网首篇被引过万的论文讲了啥?作者什么来头?
-
整理|袁小华近日,知网首篇被引量破万的中文论文及其作者备受关注。知网中心网站数据显示,截至2021年7月23日,由华南师范大学教授温忠麟等人发表在《心理学报》2004年05期上的学术论文“中介效应检验...
- 为什么我推荐使用JSX开发Vue3_为什么用vue不用jquery
-
在很长的一段时间中,Vue官方都以简单上手作为其推广的重点。这确实给Vue带来了非常大的用户量,尤其是最追求需求开发效率,往往不那么在意工程代码质量的国内中小企业中,Vue占据的份额极速增长...
-
- 【干货】一文详解html和css,前端开发需要哪些技术?
-
网站开发简介...
-
2025-02-20 18:34 yuyutoo
- 分享几个css实用技巧_cssli
-
本篇将介绍几个css小技巧,目录如下:自定义引用标签的符号重置所有标签样式...
- 如何在浏览器中运行 .NET_怎么用浏览器运行代码
-
概述:...
- 前端-干货分享:更牛逼的CSS管理方法-层(CSS Layers)
-
使用CSS最困难的部分之一是处理CSS的权重值,它可以决定到底哪条规则会最终被应用,尤其是如果你想在Bootstrap这样的框架中覆盖其已有样式,更加显得麻烦。不过随着CSS层的引入,这一...
-
- HTML 基础标签库_html标签基本结构
-
HTML标题HTML标题(Heading)是通过-...
-
2025-02-20 18:34 yuyutoo
- 前端css面试20道常见考题_高级前端css面试题
-
1.请解释一下CSS3的flexbox(弹性盒布局模型),以及适用场景?display:flex;在父元素设置,子元素受弹性盒影响,默认排成一行,如果超出一行,按比例压缩flex:1;子元素设置...
- vue引入外部js文件并使用_vue3 引入外部js
-
要在Vue中引入外部的JavaScript文件,可以使用以下几种方法:1.使用``标签引入外部的JavaScript文件。在Vue的HTML模板中,可以直接使用``标签来引入外部的JavaScrip...
- 网页设计得懂css的规范_html+css网页设计
-
在初级的前端工作人员,刚入职的时候,可能在学习前端技术,写代码不是否那么的规范,而在工作中,命名的规范的尤为重要,它直接与你的代码质量挂钩。网上也受很多,但比较杂乱,在加上每年的命名都会发生一变化。...
- Google在Chrome中引入HTML 5.1标记
-
虽然负责制定Web标准的WorldWideWebConsortium(W3C)尚未宣布HTML5正式推荐规格,而Google已经迁移到了HTML5.1。即将发布的Chrome38将引入H...
- HTML DOM 引用( ) 对象_html中如何引用js
-
引用对象引用对象定义了一个同内联元素的HTML引用。标签定义短的引用。元素经常在引用的内容周围添加引号。HTML文档中的每一个标签,都会创建一个引用对象。...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 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)