iOS底层原理探索—RunLoop的应用
探索底层原理,积累从点滴做起。大家好,我是Mars。
往期回顾
iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
iOS底层原理探索— 关联对象的本质
iOS底层原理探索— block的本质(一)
iOS底层原理探索— block的本质(二)
iOS底层原理探索— Runtime之isa的本质
iOS底层原理探索— Runtime之class的本质
iOS底层原理探索— Runtime之消息机制
iOS底层原理探索—RunLoop的本质
我们在iOS底层原理探索—RunLoop的本质一文中带领大家从底层源码入手,分析了RunLoop
的底层结构和运行逻辑,今天继续研究关于RunLoop
的应用。
RunLoop的应用
基于RunLoop
的特点,在以下场景中会用到RunLoop
:
- 解决NSTimer失效问题
- 控制线程生命周期(线程保活)
- 自动释放池
- 监控应用卡顿
- 性能优化
1、解决NSTimer失效问题
当使用定时器NSTimer
做计时或者指定时间执行某件事的时候,如果我们滑动UIScrollView
、UITableView
等可以滚动的控件时,定时器NSTimer
就会暂停,当停止滑动以后,定时器NSTimer
又会重新恢复计时。
这是由于我们创建定时器NSTimer
,并把定时器添加到RunLoop
中,如果没有给定时器设置运行模式Mode
的话,系统会选择默认运行模式kCFRunLoopDefaultMode
。
当发生滑动操作时,RunLoop
的Mode
会自动切换成UITrackingRunLoopMode
模式,定时器NSTimer
仍然是kCFRunLoopDefaultMode
的模式。由于RunLoop
同一时间只能运行在某一种模式,因此定时器就会失效。当停止滑动后,RunLoop
又会切换回kCFRunLoopDefaultMode
模式,因此定时器又会重新启动。
所以要想解决定时器失效问题,就要保证定时器的Mode
跟RunLoop
的Mode
同步。当RunLoop
的Mode
切换成UITrackingRunLoopMode
模式时,定时器的Mode
也要做出改变。这时我们就可以用NSRunLoopCommonModes
来完成。
NSRunLoopCommonModes
NSRunLoopCommonModes
是占位模式,并不是一种真正的模式。当设置了NSRunLoopCommonModes
后,就会被标记成CommonModes
。在RunLoop
底层结构中,存在一个集合CFMutableSetRef
,里面存放这一些通用的Mode
,包括UITrackingRunLoopMode
和kCFRunLoopDefaultMode
。
被NSRunLoopCommonModes
标记后,就可以在UITrackingRunLoopMode
和kCFRunLoopDefaultMode
这两种模式下运行了。
下面是具体代码:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
//设置定时器的Mode,将定时器添加到主线程RunLoop中
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
-(void)show
{
NSLog(@"-------");
}
2、控制线程生命周期(线程保活)
我们来看下面这道面试题:
[NSObject performSelector: withObject: afterDelay:];这个方法在子线程中执行会有什么问题?
答案是不会执行,这个方法会在当前线程的RunLoop
中添加定时器,但是由于子线程的RunLoop
没有启动,所以定时器也不会计时,方法也不会执行。
我们用代码验证一下:
我们在子线程中执行performSelector: withObject: afterDelay:
方法,通过打印可以看到,run
方法已经执行完了,test
方法都没有执行。
那么如何解决这个问题呢?既然子线程没有开启RunLoop
,那么我们可以通过手动开启子线程的RunLoop
,就可以执行test
方法了。
代码如上,我们通过在子线程中开启
RunLoop
,当点击屏幕时,test
方法执行并完成了打印。通过打印线程,可以看出任务的执行的确在子线程。
但是有一个问题,我们在子线程的run
方法中有打印end
的输出,但是任务都执行完了,都没有打印end
,这是为什么呢?
问题就出在[[NSRunLoop currentRunLoop] run];
这句代码。首选我们看一下官方是如何解释的:
通过官方解,我们得知,
RunLoop
的run
方法是运行在NSDefaultRunLoopMode
模式下,会重复调用runMode:beforeDate:
方法,并且是无限循环。这就说明,
NSRunLoop
的run
方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop
),所以不会打印end
。
那么既然调用系统的run
方法有瑕疵,我们就自己实现一下:
#import "ViewController.h"
#import "MThread.h"
@interface ViewController ()
@property (strong, nonatomic) MThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//控制器非空,并且没有停止子线程的RunLoop
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (!self.thread) return;
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
//点击停止按钮,停止子线程的RunLoop
- (IBAction)stop {
if (!self.thread) return;
// 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 用于停止子线程的RunLoop
- (void)stopThread
{
// 设置标记为YES
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 清空线程
self.thread = nil;
}
//控制器销毁的时候也要停止子线程的RunLoop
- (void)dealloc
{
[self stop];
}
@end
3、 自动释放池
Timer
和Source
也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大,这显然不是我们想要的。
那么什么时候释放,怎么释放呢?
RunLoop
内部有一个自动释放池,当RunLoop
开启时,就会自动创建一个自动释放池,当RunLoop
在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop
被唤醒重新开始跑圈时,Timer
、Source
等新的事件就会放到新的自动释放池中,当RunLoop
退出的时候也会被释放。
关于RunLoop
的探索今天就告一段落,如有疑问欢迎留言交流。
更多技术知识请关注公众号
iOS进阶
iOS进阶.jpg