RunLoop详解
RunLoop介绍
顾名思义,运行循环,是程序在运行中做的一些事情,RunLoop是iOS运行过程中循环做一些事情,RunLoop运用非常广泛,比如:定时器(timer)、PerformSelector、GCD Async Main Queue、事件响应、手势识别、界面刷新、网络请求、AutoreleasePool等等等等。所以说,没有RunLoop,iOS程序所做的很多事情都无法完成,下面我们就详细介绍一下RunLoop底层是如何实现,以及RunLoop在iOS开发中的实际运用。
如果没有RunLoop,代码运行完后,程序直接退出,我举个例子,大家可以用xcode新建一个命令行项目,命令行项目默认没有RunLoop。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
这段代码打印结果
运行结果.png
xcode.png
大家可以看到,程序显示已经运行结束,并且Xcode上方显示Finish running。因为命令行程序一旦执行完毕,程序会走到return 0然后退出程序。但是如果有了RunLoop,RunLoop会帮我们做一些事情,让程序不会return,这样程序会在休眠中一直等待消息唤醒,比如处理点击事件,定时器等。而且,RunLoop还会降低CPU的消耗,让程序在不必要的时候处于休眠状态以节省性能。RunLoop的概念介绍完毕,大家对RunLoop有了一个粗浅的认识,接下来我们详细介绍一下RunLoop对象,以及RunLoop对象底层的原理。
RunLoop对象
iOS中有两套API来访问和使用RunLoop
- Foundation:NSRunLoop (OC)
- Core Foundation:CFRunLoopRef(C语言)
NSRunLoop是对CFRunLoopRef的一层包装,CFRunLoopRef是开源的,RunLoop对象也有对应的两种创建方式
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
CFRunLoopRef *runLoop CFRunLoopGetCurrent();
RunLoop与线程的关系
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时,并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时摧毁
- 主线程的RunLoop已经自动获取创建,子线程默认没有开启RunLoop
RunLoop相关的类以及结构
Core Foundation中关于RunLoop的5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
我们也知道,OC对象在底层实现都是C语言的结构体,从上面的源码中我们可以找到RunLoop和RunLoopMode在底层的结构体,下面的结构体都被简化过了
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
结合上面的类,我们可以知道,CFRunLoopRef里装了很多Modes,其中有一个是Current,mode里装着Source0,Source1,Timer和Observer,总结一下就跟下图类似,后面我们会介绍一下CFRunLoopModeRef
RunLoop
CFRunLoopModeRef
- CFRunLoopModeRef代表着RunLoop的运行模式
- 一个RunLoop包含若干个Mode,每个Mode又包含若干Source0/Source1/Timer/Observers
- RunLoop启动时只能选择其中一个Mode,作为CurrentMode
- 如果需要切换Mode,只能退出当前的Loop,再重新选择一个Mode进入
- 不同组的Source0/Source1/Timer/Observers可以分开,互不影响
- 如果Mode里没有Source0/Source1/Timer/Observers,RunLoop会立刻退出
RunLoop运行时有下面几种状态
CFRunLoopObserveRef
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
在开发中我们常用的Mode有两种
1.kCFRunLoopDefaultMode(NSDefaultRunLoopMode):APP的默认Mode,通
常主线程在这个Mode下运行
2.UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
RunLoop的运行逻辑
image.png
- Source0
1、触摸时间处理
2、performSelector:onThread:- Source1
1、Port的线程间通信
2、系统时间捕捉- Timers
1、NSTimer
2、performSelector: withObject: afterDelay:- Observers
1、用于监听RunLoop的状态
2、UI刷新(BeforeWaiting)
3、Autorelease Pool(BeforeWaiting)
RunLoop的实际运用
到这里,大家应该对RunLoop的概念有了清晰地认识,下面总结一下RunLoop在实际开发中的应用
- 控制线程生命周期(线程保活)
- 解决timer在ScrollView滑动时停止工作的问题
- 监控应用卡顿
- 性能优化
首先,控制线程生命周期,在项目中可以理解为,我们开辟子线程做一些比较耗时的工作,等子线程任务完成时候,子线程便会销毁。但是有些情况下的需求是我们不希望子线程那么快的销毁,可能需要子线程等待处理某些事情的时候,我们就可以使用RunLoop,下面是个简单的例子。在这里我们在封装的类内部,手动启动线程,而且不用担心循环引用的问题,使用起来很简单。
@interface PermenantThread()
@property(strong, nonatomic)NSThread *thread;
@end
@implementation PermenantThread
- (instancetype)init
{
self = [super init];
if (self) {
self.thread = [[NSThread alloc]initWithBlock:^{
CFRunLoopSourceContext context;
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
}];
[self.thread start];
}
return self;
}
- (void)stop{
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}
- (void)executeTask:(PermenantTask)task{
task();
}
@end
解决timer停止问题,我相信大家在项目中都遇到过。通过上面的了解大家应该都清楚是什么原因造成的timer停止工作。因为RunLoop启动时只能选择一个Mode,ScrollView在滑动时默认为UITrackingRunLoopMode,而定时器默认为kCFRunLoopDefaultMode。所以我们在滑动时候进入Tracking,导致定时器失效。下面是解决方法,但是需要注意的是,NSRunLoopCommonModes并不是一个真的模式,它相当于一个标记,意思是timer在此种标记的模式下都可以运行,UITrackingRunLoopMode,NSDefaultRunLoopMode才是真正存在的模式。
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
//doSomething
}];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];
相信大家通过这篇文章对RunLoop有了更清晰的认识和了解,在项目中,RunLoop可以有更多的应用,比如性能优化等。这些东西大家有兴趣可以去翻看相关资料,以便对RunLoop有独到的见解。