RunLoop
就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束,函数返回。main
函数中的Runloop
:UIApplication
函数内部就启动了一个Runloop
该函数返回一个int类型的值;Runloop
是跟主线程相关联的。CoreFoundation
里面的CFRunLoopRef
CFRunLoopRef
是在 CoreFoundation
框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。CFRunLoopRef
是开源的,其源码链接如下:Foundation
里面的NSRunLoop
NSRunLoop
是基于 CFRunLoopRef
的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。NSRunLoop
和CFRunLoopRef
都代表着RunLoop
对象RunLoop
。RunLoop
之间是一一对应的,其关系是保存在一个全局的 Dictionary
里。RunLoop
,如果你不主动获取,那它一直都不会有。RunLoop
的创建是发生在第一次获取时(懒加载),RunLoop
的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop
(主线程除外)。CoreFoundation
里面关于 RunLoop
有5个类:CFRunLoopRef
RunLoop的对象CFRunLoopModeRef
RunLoop的运行模式CFRunLoopSourceRef
输入源/事件源CFRunLoopTimerRef
Timer事件CFRunLoopObserverRef
监听者,监听RunLoop的状态改变Runloop
和相关类之间的关系图:RunLoop
对象(CFRunLoopRef
)中包含若干个运行模式(CFRunLoopModeRef
)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef
)、Timer
事件(CFRunLoopTimerRef
)、观察者(CFRunLoopObserverRef
)。RunLoop
启动时,只能指定其中一个Mode
,这个Mode
被称作 CurrentMode
。如果需要切换Mode
,只能退出RunLoop
,再重新指定一个 Mode
进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer
,让其互不影响。CFRunLoopRef
就是CoreFoundation
框架下RunLoop
对象类,不能直接创建,只能获取:CFRunLoopGetMain();
// 获得主线程的RunLoop
对象CFRunLoopGetCurrent();
// 获得当前线程的RunLoop
对象Foundation
框架下获取RunLoop
对象类的方法如下:[NSRunLoop mainRunLoop];
// 获得主线程的RunLoop对象[NSRunLoop currentRunLoop];
// 获得当前线程的RunLoop对象CFRunloopModeRef
代表着Runloop
的运行模式,一个Runloop
中可以有多个Mode
,一个Mode
里面又可以有多个source\observer\timer
,每次runloop
启动的时候,只能指定一个Mode
,这个Mode
被称为该Runloop
的当前Mode
,如果需要切换Mode
,只能先退出当前Runloop
,再重新指定一个Mode
进入,分割不同组的定时器等,让他们相互之间不受影响。Mode
:kCFRunLoopDefaultMode
:App的默认Mode,通常主线程是在这个Mode下运行。UITrackingRunLoopMode
:跟踪用户交互事件Mode(ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。)UIInitializationRunLoopMode
: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。GSEventReceiveRunLoopMode
: 接受系统事件的内部 Mode,通常用不到。kCFRunLoopCommonModes
: 这是一个占位用的Mode,不是一种真正的Mode。CFRunLoopSourceRef
是事件源,CFRunLoopSourceRef
有两种分类方法:main
函数,main函数调用16行UIApplicationMain
函数,然后一直往上调用函数,最终调用到0行的btnClick
函数,即点击函数。同时我们可以看到11,12行中有Sources0
,也就是说我们点击事件是属于Sources0
函数的,点击事件就是在Sources0
中处理的。而至于Sources1
,则是用来接收、分发系统事件,然后再分发到Sources0
中处理的。CFRunLoopTimerRef
是定时源,理解为基于时间的触发器,时间到了我就触发一个事件,触发一个操作。基本上说的就是NSTimer
。下面结合CFRunLoopModeRef
举例说明:UITextView
,在viewDidLoad
方法中创建定时器并每两面调用一次block中的方法:- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger num = 0;
//创建定时器 ,没两秒调用一下block里面的方法
NSTimer * timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * `_Nonnull` timer) {
num ++;
NSLog(@"%zd次",num);
}];
//将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
UITextView
时,打印就会中断(暂停);停止操作后又会继续打印。RunLoop
处于NSDefaultRunLoopMode
下;当我们拖动UITextView
的时候,RunLoop就结束NSDefaultRunLoopMode
,切换到了UITrackingRunLoopMode
模式下,这个模式下没有添加NSTimer
,所以我们的NSTimer
就不工作了;当我们松开鼠标的时候,RunLoop
就结束UITrackingRunLoopMode
模式,又切换回NSDefaultRunLoopMode
模式,所以NSTimer
就又开始正常工作了。RunLoop
的模式改为UITrackingRunLoopMode
模式,会发现只有在滚动UITextView
时,定时器的block
中的打印方法才会调用。kCFRunLoopCommonModes
,NSFoundation
框架下为NSRunLoopCommonModes
)CommonModes
标记的模式下运行,如下修改即可:[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSTimer
中的scheduledTimerWithTimeInterval
方法和RunLoop
的关系如下:[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
//等等同于下面两行代码
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
####4.5.CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,用来监听RunLoop
的状态改变、可以监听的状态改变有以下几种:typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒:64
kCFRunLoopExit = (1UL << 7), // 即将从RunLoop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变
};
监听示例如下:
- (void)viewDidLoad {
[super viewDidLoad];
//创建监听者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop状态发生改变---%zd",activity);
});
//添加观察者到当前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//最后添加完需要释放掉
CFRelease(observer);
}
打印结果如下:
结果可见:RunLoop
的状态在不断的改变,最终变成了状态 32,也就是即将进入睡眠状态,说明RunLoop
最后就会进入睡眠状态。
###5.Runloop运行逻辑
UITableView
,而且每个UITableViewCell
里边都有图片。这时候当我们滚动UITableView
的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象。UIScrollView
的滚动UITableView
继承自UIScrollView
,所以我们可以通过监听UIScrollView
的滚动,实现UIScrollView
相关delegate
即可。PerformSelector
设置当前线程的RunLoop
的运行模式performSelector
方法为UIImageView
调用setImage:
方法,并利用inModes
将其设置为RunLoop
下NSDefaultRunLoopMode
运行模式。代码如下:- (void)viewDidLoad {
[super viewDidLoad];
__block NSInteger num = 0;
// 创建定时器 ,没两秒调用一下block里面的方法
NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
num ++;
NSLog(@"%zd秒",num);
}];
//将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//延迟6秒设置图片
[self.imageV performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"we"] afterDelay:6.0 inModes:@[NSDefaultRunLoopMode]];
}
结果如下:
结果可见:当滚动UITextView时,6秒后图片并没有显示,当停止滚动时图片才显示。
做法
:添加一条用于常驻内存的强引用的子线程,在该线程的RunLoop
下添加一个Sources
,开启RunLoop
。示例:
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic ,strong) NSThread * thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//初始化线程
self.thread = [[NSThread alloc]initWithBlock:^{
NSLog(@"runThread:%@",[NSThread currentThread]);
// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
NSRunLoop * currentRL = [NSRunLoop currentRunLoop];
[currentRL addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[currentRL run];
// 如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"NOopenRunLoop");
}];
//开启线程
[self.thread start];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES modes:@[NSDefaultRunLoopMode]];
}
- (void)test{
NSLog(@"testThread:%@",[NSThread currentThread]);
}
结果可见:除了之前打印的runThread
外,每当我们点击屏幕,都打印testThread
,但是没有打印NOopenRunLoop
说明开启了RunLoop
循环。
参考文档:
深入理解RunLoop
RunLoop苹果官方文档
iOS多线程相关文章:
iOS多线程简述
iOS多线程-pthread、NSThread
iOS多线程-GCD
iOS多线程-NSOperation, NSOperationQueue
iOS多线程-RunLoop
OC单例模式详解