前言:一般代码运行完就结束了,为何APP就一直能保持运行状态呢?这个秘诀就是RunLoop,本文先介绍了RunLoop的概念,引出它的作用,并在CoreFoundation框架源码查看它的底层结构;然后介绍RunLoop和线程的关系,并介绍它运行的各种模式;最后介绍RunLoop在实际项目中的应用,比如解决NSTimer在滑动时停止工作的问题,线程保活和性能优化等问题。
一、RunLoop概念
1、RunLoop概念:
顾名思义,运行循环,在程序运行过程中循环做一些事情。应用范畴有:定时器(Timer)、PerformSelector、GCD、事件响应、手势识别、界面刷新、网络请求、AutoreleasePool等。
2、RunLoop作用:
如果没有RunLoop,运行代码
int main(int argc, char * argv[]) {
@autoreleasepool {
// return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"Hello world!");
}
return 0;
}
程序运行完就结束了,App也就退出了,所以RunLoop作用有以下几条:
1)保持程序的持续运行;
2)处理App中的各种事件(比如触摸事件、定时器事件等);
3)节省CPU资源,该做事时做事,该休息时休息等;
3、RunLoop构成:
下载CoreFoundation框架源码, 在CFRunLoop.c文件中找到RunLoop的底层结构:
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list 线程锁 */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //RunLoop对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes; //记录所有标记为common的mode
CFMutableSetRef _commonModeItems; //存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; //当前运行的mode
CFMutableSetRef _modes; //存储的是CFRunLoopModeRef
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
可见,RunLoop底层是一个结构体,主要包含了一个线程,当前运行的Mode,若干个commonMode,若干个commonModeItem等。
二、RunLoop和线程
程序运行的时候需要一个常驻线程,可以让线程接收到事件的时候干活,没事的时候休眠。我们执行下面的伪代码,一直等待消息,线程就不会退出了。
do {
// 睡眠中等待消息
// 接收消息
// 处理消息
} while (消息 != 退出)
那么RunLoop和线程的关系是怎样呢?
1)每条线程都有唯一的一个与之对应的RunLoop对象;
2)RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value;
3)线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;
4)RunLoop会在线程结束时销毁;
5)主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop;
在RunLoop官方文档中,可以看到RunLoop和线程的关系
图中展现了RunLoop 在线程中的作用:从 input sources 和 timer sources 接受事件,然后在线程中处理事件。
三、RunLoop对象
1、获取RunLoop对象:
iOS中有两套来访问和使用RunLoop。
1)Foundation框架:NSRunLoop,NSRunLoop是基于CFRunLoopRef的OC的封装;
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
2)CoreFoundation框架:CFRunLoopRef;
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
2、CFRunLoopModeRef-运行模式:
Mode模式可以看做事件的管家,一个Mode管理着各种事件,在CFRunLoop.c文件中结构:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode名称
Boolean _stopped; //mode是否被终止
char _padding[3];
CFMutableSetRef _sources0; //sources0 触摸事件处理,performSelector:onThread:
CFMutableSetRef _sources1; //sources1 基于port的线程通信,系统事件捕捉
CFMutableArrayRef _observers; //观察者 用于监听RunLoop状态,UI刷新,AutoreleasePool
CFMutableArrayRef _timers; //定时器
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet; //端口
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
一个CFRunLoopMode对象有一个name,若干个sources0、sources1、observers、timers和ports,可见事件都是由Mode在管理,而RunLoop管理Mode。RunLoop运行时总是指定一种Mode,就是currentMode,当切换Mode时必须退出当前Mode,重新进入RunLoop。
在iOS中五种模式如下图:
注意:实际开发中有三个使用
1)NSDefaultRunLoopMode:这个是默认模式,使用最多;
2)UITrackingRunLoopMode:界面追踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响;
3)NSRunLoopCommonModes:这个不是一种模式,默认包括NSDefaultRunLoopMode和UITrackingRunLoopMode;
比如常用定时器代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 创建tableView
[self tableView];
// 创建一个定时器,并把它放在当前的RunLoop上,模式是NSDefaultRunLoopMode
// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
// 创建一个定时器,但是必须手动把它添加到RunLoop中,添加的时候可以指定Mode
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
// 默认模式,运行执行timerAction,当滑动tableView时,timerAction就不会执行
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
// 通用模式组合,运行执行timerAction,当滑动tableView时,timerAction依然继续执行
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
// UI追踪模式,此时默认不执行timerAction,当滑动tableView时,才会执行timerAction
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:UITrackingRunLoopMode];
}
四、RunLoop项目应用
1、解决NSTimer在滑动时停止工作的问题
1)NSTimer:如上讲述,创建timer后,将它放进当前的RunLoop,Mode格式为NSRunLoopCommonModes,NSRunLoopCommonModes包含NSDefaultRunLoopMode(没有触摸事件)和
UITrackingRunLoopMode(有触摸事件),所以不管有没有滑动事件都会执行timer。NSTimer有个属性tolerance容忍度,NSTimer依赖于RunLoop,所以执行timer的时间不会非常准确,使用的时候需要注意这点。
2)GCD Timer:GCD定时器,使用GCD定时器不会受RunLoop的影响;
- (void)viewDidLoad {
[super viewDidLoad];
// 创建tableView
[self tableView];
// 创建一个定时器(dispatch_source_t本质还是个OC对象)
dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
self.timer = sourceTimer; //持有
// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒执行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(sourceTimer, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(sourceTimer, ^{
NSLog(@"num = %ld", self.num ++);
});
// 启动定时器
dispatch_resume(sourceTimer);
}
运行代码,滑动tableView,可以看到打印台仍然有执行block的回调,也就是滑动事件不会影响GCD定时器。
3)CADisplayLink:是一个执行频率(fps)和屏幕刷新相同的定时器(可以修改preferredFramesPerSecond属性来修改具体执行的频率)。时间精度比NSTimer高,但是也要添加到RunLoop里。通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅,而NSTimer则有更广泛的用处。
- (void)viewDidLoad {
[super viewDidLoad];
// 创建tableView
[self tableView];
CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerAction)];
self.timer = timer;
if (@available(iOS 10.0, *)) {
timer.preferredFramesPerSecond = 30; //30帧
} else {
timer.frameInterval = 2; //屏幕刷新60帧,每2帧刷一次,就是每秒30帧频率
}
// 添加到NSRunLoopCommonModes
[timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
运行代码,滑动tableView,当然也会执行timerAction,与NSTimer类似。
2、控制线程生命周期(线程保活)
有时候我们需要常驻线程来处理频繁的事务,比如早期的AFNetworking创建一个常驻线程处理网络事务,比如监测网络状态等。
默认情况一个线程创建出来,运行完要做的事情,线程就会消亡。而程序启动的时候,创建的主线程已经加入到mainRunLoop中,所以主线程不会消亡。
线程保活代码:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"mainThread = %@", [NSThread mainThread]);
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
self.thread = thread;
thread.name = @"KeepThread";
[self.thread start];
}
// 子线程运行
- (void)runThread {
NSLog(@"runThread = %@", [NSThread currentThread]);
// 给runloop添加一个NSPort,就是添加一个事件源,也可以添加一个timer,或者observer,让runloop不会挂掉
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"-----runThread end-----"); //不会执行
}
// 测试线程保活
- (void)testThread {
NSLog(@"testThread = %@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(testThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
点击屏幕触发performSelector,打印结果:
mainThread = {number = 1, name = main} //主线程
runThread = {number = 5, name = KeepThread} //运行子线程
testThread = {number = 5, name = KeepThread} //子线程保活
3、性能优化
1)tableView图片显示优化:由于图片渲染到屏幕需要消耗较多资源,为了提高用户体验,当用户滚动Tableview的时候,只在后台下载图片,但是不显示图片,当用户停下来的时候才显示图片。
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:1.0 inModes:@[NSDefaultRunLoopMode]];
2)AsyncDisplayKit:AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架(最新的改为Texture),其原理大致如下:
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。
ASDK 仿照QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
3)第二条是Facebook写好的框架,下面自己手动写下计算cell的预缓存高度的伪代码:
- (void)viewDidLoad {
[super viewDidLoad];
CFRunLoopRef currentLoop = CFRunLoopGetCurrent(); //获取当前的RunLoop
CFStringRef mode = kCFRunLoopDefaultMode; //mode
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// 在RunLoop处于“空闲”状态时进行计算
NSLog(@"Thread = %@", [NSThread currentThread]);
sleep(1);
CFRunLoopRemoveObserver(currentLoop, observer, mode); //移除observer
});
CFRunLoopAddObserver(currentLoop, observer, mode); //添加observer
}
觉得写的不错,有些启发或帮助,点个赞哦!