RunLoop

一、概述

一般来说,一个线程只能执行一个任务,执行完就会退出,如果我们需要一种机制,让线程能随时处理时间但并不退出,那么 RunLoop 就是这样的一个机制。Runloop是事件接收和分发机制的一个实现。

RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。

在开始Runloop内容之前,大家先考虑下面几个问题(后面会对这些问题逐个讲解):

(1)基于NSTimer的轮播器什么情况下会被页面滚动暂停,怎样可以不被暂停,为什么?
(2)延迟执行performSelecter相关方法是怎样被执行的?在子线程中也是一样的吗?
(3)事件响应和手势识别底层处理是一致的吗,为什么?
(4)界面刷新时,是在什么时候会真正执行刷新,为什么会刷新不及时?
(5)项目程序运行中,总是伴随着多次自动释放池的创建和销毁,这些是在什么时候发生的呢?
(6)当我们在子线程上需要执行代理方法或者回调时,怎么确保当前线程没有被销毁?

 

二、RunLoop基本作用

1、保持程序持续运行

程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行。

2、处理App中的各种事件

比如:触摸事件,定时器事件,Selector事件等。

3、节省CPU资源,提高程序性能

程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情。

我们先通过API内一张图片来简单看一下RunLoop内部运行原理:

20160506091116579

通过上图可以看出,RunLoop在跑圈过程中,当接收到Input sources 或者 Timer sources时就会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了。

 

三、RunLoop的开启

大家应该都知道程序的入口是main函数,iOS程序的入口当然也为main函数:

程序主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。

进入上面main函数返回的UIApplicationMain函数:

我们发现它返回的是一个int类型的值,那么我们对main函数做一些修改:

运行程序,我们发现只会打印开始,并不会打印结束,这说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行

 

我们来看到RunLoop的源码:

我们发现RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。如果没有RunLoop,UIApplicationMain函数执行完毕之后将直接返回,也就没有程序持续运行一说了。

 

四、RunLoop对象

RunLoop对象包括Fundation中的NSRunLoop对象和CoreFoundation中的CFRunLoopRef对象。因为Fundation框架是基于CFRunLoopRef的封装,因此我们学习RunLoop还是要研究CFRunLoopRef 源码。

1、获得RunLoop对象

 

五、RunLoop和线程

1、RunLoop和线程之间的关系

(1)每条线程都有唯一的一个与之对应的RunLoop对象。

(2)主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建。

(3)RunLoop在第一次获取时创建,在线程结束时销毁。

 

2、主线程相关联的RunLoop创建

CFRunLoopRef源码:

 

3、创建与子线程相关联的RunLoop

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

CFRunLoopRef源码:

可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

[NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。

 

六、RunLoop相关类

Core Foundation中关于RunLoop的5个类:

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:

RunLoop_0

1、 CFRunLoopModeRef

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

 

系统默认注册了5个Mode,其中常见的有1.2两种:

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

 

Mode间的切换:

我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况,我们通过一段代码来看一下:

由上述代码可以看出,NSTimer不管用是因为Mode的切换,因为如果我们在主线程使用定时器,此时RunLoop的Mode为kCFRunLoopDefaultMode,即定时器属于kCFRunLoopDefaultMode,那么此时我们滑动ScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时,RunLoop的Mode切换回kCFRunLoopDefaultMode,所有NSTimer就又管用了。
同样道理的还有ImageView的显示,如下:

 

使用GCD也可以创建计时器,而且更为精确:

 

2、CFRunLoopSourceRef

Source分为两种:

Source0:非基于Port的 用于用户主动触发的事件(点击button 或点击屏幕)
Source1:基于Port的 通过内核和其他线程相互发送消息(与内核相关)
注意:Source1在处理的时候会分发一些操作给Source0去处理

 

3、CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
我们直接来看代码,给RunLoop添加监听者,监听其运行状态:

输出结果:

1434508-106d3e9fca39903f

 

七、RunLoop处理逻辑

根据苹果在文档里的说明,RunLoop 内部的逻辑大致如下:

RunLoop_1

其内部代码整理如下:

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

 

八、RunLoop退出

(1)主线程销毁RunLoop退出。

(2)Mode中有一些Timer 、Source、 Observer,这些保证Mode不为空时保证RunLoop没有空转并且是在运行的,当Mode中为空的时候,RunLoop会立刻退出。

(3)我们在启动RunLoop的时候可以设置什么时候停止。

 

九、回答概述中留的几个问题

1、基于NSTimer的轮播器什么情况下会被页面滚动暂停,怎样可以不被暂停,为什么?

这个问题前面已经讲解过了,这里不再浪费时间重讲一次,没意义。

 

2、延迟执行performSelecter相关方法是怎样被执行的?在子线程中也是一样的吗?

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

 

3、事件响应和手势识别底层处理是一致的吗,为什么?

事件响应:

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别:

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

 

4、界面刷新时,是在什么时候会真正执行刷新,为什么会刷新不及时?

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。所以说界面刷新并不一定是在setNeedsLayout相关的代码执行后立刻进行的

 

5、项目程序运行中,总是伴随着多次自动释放池的创建和销毁,这些是在什么时候发生的呢?

这个问题在峰哥之前的文章《iOS开发之内存管理》中也有讲述,感兴趣的可以自己去看。这里再详细讲解一遍:

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

 

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

 

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,一般情况下开发者也不必显示创建 Pool 了。

 

6、当我们在子线程上需要执行代理方法或者回调时,怎么确保当前线程没有被销毁?

首先引入一个概念:Event_loop,一般一个线程执行完任务后就会退出,当需要保证该线程不退出,可以通过类似以下方式:

开启一个循环,保证线程不退出,这就是Event_loop模型。这是在很多操作系统中都使用的模型,例如OS/iOS中的RunLoop。这种模型最大的作用就是管理事件/消息,在有新消息到来时立刻唤醒处理,没有待处理消息时线程休眠,避免资源浪费。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值