当前位置:网站首页>理解 OC 中 RunLoop
理解 OC 中 RunLoop
2020-11-09 10:56:00 【osc_7dn4hojn】
理解 OC 中 RunLoop
什么是RunLoop?
可以简单理解为,让程序保持运行的一个while
循环,这个循环内监听各种事件(如触摸事件、performSelector
、定时器NSTimer
等),没有事件的时候睡眠,从而有效的利用CPU(只有在有事件的时候才用CPU,没事件的时候睡眠)
不管RunLoop有多复杂,其本质就是上面所说的:一个循环,有事件的时候处理事件,无事件的时候休眠(这里的睡眠是指用户态切换到内核态,这样的休眠线程是被挂起的,不会再占用cpu资源)。
RumLoop与线程有如下关系:
- 一个线程只有一个RunLoop对象
- 主线程的RunLoop默认已经创建好了,而子线程的需要手动创建。
- RunLoop在第一次获取时创建,在线程结束时销毁。
我们验证一下,在main
函数返回之前,打印一下:
int main(int argc, char *argv[])
{
NSString *appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
int ret = UIApplicationMain(argc, argv, nil, appDelegateClassName);
NSLog(@"after ret");
return ret;
}
结果没有打印,这说明主进程已经进入了一个RunLoop主了,主进程不结束,就跳不出RunLoop,也就执行不了之后的打印。
我们打印一下主线程的RunLoop试试:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@", [NSRunLoop currentRunLoop]);
}
// 打印结果(只取关键信息):
// CFRunLoop 0x600001704700
// current mode = kCFRunLoopDefaultMode,
这说明主线程在一个RunLoop中,并且当前的运行模式是kCFRunLoopDefaultMode
这样感觉RunLoop很简单,但它又很复杂,因为要考虑的因素有很多,比如各种事件的处理顺序,定时器、多线程等等
对于一个复杂问题,解决方法之一就是抽象,苹果为解决上面的问题,抽象出了RunLoop对象,RunLoop中包含多个Mode类,每个mode类中包含若干个 Source,Observer和Timer类,关系如下:
Mode是RunLoop的运行模式,有五类:
kCFRunLoopDefaultMode //App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode //界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode // 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode // 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes // 这是一个占位用的Mode,不是一种真正的Mode,可以简单理解为kCFRunLoopDefaultMode和UITrackingRunLoopMode的结合
这里的Source是事件源,比如触摸事件。
Observer是观察者,监听事件源的事件,可以简单理解为线程,比如主线程RunLoop的的Observer是主线程。
还有一些规定:
- RunLoop虽然有多个Mode,但RunLoop函数执行的时候,只能指定一个Mode
- 如果要切换Mode,需要等到一个Loop循环结束,再让新的Mode进入
上面说一个RunLoop只有一个Mode在执行,下面做个试验看看:
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextView *textView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
@end
这里我们在ViewControlller
里面创建了一个timer
,把他加到NSDefaultRunLoopMode
中,这个ViewControlller
有个可以滚动的UITextView
(继承UIScrollView
,UIScrollView
默认的Mode是UITrackingRunLoopMode
)
当我们滑动UITextView
的时候,timer
停止触发事件了,说明RunLoop的Mode从Default切换到了UITrackingRunLoopMode
解决方法就是把timer
放入kCFRunLoopCommonModes
中,这个Mode相当于同时是kCFRunLoopDefaultMode
和UITrackingRunLoopMode:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
上面是一个经典的例子,可以解决在UIScrollView
(包括其子类)中有NSTimer
定时的场景。
受此启发,我们可以用RunLoop解决卡顿问题,有一种卡顿问题就是UITableView
中有很多高清大图需要载入,在滑动屏幕的时候卡顿。
我们先分析一下卡顿的原因:最根本的原因是RunLoop转一圈的时间太长了,因为一次RunLoop循环需要解析很多张高清大图,系统渲染每一张高清大图都需要一定的时间,这样需要等到渲染的RunLoop结束之后,才能切换滑动屏幕RunLoop的Mode(UITrackingRunLoopMode),解决方法就是:
- 创建一个定时器:每间隔一定时间(可以是0.01s)执行一个空方法来唤醒RunLoop
- 将加载图片的方法装入block,将block加入一个有数量限制的数组,当block超过最大数量限制,移除最早添加的block
- 监听RunLoop的苏醒,苏醒回掉就执行一次就从数组中取出一个block事件执行,执行完的事件从数组中删除
这样设计让RunLoop的每次循环只执行一个加载图片的block(减少RunLoop单次循环的时间)。给数组设置一个最大数量限制,可以防止同一时间需要渲染的图片过多(减少RunLoop渲染图片的总时间)。
下面我们可以看看RunLoop里面长什么样了:
RunLoop内部逻辑
这里引入了新概念:source0是触摸事件和所有执行performSelector
方法,source1是基于port的线程间的通信。
这里我们可以大概看出RunLoop中处理事件的顺序,可以简要的总结为:
- 先通知Timer,Sources要处理事件了
- 处理source0
- 看看有没有source1,没有就休眠,有就不休眠
- 休眠状态下sources,timer,dispatch,手动都可以唤醒
- 3结束或者4唤醒后,就开始处理各种其他事件(timer,source1,dispatch)
- 如果第五步处理了至少一个事件,则开始新一轮的RunLoop,否则退出RunLoop
以上逻辑可以推出,在RunLoop中,只要有任何一个事件,RunLoop就不会退出,除非是RunLoop在休眠超时被唤醒或者外部强制停止,才会退出。
下面用一个例子感受一下RunLoop里的逻辑:
- (void)viewDidLoad {
[super viewDidLoad];
[self createObserver];
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
}
- (void)timerFired
{
NSLog(@"---- timer fired ----");
}
- (void)createObserver
{
//创建监听者
/*
第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
第三个参数 Boolean repeats:YES:持续监听 NO:不持续
第四个参数 CFIndex order:优先级,一般填0即可
第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); // 添加监听者,关键!
CFRelease(observer); // 释放
}
这里给RunLoop创建了一个观察者,观察者的回调打印RunLoop里的逻辑,另外有一个Timer每隔1.0秒触发一下。结果如下:
// 23:26:30 RunLoop醒来了
// 23:26:30 ---- timer fired ----
// 23:26:30 RunLoop要处理Timers了
// 23:26:30 RunLoop要处理Sources了
// 23:26:30 RunLoop要休息了
// 23:26:31 RunLoop醒来了
// 23:26:31 ---- timer fired ----
// 23:26:31 RunLoop要处理Timers了
// 23:26:31 RunLoop要处理Sources了
// 23:26:31 RunLoop要休息了
可以看到,Timer要触发的时候,唤醒了RunLoop,RunLoop醒来后去处理Timer,执行了Timer的方法(打印---- timer fired ----
),然后RunLoop回到循环的开头,通知观察者要处理Timers和Sources了,结果发现没有要处理的,然后就去休息了,如此循环。。。基本和上面的逻辑一致。
这里介绍一个RunLoop的应用:
创建一个常驻线程
首先我们创建一个继承自NSThread
的类BZThread
,用来打印销毁时候的信息,然后在viewDidLoad
中创建一个线程:
@interface BZThread : NSThread
@end
@implementation BZThread
- (void)dealloc {
NSLog(@"BZThread is dealloced");
}
@end
@interface ViewController ()
@property NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[BZThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
self.thread = thread;
[self.thread start];
}
- (void)threadTest {
NSLog(@"thread is created");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(doSomethingInThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)doSomethingInThread {
NSLog(@"doSomethingInThread is fired");
}
@end
// BZThread is created
我们发现,线程是被创建了,也被ViewControlelr
持有了(没有马上被销毁),但是我们在这个线程里执行方法没有反应,这说明这个线程的RunLoop没有运行起来。
解决方法是在这个线程方法里,给这个线程的RunLoop创建一个Mode:
- (void)threadTest {
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"thread is created");
}
点击屏幕,我们就执行了线程的方法了:
// doSomethingInThread is fired
这是因为,虽然一个线程对应一个RunLoop,但一个RunLoop至少需要一个Mode,才能跑起来,主线程默认就有Mode了,而新的线程需要我们手动去创建新的Mode。
最后介绍一个RunLoop的应用:
检测卡顿:
如果 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。
如何检查卡顿呢?需要创建一个持续的子线程专门用来监控主线程的 RunLoop 状态。一旦发现进入睡眠前的 状态,或者唤醒后的状态,在设置的时间阈值内一直没有变化,即可判定为卡顿。接下来,我们就可以 dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。
版权声明
本文为[osc_7dn4hojn]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/4380369/blog/4708786
边栏推荐
猜你喜欢
Commodity management system -- the search function of SPU
This program cannot be started because msvcp120.dll is missing from your computer. Try to install the program to fix the problem
File queue in Bifrost (1)
The difference between GDI and OpenGL
Initial installation of linx7.5
[Python从零到壹] 五.网络爬虫之BeautifulSoup基础语法万字详解
Rainbow sorting | Dutch flag problem
Depth first search and breadth first search
How to reduce the resource consumption of istio agent through sidecar custom resource
2.计算机硬件简介
随机推荐
1.操作系统是干什么的?
搭建全分布式集群全过程
详解Python input()函数:获取用户输入的字符串
Do you know how the computer starts?
结合阿里云 FC 谈谈我对 FaaS 的理解
When iperf is installed under centos7, the solution of make: * no targets specified and no makefile found. Stop
商品管理系统——SPU检索功能
1450. 在既定时间做作业的学生人数
Talk about my understanding of FAAS with Alibaba cloud FC
ThinkPHP门面源码解析
抢球鞋?预测股市走势?淘宝秒杀?Python表示要啥有啥
Chrome浏览器引擎 Blink & V8
2. Introduction to computer hardware
object
3.你知道计算机是如何启动的吗?
23 pictures, take you to the recommended system
无法启动此程序,因为计算机中丢失 MSVCP120.dll。尝试安装该程序以解决此问题
Introduction to nmon
Open source projects for beginners on GitHub (Python)
寻找性能更优秀的动态 Getter 和 Setter 方案