之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原因,本文中将用很少的篇幅来对基础内容做以介绍,然后主要利用实例来加深大家对RunLoop的理解,本文中的代码已经上传GitHub,大家可以下载查看,有问题欢迎Issue我。本文主要分为如下几个部分:
- RunLoop的基础知识
- 初识RunLoop,如何让RunLoop进驻线程
- 深入理解Perform Selector
- 一直”活着”的后台线程
- 深入理解NSTimer
- 让两个后台线程有依赖性的一种方式
- NSURLConnetction的内部实现
- AFNetWorking中是如何使用RunLoop的?
- 其它:利用GCD实现定时器功能
- 延伸阅读
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各自的含义,读者可自己查询,网上不乏这类资源;
初识RunLoop,如何让RunLoop进驻线程
我们在主线程中添加如下代码:
1 | while (1) { |
这个时候我们可以看到主线程在执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
之后被阻塞而没有执行下面的NSLog(@"while end")
;同时,我们利用GCD,将这段代码放到一个后台线程中:
1 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
这个时候我们发现这个while循环会一直在执行;这是为什么呢?我们先将这两个RunLoop
分别打印出来:
由于这个日志比较长,我就只截取了上面的一部分。
我们再看我们新建的子线程中的RunLoop
,打印出来之后:
从中可以看出来:我们新建的线程中:
1 | sources0 = (null), |
我们看到虽然有Mode,但是我们没有给它soures,observer,timer
,其实Mode中的这些source,observer,timer
,统称为这个Mode
的item
,如果一个Mode
中一个item
都没有,则这个RunLoop会直接退出,不进入循环(其实线程之所以可以一直存在就是由于RunLoop将其带入了这个循环中)。下面我们为这个RunLoop添加个source:
1 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
这样我们可以看到能够实现了和主线程中相同的效果,线程在这个地方暂停了,为什么呢?我们明天让RunLoop在distantFuture
之前都一直run的啊?相信大家已经猜出出来了。这个时候线程被RunLoop
带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程可以在没有任务的时候休眠,在有任务的时候被唤醒;当然我们只用一个while(1)
也可以让这个线程一直存在,但是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来说是非常不高效的。
小结:我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是应为系统为其添加了很多Item
深入理解Perform Selector
我们先在主线程中使用下performselector
:
1 | - (void)tryPerformSelectorOnMianThread{ |
这样我们在ViewDidLoad中调用tryPerformSelectorOnMianThread
,就会立即执行,并且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子一样,我们使用GCD,让这个方法在后台线程中执行:
1 | - (void)tryPerformSelectorOnBackGroundThread { |
同样的,我们调用tryPerformSelectorOnBackGroundThread
这个方法,我们会发现,下面的backGroundThread
不会被调用,这是什么原因呢?
这是因为,在调用performSelector:onThread: withObject: waitUntilDone
的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop
,如果我们加上RunLoop:
1 | - (void)tryPerformSelectorOnBackGroundThread{ |
这时就会发现我们的方法正常被调用了。那么为什么主线程中的perfom selector
却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。从Apple的文档中我们也可以得到验证:
Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received. 每个执行perform selector的请求都以队列的形式被放到目标线程的run loop中。然后目标线程会根据进入run loop的顺序来一一执行。
当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop
一直”活着”的后台线程
现在有这样一个需求,每点击一下屏幕,让子线程做一个任务,然后大家一般会想到这样的方式:
1 | @interface ViewController () |
这个方法中,我们利用一个强引用来获取了后台线程中的thread,然后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork
这个方法,此时我们可以看到,在touchesBegin
方法中,self.myThread是存在的,但是这是为是什么呢?这就要从线程的五大状态来说明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,但是这个线程在执行完任务之后已经死亡了,经过上面的论述,我们应该怎样处理呢?我们可以给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工作的强烈愿望而不死亡):
1 | - (void)myThreadRun{ |
这个时候再次点击屏幕,我们就会发现,后台线程中执行的任务可以正常进行了。
正常情况下,后台线程执行完任务之后就处于死亡状态,我们要避免这种情况的发生可以利用RunLoop,并且给它一个Source这样来保证线程依旧还在
深入理解NSTimer
我们平时使用NSTimer,一般是在主线程中的,代码大多如下:
1 | - (void)tryTimerOnMainThread{ |
这个时候代码按照我们预定的结果运行,如果我们把这个Tiemr放到后台线程中呢?
1 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
这个时候我们会发现,这个timer只执行了一次,就停止了。这是为什么呢?通过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop
,我们在后台线程中加入如下的代码:
1 | NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; |
这样我们就会发现程序正常运行了。在Timer注册到RunLoop之后,RunLoop会为其重复的时间点注册好事件,比如1:10,1:20,1:30这几个时间点。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance
,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink
,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,这在最后会给予说明。
让两个后台线程有依赖性的一种方式
给两个后台线程添加依赖可能有很多的方式,这里说明一种利用RunLoop
实现的方式。原理很简单,我们先让一个线程工作,当工作完成之后唤醒另外的一线程,通过上面对RunLoop
的说明,相信大家很容易能够理解这些代码:
1 | - (void)runLoopAddDependance{ |
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。这样我们就可以理解我们平时封装网络请求时候常见的下面逻辑了:
1 | while (!_isEndRequest) |
这里我们就可以解决下面这些疑问了:
- 为什么这个While循环不停的执行,还需要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,我们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。
- 如果没有为RunLoop添加item,那么它就会立即退出,这里的item呢? 其实系统已经给我们默认添加了4个source了。
- 既然
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
让线程在这里停下来,那么为什么这个循环会持续的执行呢?因为这个一直在处理任务,并且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。
AFNetWorking中是如何使用RunLoop的 ?
在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。
为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item
1 | + (void)networkRequestThreadEntryPoint:(id)__unused object { |
这里这个NSMachPort
的作用和上文中的一样,就是让线程不至于在很快死亡,然后RunLoop不至于退出(如果要使用这个MachPort的话,调用者需要持有这个NSMachPort,然后在外部线程通过这个port发送信息到这个loop内部,它这里没有这么做)。然后和上面的做法相似,在需要后台执行这个任务的时候,会通过调用:[NSObject performSelector:onThread:..]
来将这个任务扔给后台线程的RunLoop中来执行。
1 | - (void)start { |
GCD定时器的实现
1 | - (void)gcdTimer{ |
延伸阅读
- http://chun.tips/blog/2014/10/20/zou-jin-run-loopde-shi-jie-%5B%3F%5D-:shi-yao-shi-run-loop%3F/
- https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
- http://www.cocoachina.com/ios/20150601/11970.html
- http://www.jianshu.com/p/de2716807570
- http://blog.csdn.net/enuola/article/details/9163051