iOS,Runloop

1.Runloop概述

2.Runloop Mode

3.RunLoop应用

Runloop概述

RunLoop即运行循环,通常所说的RunLoop指的是NSRunloop或者CFRunloopRef,CFRunloopRef是纯C的函数,而NSRunloop仅仅是CFRunloopRef的OC封装,并未提供额外的其他功能,因此下面主要分析CFRunloopRef,苹果已经开源了CoreFoundation源代码,因此很容易找到CFRunloop源代码。

 

主要负责什么?

  1. 使程序一直运行并接受用户输入
  2. 决定程序在何时处理一些Event
  3. 调用解耦(Message Queue)
  4. 节省CPU时间(没事的时候闲着,有事的时候处理) 

Runloop运行流程(基本描述了上面Runloop的核心流程,当然可以查看官方The Run Loop Sequence of Events描述):整个流程并不复杂(需要注意的就是_黄色_区域的消息处理中并不包含source0,因为它在循环开始之初就会处理),整个流程其实就是一种Event Loop的实现,其他平台均有类似的实现,只是这里叫做Runloop。注意的是尽管CFRunLoopPerformBlock在上图中作为唤醒机制有所体现,但事实上执行CFRunLoopPerformBlock只是入队,下次RunLoop运行才会执行,而如果需要立即执行则必须调用CFRunLoopWakeUp。

iOS,Runloop_第1张图片

 

 

 

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互不影响。

 

 

系统默认提供的Run Loop Modes有:

kCFRunLoopDefaultMode(NSDefaultRunLoopMode),系统默认的Runloop Mode,例如进入iOS程序默认不做任何操作就处于这种Mode中

UITrackingRunLoopMode,需要切换到对应的Mode时只需要传入对应的名称即可。此时滑动UIScrollView,主线程就切换Runloop到到UITrackingRunLoopMode,不再接受其他事件操作(除非你将其他Source/Timer设置到UITrackingRunLoopMode下)。

kCFRunLoopCommonModesNSRunLoopCommonModes),其实这个并不是某种具体的Mode,而是一种模式组合,在iOS系统中默认包含了

 NSDefaultRunLoopMode和 UITrackingRunLoopMode(相当于分别注册了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。当然你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合)。

 

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

iOS,Runloop_第2张图片

那么CFRunLoopSourceRef、CFRunLoopTimerRef和CFRunLoopObserverRef在Runloop运行流程中起到什么作用:

 

Source

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

iOS,Runloop_第3张图片

再结合前面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

    struct __CFRunLoopObserver {

        CFRuntimeBase _base;

        pthread_mutex_t _lock;

        CFRunLoopRef _runLoop;

        CFIndex _rlCount;

        CFOptionFlags _activities;      /* immutable */

        CFIndex _order;         /* immutable */

        CFRunLoopObserverCallBack _callout; /* immutable */

        CFRunLoopObserverContext _context;  /* immutable, except invalidation */

    };

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

    /* Run Loop Observer Activities */

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

        kCFRunLoopEntry = (1UL << 0), // 进入RunLoop 

        kCFRunLoopBeforeTimers = (1UL << 1), // 即将开始Timer处理

        kCFRunLoopBeforeSources = (1UL << 2), // 即将开始Source处理

        kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠

        kCFRunLoopAfterWaiting = (1UL << 6), //从休眠状态唤醒

        kCFRunLoopExit = (1UL << 7), //退出RunLoop

        kCFRunLoopAllActivities = 0x0FFFFFFFU

    };

 

 

Call out

在开发过程中几乎所有的操作都是通过Call out进行回调的(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听Observer也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数):

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();

    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

例如在控制器的touchBegin中打入断点查看堆栈(由于UIEvent是Source0,所以可以看到一个Source0的Call out函数CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION调用):

iOS,Runloop_第4张图片

 

 

RunLoop休眠

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

iOS,Runloop_第5张图片

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

 

  /*

     *  Routine:    mach_msg

     *  Purpose:

     *      Send and/or receive a message.  If the message operation

     *      is interrupted, and the user did not request an indication

     *      of that fact, then restart the appropriate parts of the

     *      operation silently (trap version does not restart).

     */

    __WATCHOS_PROHIBITED __TVOS_PROHIBITED

    extern mach_msg_return_t    mach_msg(

                        mach_msg_header_t *msg,

                        mach_msg_option_t option,

                        mach_msg_size_t send_size,

                        mach_msg_size_t rcv_size,

                        mach_port_name_t rcv_name,

                        mach_msg_timeout_t timeout,

                        mach_port_name_t notify);

 

                        

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)。

iOS,Runloop_第6张图片

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

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

     - (void)run; 

     - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

     - (void)runUntilDate:(NSDate *)limitDate;

  • 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。

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;

    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

二者最大的区别就是后者除了创建一个定时器外会自动以NSDefaultRunLoopMode的Mode添加到当前线程RunLoop中,不添加到RunLoop中的NSTimer是无法正常工作的( [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];)。例如下面的代码中如果timer2不加入到RunLoop中是无法正常工作的。同时注意如果滚动UIScrollView(UITableView、UICollectionview是类似的)二者是无法正常工作的,但是如果将NSDefaultRunLoopMode改为NSRunLoopCommonModes则可以正常工作,这也解释了前面介绍的Mode内容。

   

 

 

AutoreleasePool

AutoreleasePool是另一个与RunLoop相关讨论较多的话题。其实从RunLoop源代码分析,AutoreleasePoolRunLoop并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。不妨在应用程序刚刚启动时打印currentRunLoop可以看到系统默认注册了很多个Observer,其中有两个Observercallout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,这两个是和自动释放池相关的两个监听。

    {valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = {type = mutable-small, count = 0, values = ()}}

    '' {valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = {type = mutable-small, count = 0, values = ()}}

第一个Observer会监听RunLoop的进入,它会回调objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增加一个哨兵对象标志创建自动释放池。这个Observerorder-2147483647优先级最高,确保发生在所有回调操作之前。

第二个Observer会监听RunLoop的进入休眠和即将退出RunLoop两种状态,在即将进入休眠时会调用objc_autoreleasePoolPop()  objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出RunLoop时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observerorder2147483647,优先级最低,确保发生在所有回调操作之后。

主线程的其他操作通常均在这个AutoreleasePool之内(main函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建AutoreleasePool否则一般不需要自己创建)。

其实在应用程序启动后系统还注册了其他Observer(例如即将进入休眠时执行注册回调_UIGestureRecognizerUpdateObserver用于手势处理、回调为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPvObserver用于界面实时绘制更新)和多个Source1(例如contextCFMachPortSource1用于接收硬件事件响应进而分发到应用程序一直到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以后就会不断调用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中。

 

GCDRunLoop的关系

在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又是可以自定义的,这么推断下来其实无论是Source1、Timer还是Observer开发者都可以利用,但是通常情况下不会自定义Timer,更不会自定义一个完整的Mode,利用更多的其实是Observer和Mode的切换。

例如很多人都熟悉的使用perfromSelector在默认模式下设置图片,防止UITableView滚动卡顿([[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。还有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。再有老谭的PerformanceMonitor关于iOS实时卡顿监控,同样是利用Observer对RunLoop进行监视。

 

转载于:https://www.cnblogs.com/douniwanxia/p/8057898.html

你可能感兴趣的:(iOS,Runloop)