iOS刨根问底-深刻理解RunLoop

概述

RunLoop做为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是不少常见技术的幕后功臣。尽管在平时多数开发者不多直接使用RunLoop,可是理解RunLoop能够帮助开发者更好的利用多线程编程模型,同时也能够帮助开发者解答平常开发中的一些疑惑。本文将从RunLoop源码着手,结合RunLoop的实际应用来逐步解开它的神秘面纱。php

开源的RunloopRef

一般所说的RunLoop指的是NSRunloop或者CFRunloopRef,CFRunloopRef是纯C的函数,而NSRunloop仅仅是CFRunloopRef的OC封装,并未提供额外的其余功能,所以下面主要分析CFRunloopRef,苹果已经开源了CoreFoundation源代码,所以很容易找到CFRunloop源代码
从代码能够看出CFRunloopRef其实就是**__CFRunloop这个结构体指针(按照OC的思路咱们能够将RunLoop当作一个对象),这个对象的运行才是咱们一般意义上说的运行循环,核心方法是__CFRunloopRun()**,为了便于阅读就再也不直接贴源代码,放一段伪代码方便你们阅读:html

 
 

源代码尽管不算太长,可是若是不太熟悉的话面对这么一堆不知道作什么的函数调用仍是会给人一种神秘感。可是如今能够不用逐行阅读,后面慢慢解开这层神秘面纱。如今只要了解上面的伪代码知道核心的方法**__CFRunLoopRun**内部实际上是一个_do while_循环,这也正是Runloop运行的本质。执行了这个函数之后就一直处于“等待-处理”的循环之中,直到循环结束。只是不一样于咱们本身写的循环它在休眠时几乎不会占用系统资源,固然这是因为系统内核负责实现的,也是Runloop精华所在。git

随着Swift的开源苹果也维护了一个Swift版本的跨平台CoreFoundation版本,除了mac平台它仍是适配了Linux和Windows平台。可是鉴于目前不少关于Runloop的讨论都是以OC版展开的,因此这里也主要分析OC版本。github

下图描述了Runloop运行流程(基本描述了上面Runloop的核心流程,固然能够查看官方The Run Loop Sequence of Events描述):编程

RunLoopswift

整个流程并不复杂(须要注意的就是_黄色_区域的消息处理中并不包含source0,由于它在循环开始之初就会处理),整个流程其实就是一种Event Loop的实现,其余平台均有相似的实现,只是这里叫作Runloop。可是既然RunLoop是一个消息循环,谁来管理和运行Runloop?那么它接收什么类型的消息?休眠过程是怎么样的?如何保证休眠时不占用系统资源?如何处理这些消息以及什么时候退出循环?还有一系列问题须要解开。缓存

注意的是尽管CFRunLoopPerformBlock在上图中做为唤醒机制有所体现,但事实上执行CFRunLoopPerformBlock只是入队,下次RunLoop运行才会执行,而若是须要当即执行则必须调用CFRunLoopWakeUp。markdown

Runloop Mode

从源码很容易看出,Runloop老是运行在某种特定的CFRunLoopModeRef下(每次运行**__CFRunLoopRun()函数时必须指定Mode)。而经过CFRunloopRef对应结构体的定义能够很容易知道每种Runloop均可以包含若干个Mode,每一个Mode又包含Source/Timer/Observer。每次调用Runloop的主函数__CFRunLoopRun()时必须指定一种Mode,这个Mode称为 _currentMode**,当切换Mode时必须退出当前Mode,而后从新进入Runloop以保证不一样Mode的Source/Timer/Observer互不影响。cookie

 

系统默认提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)UITrackingRunLoopMode,须要切换到对应的Mode时只须要传入对应的名称便可。前者是系统默认的Runloop Mode,例如进入iOS程序默认不作任何操做就处于这种Mode中,此时滑动UIScrollView,主线程就切换Runloop到到UITrackingRunLoopMode,再也不接受其余事件操做(除非你将其余Source/Timer设置到UITrackingRunLoopMode下)。
可是对于开发者而言常常用到的Mode还有一个kCFRunLoopCommonModes(NSRunLoopCommonModes),其实这个并非某种具体的Mode,而是一种模式组合,在iOS系统中默认包含了
 NSDefaultRunLoopMode UITrackingRunLoopMode(注意:并非说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是至关于分别注册了 NSDefaultRunLoopMode UITrackingRunLoopMode。固然你也能够经过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合)。网络

注意:咱们经常还会碰到一些系统框架自定义Mode,例如Foundation中NSConnectionReplyMode。还有一些系统私有Mode,例如:GSEventReceiveRunLoopMode接受系统事件,UIInitializationRunLoopMode App启动过程当中初始化Mode。更多系统或框架Mode查看这里

CFRunLoopRef和CFRunloopMode、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef关系以下图:

RunLoopMode

那么CFRunLoopSourceRef、CFRunLoopTimerRef和CFRunLoopObserverRef到底是什么?它们在Runloop运行流程中起到什么做用呢?

Source

首先看一下官方Runloop结构图(注意下图的Input Source Port和前面流程图中的Source0并不对应,而是对应Source1。Source1和Timer都属于端口事件源,不一样的是全部的Timer都共用一个端口“Mode Timer Port”,而每一个Source1都有不一样的对应端口):

RunLoopSource

再结合前面RunLoop核心运行流程能够看出Source0(负责App内部事件,由App负责管理触发,例如UITouch事件)和Timer(又叫Timer Source,基于时间的触发器,上层对应NSTimer)是两个不一样的Runloop事件源(固然Source0是Input Source中的一类,Input Source还包括Custom Input Source,由其余线程手动发出),RunLoop被这些事件唤醒以后就会处理并调用事件处理方法(CFRunLoopTimerRef的回调指针和CFRunLoopSourceRef均包含对应的回调指针)。
可是对于CFRunLoopSourceRef除了Source0以外还有另外一个版本就是Source1,Source1除了包含回调指针外包含一个mach port,和Source0须要手动触发不一样,Source1能够监听系统端口和其余线程相互发送消息,它可以主动唤醒RunLoop(由操做系统内核进行管理,例如CFMessagePort消息)。官方也指出能够自定义Source,所以对于CFRunLoopSourceRef来讲它更像一种协议,框架已经默认定义了两种实现,若是有必要开发人员也能够自定义,详细状况能够查看官方文档

Observer

 

相对来讲CFRunloopObserverRef理解起来并不复杂,它至关于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态(它包含一个函数指针_callout_将当前状态及时告诉观察者)。具体的Observer状态以下:

 

Call out

在开发过程当中几乎全部的操做都是经过Call out进行回调的(不管是Observer的状态通知仍是Timer、Source的处理),而系统在回调时一般使用以下几个函数进行回调(换句话说你的代码其实最终都是经过下面几个函数来负责调用的,即便你本身监听Observer也会先调用下面的函数而后间接通知你,因此在调用堆栈中常常看到这些函数):

 

例如在控制器的touchBegin中打入断点查看堆栈(因为UIEvent是Source0,因此能够看到一个Source0的Call out函数CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION调用):

RunLoop_Source0_UITouch

RunLoop休眠

其实对于Event Loop而言RunLoop最核心的事情就是保证线程在没有消息时休眠以免占用系统资源,有消息时可以及时唤醒。RunLoop的这个机制彻底依靠系统内核来完成,具体来讲是苹果操做系统核心组件Darwin中的Mach来完成的(Darwin是开源的)。能够从下图最底层Kernel中找到Mach:

osx_architecture-kernels_drivers

Mach是Darwin的核心,能够说是内核的核心,提供了进程间通讯(IPC)、处理器调度等基础服务。在Mach中,进程、线程间的通讯是以消息的方式来完成的,消息在两个Port之间进行传递(这也正是Source1之因此称之为Port-based Source的缘由,由于它就是依靠系统发送消息到指定的Port来触发的)。消息的发送和接收使用<mach/message.h>中的mach_msg()函数(事实上苹果提供的Mach API不多,并不鼓励咱们直接调用这些API):

 

mach_msg()的本质是一个调用mach_msg_trap(),这至关于一个系统调用,会触发内核状态切换。当程序静止时,RunLoop停留在**__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而这个函数内部就是调用了mach_msg**让程序处于休眠状态。

Runloop和线程的关系

Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操做底层API。它是mach thread的上层封装(能够参见Kernel Programming Guide),和NSThread一一对应(而NSThread是一套面向对象的API,因此在iOS开发中咱们也几乎不用直接使用pthread)。

pthread

苹果开发的接口中并无直接建立Runloop的接口,若是须要使用Runloop一般CFRunLoopGetMain()CFRunLoopGetCurrent()两个方法来获取(经过上面的源代码也能够看到,核心逻辑在_CFRunLoopGet_当中),经过代码并不难发现其实只有当咱们使用线程的方法主动get Runloop时才会在第一次建立该线程的Runloop,同时将它保存在全局的Dictionary中(线程和Runloop两者一一对应),默认状况下线程并不会建立Runloop(主线程的Runloop比较特殊,任何线程建立以前都会保证主线程已经存在Runloop),同时在线程结束的时候也会销毁对应的Runloop。

iOS开发过程当中对于开发者而言更多的使用的是NSRunloop,它默认提供了三个经常使用的run方法:

 
 
  • run方法对应上面CFRunloopRef中的CFRunLoopRun并不会退出,除非调用CFRunLoopStop();一般若是想要永远不会退出RunLoop才会使用此方法,不然可使用runUntilDate。
  • runMode:beforeDate:则对应CFRunLoopRunInMode(mode,limiteDate,true)方法,只执行一次,执行完就退出;一般用于手动控制RunLoop(例如在while循环中)。
  • runUntilDate:方法实际上是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false),执行完并不会退出,继续下一次RunLoop直到timeout。

RunLoop应用

NSTimer

前面一直提到Timer Source做为事件源,事实上它的上层对应就是NSTimer(其实就是CFRunloopTimerRef)这个开发者常常用到的定时器(底层基于使用mk_timer实现),甚至不少开发者接触RunLoop仍是从NSTimer开始的。其实NSTimer定时器的触发正是基于RunLoop运行的,因此使用NSTimer以前必须注册到RunLoop,可是RunLoop为了节省资源并不会在很是准确的时间点调用定时器,若是一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,若是确实想要使用NSTimer而且但愿尽量的准确,则能够设置此属性)。

NSTimer的建立一般有两种方式,尽管都是类方法,一种是timerWithXXX,另外一种scheduedTimerWithXXX。

 
 

两者最大的区别就是后者除了建立一个定时器外会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中,不添加到RunLoop中的NSTimer是没法正常工做的。例以下面的代码中若是timer2不加入到RunLoop中是没法正常工做的。同时注意若是滚动UIScrollView(UITableView、UICollectionview是相似的)两者是没法正常工做的,可是若是将NSDefaultRunLoopMode改成NSRunLoopCommonModes则能够正常工做,这也解释了前面介绍的Mode内容。

 
 

注意上面代码中UIViewController1对timer1和timer2并无强引用,对于普通的对象而言,执行完viewDidLoad方法以后(准确的说应该是执行完viewDidLoad方法后的的一个RunLoop运行结束)两者应该会被释放,但事实上两者并无被释放。缘由是:为了确保定时器正常运转,当加入到RunLoop之后系统会对NSTimer执行一次retain操做(特别注意:timer2建立时并没直接赋值给timer2,缘由是timer2是weak属性,若是直接赋值给timer2会被当即释放,由于timerWithXXX方法建立的NSTimer默认并无加入RunLoop,只有后面加入RunLoop之后才能够将引用指向timer2)。
可是即便使用了弱引用,上面的代码中ViewController1也没法正常释放,缘由是在建立NSTimer2时指定了target为self,这样一来形成了timer1和timer2对ViewController1有一个强引用。解决这个问题的方法一般有两种:一种是将target分离出来独立成一个对象(在这个对象中建立NSTimer并将对象自己做为NSTimer的target),控制器经过这个对象间接使用NSTimer;另外一种方式的思路仍然是转移target,只是能够直接增长NSTimer扩展(分类),让NSTimer自身作为target,同时能够将操做selector封装到block中。后者相对优雅,也是目前使用较多的方案(目前有大量相似的封装,例如:NSTimer+Block)。显然Apple也认识到了这个问题,若是你能够确保代码只在iOS 10下运行就可使用iOS 10新增的系统级block方案(上面的代码中已经贴出这种方法)。
固然使用上面第二种方法能够解决控制器没法释放的问题,可是会发现即便控制器被释放了两个定时器仍然正常运行,要解决这个问题就须要调用NSTimer的invalidate方法(注意:不管是重复执行的定时器仍是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操做后会自动调用invalidate方法)。修改后的代码以下:

 
 

其实和定时器相关的另外一个问题你们也常常碰到,那就是NSTimer不是一种实时机制,官方文档明确说明在一个循环中若是RunLoop没有被识别(这个时间大概在50-100ms)或者说当前RunLoop在执行一个长的call out(例如执行某个循环操做)则NSTimer可能就会存在偏差,RunLoop在下一次循环中继续检查并根据状况肯定是否执行(NSTimer的执行时间老是固定在必定的时间间隔,例如1:00:00、1:00:0一、1:00:0二、1:00:05则跳过了第四、5次运行循环)。
要演示这个问题请看下面的例子(注意:有些示例中可能会让一个线程中启动一个定时器,再在主线程启动一个耗时任务来演示这个问,若是实际测试可能效果不会太明显,由于如今的iPhone都是多核运算的,这样一来这个问题会变得相对复杂,所以下面的例子选择在同一个RunLoop中即加入定时器和执行耗时任务)

 
 

若是运行而且不退出上面的程序会发现,前两秒NSTimer能够正常执行,可是两秒后因为同一个RunLoop中循环操做的执行形成定时器跳过了中间执行的机会一直到caculator循环完毕,这也正说明了NSTimer不是实时系统机制的缘由。

可是以上程序还有几点须要说明一下:

  1. NSTimer会对Target进行强引用直到任务结束或exit以后才会释放。若是上面的程序没有进行线程cancel而终止任务则及时关闭控制器也没法正确释放。
  2. 非主线程的RunLoop并不会自动运行(同时注意默认状况下非主线程的RunLoop并不会自动建立,直到第一次使用),RunLoop运行必需要在加入NSTimer或Source0、Sourc一、Observer输入后运行不然会直接退出。例如上面代码若是run放到NSTimer建立以前则既不会执行定时任务也不会执行循环运算。
  3. performSelector:withObject:afterDelay:执行的本质仍是经过建立一个NSTimer而后加入到当前线程RunLoop(通而过先后两次打印RunLoop信息能够看到此方法执行以后RunLoop的timer会增长1个。相似的还有performSelector:onThread:withObject:afterDelay:,只是它会在另外一个线程的RunLoop中建立一个Timer),因此此方法事实上在任务执行完以前会对触发对象造成引用,任务执行完进行释放(例如上面会对ViewController造成引用,注意:performSelector: withObject:等方法则等同于直接调用,原理与此不一样)。
  4. 同时上面的代码也充分说明了RunLoop是一个循环事实,run方法以后的代码不会当即执行,直到RunLoop退出。
  5. 上面程序的运行过程当中若是忽然dismiss,则程序的实际执行过程要分为两种状况考虑:若是循环任务caculate尚未开始则会在timer1中中止timer1运行(中止了线程中第一个任务),而后等待caculate执行并break(中止线程中第二个任务)后线程任务执行结束释放对控制器的引用;若是循环任务caculate执行过程当中dismiss则caculate任务执行结束,等待timer1下个周期运行(由于当前线程的RunLoop并无退出,timer1引用计数器并不为0)时检测到线程取消状态则执行invalidate方法(第二个任务也结束了),此时线程释放对于控制器的引用。

CADisplayLink是一个执行频率(fps)和屏幕刷新相同(能够修改preferredFramesPerSecond改变刷新频率)的定时器,它也须要加入到RunLoop才能执行。与NSTimer相似,CADisplayLink一样是基于CFRunloopTimerRef实现,底层使用mk_timer(能够比较加入到RunLoop先后RunLoop中timer的变化)。和NSTimer相比它精度更高(尽管NSTimer也能够修改精度),不过和NStimer相似的是若是遇到大任务它仍然存在丢帧现象。一般状况下CADisaplayLink用于构建帧动画,看起来相对更加流畅,而NSTimer则有更普遍的用处。

AutoreleasePool

AutoreleasePool是另外一个与RunLoop相关讨论较多的话题。其实从RunLoop源代码分析,AutoreleasePool与RunLoop并无直接的关系,之因此将两个话题放到一块儿讨论最主要的缘由是由于在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。不妨在应用程序刚刚启动时打印currentRunLoop能够看到系统默认注册了不少个Observer,其中有两个Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,这两个是和自动释放池相关的两个监听。

 

第一个Observer会监听RunLoop的进入,它会回调objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增长一个哨兵对象标志建立自动释放池。这个Observer的order是-2147483647优先级最高,确保发生在全部回调操做以前。
第二个Observer会监听RunLoop的进入休眠和即将退出RunLoop两种状态,在即将进入休眠时会调用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据状况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出RunLoop时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer的order是2147483647,优先级最低,确保发生在全部回调操做以后。
主线程的其余操做一般均在这个AutoreleasePool以内(main函数中),以尽量减小内存维护操做(固然你若是须要显式释放【例如循环】时能够本身建立AutoreleasePool不然通常不须要本身建立)。
其实在应用程序启动后系统还注册了其余Observer(例如即将进入休眠时执行注册回调_UIGestureRecognizerUpdateObserver用于手势处理、回调为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面实时绘制更新)和多个Source1(例如context为CFMachPort的Source1用于接收硬件事件响应进而分发到应用程序一直到UIEvent),这里再也不一一详述。

UI更新

若是打印App启动以后的主线程RunLoop能够发现另一个callout为**_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv**的Observer,这个监听专门负责UI变化后的更新,好比修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout以后就会将这些操做提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历全部的UI更新并提交进行实际绘制更新。
一般状况下这种方式是完美的,由于除了系统的更新,还能够利用setNeedsDisplay等方法手动触发下一次RunLoop运行的更新。可是若是当前正在执行大量的逻辑运算可能UI的更新就会比较卡,所以facebook推出了AsyncDisplayKit来解决这个问题。AsyncDisplayKit实际上是将UI排版和绘制运算尽量放到后台,将UI的最终更新操做放到主线程(这一步也必须在主线程完成),同时提供一套类UIView或CALayer的相关属性,尽量保证开发者的开发习惯。这个过程当中AsyncDisplayKit在主线程RunLoop中增长了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务一一执行。

NSURLConnection

在前面的网络开发的文章中已经介绍过NSURLConnection的使用,一旦启动NSURLConnection之后就会不断调用delegate方法接收数据,这样一个连续的的动做正是基于RunLoop来运行。
一旦NSURLConnection设置了delegate会当即建立一个线程com.apple.NSURLConnectionLoader,同时内部启动RunLoop并在NSDefaultMode模式下添加4个Source0。其中CFHTTPCookieStorage用于处理cookie ;CFMultiplexerSource负责各类delegate回调并在回调中唤醒delegate内部的RunLoop(一般是主线程)来执行实际操做。
早期版本的AFNetworking库也是基于NSURLConnection实现,为了可以在后台接收delegate回调AFNetworking内部建立了一个空的线程并启动了RunLoop,当须要使用这个后台线程执行任务时AFNetworking经过performSelector: onThread: 将这个任务放到后台线程的RunLoop中。

GCD和RunLoop的关系

在RunLoop的源代码中能够看到用到了GCD的相关内容,可是RunLoop自己和GCD并无直接的关系。当调用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,而且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__回调里执行这个block。不过这个操做仅限于主线程,其余线程dispatch操做是所有由libDispatch驱动的。

更多RunLoop使用

前面看了不少RunLoop的系统应用和一些知名第三方库使用,那么除了这些究竟在实际开发过程当中咱们本身能不能适当的使用RunLoop帮咱们作一些事情呢?

思考这个问题其实只要看RunLoopRef的包含关系就知道了,RunLoop包含多个Mode,而它的Mode又是能够自定义的,这么推断下来其实不管是Source一、Timer仍是Observer开发者均可以利用,可是一般状况下不会自定义Timer,更不会自定义一个完整的Mode,利用更多的实际上是Observer和Mode的切换。
例如不少人都熟悉的使用perfromSelector在默认模式下设置图片,防止UITableView滚动卡顿([[UIImageView allocinitWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。还有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。再有老谭的PerformanceMonitor关于iOS实时卡顿监控,一样是利用Observer对RunLoop进行监视。

关于如何自定义一个Custom Input Source官网给出了详细的流程。

 
 
https://www.cnblogs.com/kenshincui/p/6823841.html
相关文章
相关标签/搜索