当前位置:网站首页>理解 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
边栏推荐
- 为什么我们不使用GraphQL? - Wundergraph
- Five indispensable open source tools for me
- C + + adjacency matrix
- 5年程序员面试,常见面试问题解析
- File queue in Bifrost (1)
- Finally, the python project is released as exe executable program process
- 2.计算机硬件简介
- 14. Introduction to kubenetes
- 财富自由梦缓?蚂蚁金服暂停上市,监管后估值或下跌
- Mac 终端(terminal) oh-my-zsh+solarized配置
猜你喜欢
Finally, the python project is released as exe executable program process
WordPress Import 上传的文件尺寸超过php.ini中定义的upload_max_filesize值--&gt;解决方法。
Platform in architecture
商品管理系统——SPU检索功能
In 2020, what are the best tools for Android developers to break the cold winter?
5 个我不可或缺的开源工具
Mac 必备优质工具推荐
Commodity management system -- implementation of local preservation of new commodities
Capture bubbles? Is browser a fish?
从实践谈 Ruby 语法上的几个设计不一致带来的问题。
随机推荐
卧槽,这年轻人不讲武德,应届生凭“小抄”干掉5年老鸟,成功拿到字节20Koffer
Rainbow sorting | Dutch flag problem
MapStruct 解了对象映射的毒
14. Introduction to kubenetes
程序人生|从网瘾少年到微软、BAT、字节offer收割机逆袭之路
Recommendation system, in-depth paper analysis gbdt + LR
A solution to the problem that color picker (palette) cannot use shortcut keys in sublime Text3 plug-in
The difference between GDI and OpenGL
搭建全分布式集群全过程
自然语言处理(NLP)路线图 - kdnuggets
Have you ever thought about why the transaction and refund have to be split into different tables
“微服务”技术另一个可能更合适的名字
A bunch of code forgot to indent? Shortcut teach you carefree!
From the practice, this paper discusses the problems caused by the inconsistent design of ruby syntax.
BIOS of operating system
Commodity management system -- integrate warehouse services and obtain warehouse list
1486. Array XOR operation
GDI 及OPENGL的区别
1450. 在既定时间做作业的学生人数
百亿级数据分表后怎么分页查询?