一.RunLoop定义
RunLoop:运行循环,简单的说就是处理线程事件和管理线程的一种机制。当子线程的事件结束时,runloop将会自动休眠,app主线程中的runloop处于一直唤醒状态。当用户触发事件时,runloop通知线程执行事件内容。
二.线程与RunLoop的关系
1.每条线程都有唯一的一个与之对应的RunLoop对象,没有线程,也就没有RunLoop存在的必要。
2.RunLoop在第一次获取时创建,在线程结束时销毁;只能在一个线程的内部获取其 RunLoop(主线程除外)。
3.主线程的RunLoop系统默认启动,子线程的RunLoop需要主动开启;
有时候我们感觉自己在实际开发中很少用到RunLoop,其实在我们每次建立项目的时候,就已经使用上了RunLoop。
在程序的启动入口 main 函数中有这样一段熟悉的代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain 函数内部就启动了一个与主线程相关联的 RunLoop。
当我们点击运行,系统运行 UIApplicationMain 函数,系统进入了:主线程 main 的运行循环。RunLoop 使得主线程一直处在运行循环中。
我们可以这样测试下UIApplicationMain的作用,用下面代码代替:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@“启动”);
return 0;
}
}
结果: 程序打印出“启动”后,就直接关闭了,控件与其他程序有关的都没有执行,界面空白,这说明了在 UIApplicationMain 函数中,开启了一个和主线程相关的 RunLoop,让 UIApplicationMain 不会返回,一直在运行中,也就保证了程序的持续运行。这就是为什么App程序启动之后能够持续运行在前台的原因。
三. RunLoop 对象和相关类
iOS中有2套API来访问和使用RunLoop:
Foundation:NSRunLoop
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation:CFRunLoopRef
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
文档中的相关类:
CFRunLoopRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopModeRef
CFRunLoopObserverRef`
他们的关系如下图:
关系图
一个RunLoop包含若干个Mode,而每个Mode又包含若干个Source/Timer/Observer。
RunLoop每次只能指定一种Mode。而且如果需要切换 Mode,只能退出当前 Loop。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
如果一个 mode 中一个 “Source/Timer/Observer” 都没有,则 RunLoop 会直接退出,不进入循环。
CFRunLoopSourceRef 输入源
是事件产生的地方,函数调用栈上Source有两个版本:Source0 和 Source1。
Source0:非基于端口port,例如触摸,滚动,selector选择器等用户触发的事件;(只包含了一个回调函数,它并不能主动触发事件)
Source1:基于端口port,一些系统事件; (包含了一个 mach_port 和一个回调函数,被用于通过内核和其他线程相互发送消息。能主动唤醒 RunLoop 的线程)
CFRunLoopTimerRef 定时源
基于时间的触发器,与NSTimer可混用。
包含了一个时间长度和一个回调函数。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
创建定时器源有两种方法,
方法一:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0
target:self
selector:@selector(backgroundThreadFire:) userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];
方法二:
[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(backgroundThreadFire:)
userInfo:nil
repeats:YES];
CFRunLoopModeRef mode类型
事实上CFRunLoopModeRef 类并没有对外暴露,而如果在Xcode中查看CFRunLoopRef,可以看到CFRunLoopModeRef 类,通过 CFRunLoopRef 的接口进行了封装。
CFRunLoopModeRef有5种形式:
kCFRunLoopDefaultMode 默认模式,通常主线程在这个模式下运行;
UITrackingRunLoopMode 界面跟踪Mode,用于追踪Scrollview触摸滑动时的状态;
kCFRunLoopCommonModes 占位符,带有Common标记的字符串,比较特殊的一个mode;
UIInitializationRunLoopMode 刚启动App时进入的第一个Mode,启动后不在使用;
GSEventReceiveRunLoop 内部Mode,接收系事件。
从关系图,我们可以知道 RunLoop 一次只能指定一种 Mode,且能够让不同组的 Source/Timer/Observer 互不影响.
CFRunLoopObserverRef 观察者
RunLoop的观察者,能够监听RunLoop的状态改变。
每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化,可以观察到不同时刻的状态有以下几个:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
四、在实际开发中的应用
(1). 控制线程生命周期(线程保活)
在项目中,有时我们需要创建子线程,因为如果把所有的事情都放在主线程中去做,就会阻塞住主线程。导致APP 看起来很卡。这个时候就可以开启一个子线程,把耗时的操作放到子线程中。子线程做完事情以后,就会销毁。有时我们不希望子线程大量的创建和销毁,就可以使用 RunLoop 控制子线程的生命周期。
-(void)tapBtn{
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(todo) object:nil];
[thread start];
}
-(void)todo{
NSLog(@“执行此方法”);
NSLog(@"%@", [NSThread currentThread]);
}
每一次点击按钮的时候,线程执行完方法,直接释放掉了,下一次直接创建了一个新的线程
使用 RunLoop 控制子线程保活
/** 线程对象 */
@property(strong,nonatomic) NSThread *thread;
@end
@implementation MyPurseViewController
(void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc]initWithFrame: CGRectMake(50, 150, 200, 80)];
btn.backgroundColor = [UIColor redColor];
[btn addTarget:self action:@selector(tapBtn) forControlEvents:UIControlEventTouchUpInside];
[btn setTitle:@“点击测试线程” forState:UIControlStateNormal];
[self.view addSubview:btn];
_thread = [[NSThread alloc]initWithTarget:self selector:@selector(todo) object:nil];
[_thread start];
// Do any additional setup after loading the view.
}
-(void)tapBtn{
NSLog(@"%@", [NSThread currentThread]);
}
-(void)todo{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
(2). 解决NSTimer在UIScrollView滑动时停止工作的问题
默认情况下,在滚动 tableView、UIScrollView 的时候,NSTimer会停止工作,这是因为在滚动时,RunLoop 会进入另一个Mode 模式UITrackingRunLoopMode 下,在该模式下,定时器就会停止,当不在滚动 UITextView , 定时器会重新开始。 RunLoop 同一时间,只能运行一种模式。
例如:UIScrollView+ NSTimer演示滚动时,定时器停止工作
(void)viewDidLoad {
[super viewDidLoad];
UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, kScreenWidth, kScreenHeight)];
[self.view addSubview:scrollView];
scrollView.backgroundColor = [UIColor redColor];
scrollView.contentSize = CGSizeMake(0, 3000);
static int count = 0;
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@“该方法第%d次”,++count);
}];
}
从后台打印日志看出,当我们在手机屏幕上滑动时,定时器不工作,日志不打印,放开手后,定时器重新工作,开始打印。(应用最常见的应该为轮播图自动播放时)
边滚动,定时器边工作,我们就可以用NSRunLoop的默认模式:
static int count = 0;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@“该方法第%d次”,++count);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
(3). 监控应用卡顿
有时我们在滑动列表时,感觉很卡,特别是列表上有很多图片要显示时,如何解决卡顿呢,因为我们现在加载图片用的SDWebImage,源码中已经处理了该问题,所以有时我们滑动列表时很顺畅。
(4). 性能优化
一个RunLoop对应一个线程
建议每一次启动RunLoop的时候,包装一个自动释放池,临时创建了很多对象,等着我们释放,在很多优秀的开源库中,都有这个说明
(void)viewDidLoad {
[super viewDidLoad];
_thread = [[NSThread alloc] initWithTarget:self
selector:@selector(todo)
object:nil];
[_thread start];
}
(void) todo{
// 该方法默认不加入RunLoop中,使用schedule可以
@autoreleasepool {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.3
target:self
selector:@selector(test2)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
}
作者:honey缘木鱼
链接:https://www.jianshu.com/p/10256855319a
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。