前言
iOS开发中,RunLoop就是个神秘的领域,很多2~3年的开发者都不能准确的描述它的具体含义,甚至可能从来都没有接触过该方面的技术,或者项目中隐约用到过(定时器),但是不知道是其的作用,问题一解决就不在深究了,不过也有情可原,毕竟大部分开发者还处于基本功能的构建,很少涉及性能的优化。
RunLoop之所以神秘,个人认为,系统能够利用RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能。实现这种技术的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
RunLoop的基本概念
简介
RunLoop从字面上来看:运行循环,或者是跑圈。
#基本作用:
1、保持程序的持续运行(比如主运行循环)
2、处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
3、节省CPU资源,提高性能。
通常来讲,一个线程一次只能执行一个任务,当线程中的任务执行完成后,线程就会自动退出。如果我们需要一个长运行机制,让线程能够随时处理事件并且保证不退出,通常我们的做法,逻辑是这样的:
function loop {
initialize();
BOOL running = YES;
do{
// 执行各种任务,处理各种事件
}while (running);
return 0;
}
上述函数中,通过一个循环来执行任务,根据事件的回调和观察者的属性来改更改running的值,有效的使得函数中的任务长久的运行。简单的说,这就是程序的运行模式。
众所周知,iOS应用启动后,不会正常的自动退出。这就是因为iOS系统拥有的RunLoop机制的作用,当应用启动后,主线程的RunLoop默认开启,从而应用进入一个死循环,阻止了程序在运行完毕之后退出。
int main(int argc, char * argv[]) {
@autoreleasepool {
//这个函数永远不会有返回值
return UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
}
}
1.在UIApplicationMain函数内部就启动了一个RunLoop
2.UIApplicationMain函数一直没有返回,保持了程序的持续运行
3.这个默认启动的RunLoop是主线程关联的
4.一个线程对应一个RunLoop,主线程的RunLoop默认已经启动
5.子线程的RunLoop得手动启动(调用运行方法)
6.RunLoop只能选择一个模式启动,如果当前模式中没有任Source,Timer,Observer,那么就直接退出RunLoop
RunLoop在循环过程中监听着port事件和timer事件,当前线程有任务时,唤醒当当线程去执行任务,任务执行完成以后,使当前线程进入休眠状态。
RunLoop与线程
RunLoop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,RunLoop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。RunLoop是线程的基础架构部分,Cocoa和CoreFundation框架都提供了RunLoop的相关接口,方便配置和管理线程的RunLoop。每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象,主线程的RunLoop对象是默认开启的,子线程的run loop对象需要程序员手动开启。
从源码中可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
RunLoop与autorelease pool
从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件,触摸事件。我们都知道: 所有autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。但是如果每次都放进应用程序的main.m中的autoreleasepool中,迟早有被撑满的一刻。这个过程中必须有一个释放的动作。在一次完整的运行循环结束之前,会被销毁。
Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的runloop迭代结束时释放。
在使用手动的内存管理方式的项目中,会经常用到很多自动释放的对象,如果这些对象不能够被即时释放掉,会造成内存占用量急剧增大。Run loop就为我们做了这样的工作,每当一个运行循环结束的时候,它都会释放一次autorelease pool,同时pool中的所有自动释放类型变量都会被释放掉。
RunLoop常用的类
首先要知道iOS里面有两套API可以访问和使用RunLoop:
1、Foundation ---> NSRunLoop
2、Core Foundation ---> CFRunLoopRef
CoreFoundation 里面关于 RunLoop 有5个类:
1、CFRunLoopRef
2、CFRunLoopModeRef
3、CFRunLoopSourceRef
4、CFRunLoopTimerRef
5、CFRunLoopObserverRef
上面两套都可以使用,但是要知道CFRunLoopRef是用c语言写的,是开源的,相比于NSRunLoop更加底层,而NSRunLoop其实是对CFRunLoopRef的一个简单的封装。便于使用而已。这样说来,显然CFRunLoopRef的性能要高一点。
另外,我们不能在一个线程中去操作另外一个线程的run loop对象,那很可能会造成意想不到的后果。不过幸运的是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的run loop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:
- (CFRunLoopRef)getCFRunLoop;
获取对应的CFRunLoopRef类,来达到线程安全的目的。
CFRunLoopSourceRef
Run loop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。
事件源(input source)
按照官方文档的分类
Port-Based Sources (基于端口,跟其他线程交互,通过内核发布的消息)
Custom Input Sources (自定义)
Cocoa Perform Selector Sources (performSelector…方法)
按照函数调用栈的分类
Source0:非基于Port的
Source1:基于Port的
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。
定时源(timer source)
定时源在预设的时间点同步方式传递消息,这些消息都会发生在特定时间或者重复的时间间隔。定时源则直接传递消息给处理例程,不会立即退出run loop。
需要注意的是,尽管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和你的run loop的特定模式相关。如果定时器所在的模式当前未被run loop监视,那么定时器将不会开始直到run loop运行在相应的模式下。类似的,如果定时器在run loop处理某一事件期间开始,定时器会一直等待直到下次run loop开始相应的处理程序。如果run loop不再运行,那定时器也将永远不启动。
创建定时器源有两种方法,
方法一:
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];
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
- NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响
- GCD的定时器不受RunLoop的Mode影响
开发中,我们需要一个定时操作,来循环的执行任务,如果这个任务是不耗时的,我们可以选择NSTimer和GCD,如果任务是耗时的,我们通常会在子线程创建一个定时器,来执行耗时任务,当选择NSTimer时计时,定时器里的任务不会执行,原因是当前线程对相应的RunLoop并没有开启,创建线程的函数执行完成之后,这个线程就被销毁了(可以用继承CustomThread : NSThread,重写dealloc方法来验证),此时,你可能想用一个全局的对象来保存线程,任然不能解决问题,全局的对象描述指向线程的那个引用地址,这和线程的实体没有关系,并不能代替一个实在的线程。
解决方案:开启当前线程的RunLoop,当不需要任务的时候关闭
//创建一个线程
CustomThread *thread = [[CustomThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
while (!_isfinished) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
NSLog(@"线程开始了");
}];
//开启线程
[thread start];
- (void)timerAction:(id) objc
{
while (_isfinished) {
[NSThread exit];//不需要时,关闭当前线程
}
NSLog(@"定时器开启了");
}
GCD执行,不存在上述问题,因为,系统内部默认给我们开启当前线程的RunLoop,性能更加稳定,还不存在内存泄露。
/*timer需要全局变量*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_currentTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_currentTimer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_currentTimer, ^{
NSLog(@"-----开始前线程:%@----", [NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
// 在主线程中实现需要的功能
NSLog(@"-----开始后线程:%@----", [NSThread currentThread]);
});
});
dispatch_resume(_currentTimer);
/*
// 挂起定时器(dispatch_suspend 之后的 Timer,是不能被释放的!会引起崩溃)
dispatch_suspend(_timer);
// 关闭定时器
dispatch_source_cancel(_timer);
*/
选择 GCD 还是 NSTimer? 参考文章
CFRunLoopObserverRef
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
应用
//添加runloop监听者
- (void)addRunloopObserver{
//获取 当前的Runloop ref - 指针
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
//定义一个RunloopObserver
CFRunLoopObserverRef defaultModeObserver;
//上下文
/*
typedef struct {
CFIndex version; //版本号 long
void * info;//万能指针,这里我们要填写对象(self或者传进来的对象)
const void *(*retain)(const void *info); //填写&CFRetain
void (*release)(const void *info); //填写&CGFRelease
CFStringRef (*copyDescription)(const void *info); //NULL
} CFRunLoopObserverContext;
*/
CFRunLoopObserverContext context = {
0,
(__bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
/*
1 NULL空指针 nil空对象 这里填写NULL
2 模式
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理NSTimer
kCFRunLoopBeforeSources = (1UL << 2), //即将处理Sources
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变};
3 是否重复 - YES
4 nil 或者 NSIntegerMax - 999
5 回调
6 上下文
*/
//创建观察者
defaultModeObserver = CFRunLoopObserverCreate(NULL,
kCFRunLoopBeforeWaiting, YES,
NSIntegerMax - 999,
&Callback,
&context);
//添加当前runloop的观察着
if(defaultModeObserver){
CFRunLoopAddObserver(currentRunLoop, defaultModeObserver, kCFRunLoopDefaultMode);
//释放
CFRelease(defaultModeObserver);
}
}
//这里处理耗时操作了
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
//通过info桥接为需要的对象
}
CFRunLoopModeRef
系统默认注册了5个Mode:(前两个跟最后一个常用)
kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个
Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触
摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个
Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
RunLoop的事件队列
从这个事件队列中可以看出:
①如果是事件到达,消息会被传递给相应的处理程序来处理, runloop处理完当次事件后,run loop会退出,而不管之前预定的时间到了没有。你可以重新启动run loop来等待下一事件。
②如果线程中有需要处理的源,但是响应的事件没有到来的时候,线程就会休眠等待相应事件的发生。这就是为什么run loop可以做到让线程有工作的时候忙于工作,而没工作的时候处于休眠状态。
RunLoop 的实际应用举例
AF2.x,创建了一条常驻线程专门处理所有请求的回调事件。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。