实例化讲解RunLoop
以前看过不少有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,不多用真正的实例来说解RunLoop的,这其中有大部分缘由是因为你们在项目中不多能用到RunLoop吧。基于这种缘由,本文中将用不多的篇幅来对基础内容作以介绍,而后主要利用实例来加深你们对RunLoop的理解。本文主要分为以下几个部分:
RunLoop的基础知识
初识RunLoop,如何让RunLoop进驻线程
深刻理解Perform Selector
一直"活着"的后台线程
深刻理解NSTimer
让两个后台线程有依赖性的一种方式
NSURLConnetction的内部实现
AFNetWorking中是如何使用RunLoop的?
其它:利用GCD实现定时器功能
延伸阅读
1、RunLoop的基本概念:
什么是RunLoop?提到RunLoop,咱们通常都会提到线程,这是为何呢?先来看下官方对RunLoop的定义:RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各类事件。RunLoop的目的是让你的线程在有工做的时候忙碌,没有工做的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop到底是用来作什么的,打个比方来讲明:咱们把线程比做一辆跑车,把这辆跑车的主人比做RunLoop,那么在没有'主人'的时候,这个跑车的生命是直线型的,其启动,运行完以后就会废弃(没有人对其进行控制,'撞坏'被收回),当有了RunLoop这个主人以后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,而且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候能够休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程须要消耗512k内存,咱们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工做),这样能够增长跑车的效率,也就是说RunLoop是为线程所服务的。这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点能够从苹果公开的源码中看出来),其实RunLoop是管理线程的一种机制,这种机制不只在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有相似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observer,RunLoop Mode包含如下几种:
NSDefaultRunLoopMode,
NSEventTrackingRunLoopMode,
UIInitializationRunLoopMode,
NSRunLoopCommonModes,
NSConnectionReplyMode,
NSModalPanelRunLoopMode
复制代码
至于这些mode各自的含义,读者可本身查询,网上不乏这类资源;
2、初识RunLoop,如何让RunLoop进驻线程
咱们在主线程中添加以下代码:
while (1) {
NSLog(@"while begin" );
// the thread be blocked here
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// this will not be executed
NSLog(@"while end" );
}
复制代码
这个时候咱们能够看到主线程在执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 以后被阻塞而没有执行下面的NSLog(@"while end");同时,咱们利用GCD,将这段代码放到一个后台线程中:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSLog(@"while begin" );
NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
[subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"while end" );
}
});
复制代码
这个时候咱们发现这个while循环会一直在执行;这是为何呢?咱们先将这两个RunLoop分别打印出来:
主线程的RunLoop
因为这个日志比较长,我就只截取了上面的一部分。咱们再看咱们新建的子线程中的RunLoop,打印出来以后:
backGroundThreadRunLoop.png
从中能够看出来:咱们新建的线程中:
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null)
复制代码
咱们看到虽然有Mode,可是咱们没有给它soures,observer,timer,其实Mode中的这些source,observer,timer,统称为这个Mode的item,若是一个Mode中一个item都没有,则这个RunLoop会直接退出,不进入循环(其实线程之因此能够一直存在就是因为RunLoop将其带入了这个循环中)。下面咱们为这个RunLoop添加个source:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSPort *macPort = [NSPort port];
NSLog(@"while begin" );
NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
[subRunLoop addPort:macPort for Mode:NSDefaultRunLoopMode];
[subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"while end" );
NSLog(@"%@" ,subRunLoop);
}
});
复制代码
这样咱们能够看到可以实现了和主线程中相同的效果,线程在这个地方暂停了,为何呢?咱们明天让RunLoop在distantFuture以前都一直run的啊?相信你们已经猜出出来了。这个时候线程被RunLoop带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程能够在没有任务的时候休眠,在有任务的时候被唤醒;固然咱们只用一个while(1)也可让这个线程一直存在,可是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来讲是很是不高效的。
小结:咱们的RunLoop要想工做,必需要让它存在一个Item(source,observer或者timer),主线程之因此可以一直存在,而且随时准备被唤醒就是应为系统为其添加了不少Item
3、深刻理解Perform Selector
咱们先在主线程中使用下performselector:
- (void)tryPerformSelectorOnMianThread{
[self performSelector:@selector(mainThreadMethod) withObject:nil]; }
- (void)mainThreadMethod{
NSLog(@"execute %s" ,__func__);
// print : execute -[ViewController mainThreadMethod]
}
复制代码
这样咱们在ViewDidLoad中调用tryPerformSelectorOnMianThread,就会当即执行,而且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子同样,咱们使用GCD,让这个方法在后台线程中执行
- (void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil wait UntilDone:NO];
});
}
- (void)backGroundThread{
NSLog(@"%u" ,[NSThread isMainThread]);
NSLog(@"execute %s" ,__FUNCTION__);
}
复制代码
一样的,咱们调用tryPerformSelectorOnBackGroundThread这个方法,咱们会发现,下面的backGroundThread不会被调用,这是什么缘由呢?
这是由于,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给咱们建立一个Timer的source,加到对应的RunLoop上去,然而这个时候咱们没有RunLoop,若是咱们加上RunLoop:
- (void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil wait UntilDone:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
});
}
复制代码
这时就会发现咱们的方法正常被调用了。那么为何主线程中的perfom selector却可以正常调用呢?经过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,因此咱们在主线程中执行的时候,无需再添加RunLoop。
小结:当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop
4、一直"活着"的后台线程
如今有这样一个需求,每点击一下屏幕,让子线程作一个任务,而后你们通常会想到这样的方式:
@interface ViewController ()
@property(nonatomic,strong) NSThread *myThread;
@end
@implementation ViewController
- (void)alwaysLiveBackGoundThread{
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund" ];
self.myThread = thread;
[self.myThread start];
}
- (void)myThreadRun{
NSLog(@"my thread run" );
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%@" ,self.myThread);
[self performSelector:@selector(do BackGroundThreadWork) onThread:self.myThread withObject:nil wait UntilDone:NO];
}
- (void)do BackGroundThreadWork{
NSLog(@"do some work %s" ,__FUNCTION__);
}
@end
复制代码
这个方法中,咱们利用一个强引用来获取了后台线程中的thread,而后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork这个方法,此时咱们能够看到,在touchesBegin方法中,self.myThread是存在的,可是这是为是什么呢?这就要从线程的五大状态来讲明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,可是这个线程在执行完任务以后已经死亡了,通过上面的论述,咱们应该怎样处理呢?咱们能够给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工做的强烈愿望而不死亡):
- (void)myThreadRun{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] for Mode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run]
NSLog(@"my thread run" );
}
复制代码
这个时候再次点击屏幕,咱们就会发现,后台线程中执行的任务能够正常进行了。
小结:正常状况下,后台线程执行完任务以后就处于死亡状态,咱们要避免这种状况的发生能够利用RunLoop,而且给它一个Source这样来保证线程依旧还在
5、深刻理解NSTimer
咱们平时使用NSTimer,通常是在主线程中的,代码大多以下:
- (void)tryTimerOnMainThread{
NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
selector:@selector(timerAction) userInfo:nil repeats:YES];
[myTimer fire];
}
- (void)timerAction{
NSLog(@"timer action" );
}
复制代码
这个时候代码按照咱们预约的结果运行,若是咱们把这个Tiemr放到后台线程中呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[myTimer fire];
});
复制代码
这个时候咱们会发现,这个timer只执行了一次,就中止了。这是为何呢?经过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop以后才会生效,这个注册是由系统自动给咱们完成的,既然须要注册到RunLoop,那么咱们就须要有一个RunLoop,咱们在后台线程中加入以下的代码:
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];
复制代码
这样咱们就会发现程序正常运行了。在Timer注册到RunLoop以后,RunLoop会为其重复的时间点注册好事件,好比1:10,1:20,1:30这几个时间点。有时候咱们会在这个线程中执行一个耗时操做,这个时候RunLoop为了节省资源,并不会在很是准确的时间点回调这个Timer,这就形成了偏差(Timer有个冗余度属性叫作tolerance,它标明了当前点到后,允许有多少最大偏差),能够在执行一段循环以后调用一个耗时操做,很容易看到timer会有很大的偏差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的偏差。系统还有一个CADisplayLink,也能够实现定时效果,它是一个和屏幕的刷新率同样的定时器。若是在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,形成界面卡顿。另外GCD也能够实现定时器的效果,因为其和RunLoop没有关联,因此有时候使用它会更加的准确,这在最后会给予说明。
6、让两个后台线程有依赖性的一种方式
给两个后台线程添加依赖可能有不少的方式,这里说明一种利用RunLoop实现的方式。原理很简单,咱们先让一个线程工做,当工做完成以后唤醒另外的一线程,经过上面对RunLoop的说明,相信你们很容易可以理解这些代码:
- (void)runLoopAddDependance{
self.runLoopThreadDidFinishFlag = NO;
NSLog(@"Start a New Run Loop Thread" );
NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
[runLoopThread start];
NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside" );
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (!_runLoopThreadDidFinishFlag) {
self.myThread = [NSThread currentThread];
NSLog(@"Begin RunLoop" );
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSPort *myPort = [NSPort port];
[runLoop addPort:myPort for Mode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"End RunLoop" );
[self.myThread cancel];
self.myThread = nil;
}
});
}
- (void)handleRunLoopThreadTask
{
NSLog(@"Enter Run Loop Thread" );
for (NSInteger i = 0; i < 5; i ++) {
NSLog(@"In Run Loop Thread, count = %ld" , i);
sleep(1);
}
// 错误示范
_runLoopThreadDidFinishFlag = YES;
// 这个时候并不能执行线程完成以后的任务,由于Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被从新赋值。Run Loop这个时候没有被任务事件源唤醒。
// 正确的作法是使用 "selector" 方法唤醒Run Loop。 即以下:
NSLog(@"Exit Normal Thread" );
[self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil wait UntilDone:NO];
// NSLog(@"Exit Run Loop Thread" );
}
复制代码
7、NSURLConnection的执行过程
在使用NSURLConnection时,咱们会传入一个Delegate,当咱们调用了[connection start]以后,这个Delegate会不停的收到事件的回调。实际上,start这个函数的内部会获取CurrentRunloop,而后在其中的DefaultMode中添加4个source。以下图所示,CFMultiplexerSource是负责各类Delegate回调的,CFHTTPCookieStorage是处理各类Cookie的。以下图所示:
从中能够看出,当开始网络传输是,咱们能够看到NSURLConnection建立了两个新的线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是处理底层socket连接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket的事件,并经过以前添加的source,来通知(唤醒)上层的Delegate。这样咱们就能够理解咱们平时封装网络请求时候常见的下面逻辑了:
while (!_isEndRequest)
{
NSLog(@"entered run loop" );
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"main finished,task be removed" );
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
_isEndRequest = YES;
}
复制代码
这里咱们就能够解决下面这些疑问了:
为何这个While循环不停的执行,还须要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,咱们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。
若是没有为RunLoop添加item,那么它就会当即退出,这里的item呢? 其实系统已经给咱们默认添加了4个source了。
既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];让线程在这里停下来,那么为何这个循环会持续的执行呢?由于这个一直在处理任务,而且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。
8、AFNetWorking中是如何使用RunLoop的?
在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其但愿可以在后台线程来接收Delegate的回调。为此AFN建立了一个线程,而后在里面开启了一个RunLoop,而后添加item
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] set Name:@"AFNetworking" ];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] for Mode: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;
}
复制代码
这里这个NSMachPort的做用和上文中的同样,就是让线程不至于在很快死亡,而后RunLoop不至于退出(若是要使用这个MachPort的话,调用者须要持有这个NSMachPort,而后在外部线程经过这个port发送信息到这个loop内部,它这里没有这么作)。而后和上面的作法类似,在须要后台执行这个任务的时候,会经过调用:[NSObject performSelector:onThread:..]来将这个任务扔给后台线程的RunLoop中来执行。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil wait UntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil wait UntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
复制代码
GCD定时器的实现
- (void)gcdTimer{
// get the queue
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// creat timer
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// config the timer (starting time,interval)
// set begining time
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
// set the interval
uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interver, 0.0);
dispatch_source_set_event_handler(self.timer, ^{
// the tarsk needed to be processed async
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i++) {
NSLog(@"gcdTimer" );
}
});
});
dispatch_resume(self.timer);
}
复制代码