实例化讲解RunLoop

转自此处

实例化讲解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 forMode: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 waitUntilDone: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 waitUntilDone: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(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)doBackGroundThreadWork{

    NSLog(@"do some work %s",__FUNCTION__);

}
@end
复制代码

这个方法中,咱们利用一个强引用来获取了后台线程中的thread,而后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork这个方法,此时咱们能够看到,在touchesBegin方法中,self.myThread是存在的,可是这是为是什么呢?这就要从线程的五大状态来讲明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,可是这个线程在执行完任务以后已经死亡了,通过上面的论述,咱们应该怎样处理呢?咱们能够给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工做的强烈愿望而不死亡):

- (void)myThreadRun{

 [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode: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 forMode: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);
}
#if 0
// 错误示范
_runLoopThreadDidFinishFlag = YES;
// 这个时候并不能执行线程完成以后的任务,由于Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被从新赋值。Run Loop这个时候没有被任务事件源唤醒。
// 正确的作法是使用 "selector"方法唤醒Run Loop。 即以下:
#endif
NSLog(@"Exit Normal Thread");
[self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone: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] 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;
}
复制代码

这里这个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 waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
    self.state = AFOperationExecutingState;
    [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone: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);
}
复制代码
相关文章
相关标签/搜索