RunLoop

作者: SunZzzl | 来源:发表于2017-05-23 21:02 被阅读34次

    RunLoop的概念

    一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

    function loop() {
        initialize();
        do {
            var message = get_next_message();
            process_message(message);
        } while (message != quit);
    }
    

    这其实就是一种事件模型(Event Loop),实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

    所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

    RunLoop基本作用

    • 保持程序的持续运行(ios程序为什么能一直活着不会死)
    • 处理app中的各种事件(比如触摸事件、定时器事件(NSTimer)、selector事件(performSelector)
    • 节省CPU资源,提高程序性能,有事情就做事情,没事情就休息

    重要说明

    • 如果没有Runloop,那么程序一启动就会退出,什么事情都做不了。
    • 如果有了Runloop,那么相当于在内部有一个死循环,能够保证程序的持续运行
    • main函数中的Runloop
      a 在UIApplication函数内部就启动了一个Runloop,该函数返回一个int类型的值
      b 这个默认启动的Runloop是跟主线程相关联的
    WechatIMG49.jpeg

    在上图的调用栈中可以看到相关信息

    RunLoop相关类

    iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
    CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
    NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

    RunLoop与线程之间的关系

    • Runloop和线程的关系:一个Runloop对应着一条唯一的线程

    • Runloop的创建:主线程Runloop已经创建好了,子线程的runloop需要手动创建获得当前Runloop对象
      // 获取子线程中的
      // NSRunloop
      NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];(本身是懒加载)
      // CFRunLoopRef
      CFRunLoopRef runloop2 = CFRunLoopGetCurrent();

            // 获取当前应用程序的主Runloop(主线程对应的Runloop)
            //NSRunloop
            NSRunLoop * runloop1 = [NSRunLoop mainRunLoop];
            // CFRunLoopRef
            CFRunLoopRef runloop2 =   CFRunLoopGetMain();
      
    • Runloop的生命周期:在第一次获取时创建,在线程结束时销毁

    • 注意点:
      在子线程中,如果不主动获取Runloop的话,那么子线程内部是不会创建Runloop的。

    相关代码

        // Runloop对象是利用全局的Dictionary来进行存储,key是对应的线程, value 是 CFRunLoopRef
        static CFMutableDictionaryRef loopsDic;
        // 访问 loopsDic 时的锁
        static CFSpinLock_t loopsLock;
        
        // 获取一个 pthread 对应的 RunLoop。
        CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
            OSSpinLockLock(&loopsLock);
            
            if (!loopsDic) {
                // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
                loopsDic = CFDictionaryCreateMutable();
                CFRunLoopRef mainLoop = _CFRunLoopCreate();
                CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
            }
            
            /// 直接从 Dictionary 里获取。
            CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
            
            if (!loop) {
                /// 取不到时,创建一个
                loop = _CFRunLoopCreate();
                CFDictionarySetValue(loopsDic, thread, loop);
                /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
                _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
            }
            
            OSSpinLockUnLock(&loopsLock);
            return loop;
        }
        
        CFRunLoopRef CFRunLoopGetMain() {
            return _CFRunLoopGet(pthread_main_thread_np());
        }
        
        CFRunLoopRef CFRunLoopGetCurrent() {
            return _CFRunLoopGet(pthread_self());
        }    
    

    (可以下载CFRunloopRef的源码,搜索_CFRunloopGet0,查看代码.
    相关源码下载链接: http://opensource.apple.com/source/CF/CF-1151.16/)

    RunLoop对外的接口

    在 CoreFoundation 里面关于 RunLoop 有5个类:
    - a.CFRunloopRef
    b.CFRunloopModeRef【Runloop的运行模式】
    c.CFRunloopSourceRef【Runloop要处理的事件源】
    d.CFRunloopTimerRef【Timer事件】
    e.CFRunloopObserverRef【Runloop的观察者(监听者)】
    Runloop和相关类之间的关系图

    WechatIMG1.jpeg
    CFRunloopModeRef
    • Runloop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\observer\timer,至少要有其中的一个。
      01.CFRunloopModeRef代表着Runloop的运行模式
      02.一个Runloop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等
      03.每次runloop启动的时候,只能指定一个mode,这个mode被称为该Runloop的当前mode
      04.如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入
      05.这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响
      06.系统默认注册了5个mode
      a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
      b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
      c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
      d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
      e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
    CFRunLoopSourceRef(输入源)

    Source有两个版本:Source0 和 Source1
    输入源向线程发送异步消息。消息来源取决于输入源的种类:基于端口的输入源和自定义输入源。基于端口的源监听程序相应的端口,而自定义输入源则关注 自定义的消息。至于run loop,它不关心输入源的种类。系统会去实现两种源供你使用。两类输入源的区别在于如何显示的:基于端口的源由内核自动发送,而自定义的则需要人工从其 他线程发送。
    当你创建输入源,你需要将其分配给run loop中的一个或多个模式。模式只会在特定事件影响监听的源。大多数情况下,run loop运行在默认模式下,但是你也可以使其运行在自定义模式。若某一源在当前模式下不被监听,那么任何其生成的消息只有当run loop运行在其关联的模式下才会被传递。
    http://www.cnblogs.com/scorpiozj/

    简单理解:
    Source0:非基于Port的自定义输入源(例如用户界面交互事件,@selecter事件) 而自定义的输入源则需要人工从其他线程发送。
    Source1:基于Port的输入源( 可以通过NSPort的子类来实现) 基于端口的输入源由内核自动发送,

    基于端口的源:

    cocoa和core foundation为使用端口相关的对象和函数创建的基于端口的源提供了内在支持。Cocoa中你从不需要直接创建输入源。你只需要简单的创建端口对象,并使用NSPort的方法将端口对象加入到run loop。端口对象会处理创建以及配置输入源。
    在core foundation,你必须手动的创建端口和源,你都可以使用端口类型(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建。

    自定义输入源:

    在Core Foundation程序中,必须使用CFRunLoopSourceRef类型相关的函数来创建自定义输入源,接着使用回调函数来配置输入源。Core Fundation会在恰当的时候调用回调函数,处理输入事件以及清理源。
    除了定义如何处理消息,你也必须定义源的消息传递机制——它运行在单独的进程,并负责传递数据给源和通知源处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。
    关于创建自定义输入源的例子,见 定义自定义输入源。关于自定义输入源的信息参见CFRunLoopSource。

    Cocoa Perform Selector Sources:

    除了基于端口的源,Cocoa提供了可以在任一线程执行函数(perform selector)的输入源。和基于端口的源一样,perform selector请求会在目标线程上序列化,减缓许多在单个线程上容易引起的同步问题。而和基于端口的源不同的是,perform selector执行完后会自动清除出run loop。
    当perform selector在其它线程中执行时,目标线程须有一活动中的run loop。对于你创建的线程而言,这意味着线程直到你显示的开始run loop否则处于等待状态。然而,由于主线程自己启动run loop,在程序调用applicationDidFinishlaunching:的时候你会遇到线程调用的问题。因为Run loop通过每次循环来处理所有排列的perform selector调用,而不时通过每次的循环迭代来处理perform selector
    http://blog.csdn.net/csj1987/article/details/7527590

    CFRunLoopTimerRef

    是基于时间的触发器,它和 NSTimer 是 可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

    CFRunLoopObserverRef

    CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

               //创建一个runloop监听者
                CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    
                    NSLog(@"监听runloop状态改变---%zd",activity);
                });
    
                //为runloop添加一个监听者
                CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
                CFRelease(observer);
            // 监听的状态
            typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
                kCFRunLoopEntry = (1UL << 0),   //即将进入Runloop
                kCFRunLoopBeforeTimers = (1UL << 1),    //即将处理NSTimer
                kCFRunLoopBeforeSources = (1UL << 2),   //即将处理Sources
                kCFRunLoopBeforeWaiting = (1UL << 5),   //即将进入休眠
                kCFRunLoopAfterWaiting = (1UL << 6),    //刚从休眠中唤醒
                kCFRunLoopExit = (1UL << 7),            //即将退出runloop
                kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有状态改变
            };
    

    综上 :

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

    RunLoop运行逻辑

    3.png

    官方图:

    WechatIMG56.jpeg

    更好理解的

    4.png

    其内部代码整理如下

    /// 用DefaultMode启动
    void CFRunLoopRun(void) {
        CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
    }
     
    /// 用指定的Mode启动,允许设置RunLoop超时时间
    int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
        return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
    }
     
    /// RunLoop的实现
    int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
        
        /// 首先根据modeName找到对应mode
        CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
        /// 如果mode里没有source/timer/observer, 直接返回。
        if (__CFRunLoopModeIsEmpty(currentMode)) return;
        
        /// 1. 通知 Observers: RunLoop 即将进入 loop。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
        
        /// 内部函数,进入loop
        __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
            
            Boolean sourceHandledThisLoop = NO;
            int retVal = 0;
            do {
     
                /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
                /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
                /// 4. RunLoop 触发 Source0 (非port) 回调。
                sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
     
                /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
                if (__Source0DidDispatchPortLastTime) {
                    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                    if (hasMsg) goto handle_msg;
                }
                
                /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
                if (!sourceHandledThisLoop) {
                    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
                }
                
                /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
                /// • 一个基于 port 的Source 的事件。
                /// • 一个 Timer 到时间了
                /// • RunLoop 自身的超时时间到了
                /// • 被其他什么调用者手动唤醒
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                    mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
                }
     
                /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
                
                /// 收到消息,处理消息。
                handle_msg:
     
                /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
                if (msg_is_timer) {
                    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
                } 
     
                /// 9.2 如果有dispatch到main_queue的block,执行block。
                else if (msg_is_dispatch) {
                    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                } 
     
                /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
                else {
                    CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                    sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                    if (sourceHandledThisLoop) {
                        mach_msg(reply, MACH_SEND_MSG, reply);
                    }
                }
                
                /// 执行加入到Loop的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
     
                if (sourceHandledThisLoop && stopAfterHandle) {
                    /// 进入loop时参数说处理完事件就返回。
                    retVal = kCFRunLoopRunHandledSource;
                } else if (timeout) {
                    /// 超出传入参数标记的超时时间了
                    retVal = kCFRunLoopRunTimedOut;
                } else if (__CFRunLoopIsStopped(runloop)) {
                    /// 被外部调用者强制停止了
                    retVal = kCFRunLoopRunStopped;
                } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                    /// source/timer/observer一个都没有了
                    retVal = kCFRunLoopRunFinished;
                }
                
                /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
            } while (retVal == 0);
        }
        
        /// 10. 通知 Observers: RunLoop 即将退出。
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    }
    
    

    参考:http://blog.ibireme.com/2015/05/18/runloop/

    iOS 系统利用RunLoop所做的事情

    AutoreleasePool

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

    事件响应

    苹果注册了一个 Source0 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

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

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

    下图就是测试的一个按钮点击事件的传递处理过程,查看调用栈

    WechatIMG69.jpeg
    手势识别

    当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

    苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

    当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

    界面更新

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

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

    定时器

    NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

    如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

    CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop.

    PerformSelecter

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

    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    同样的可以通过断点查看相应的函数调用栈来观察整个过程


    WechatIMG2.jpeg
    网络请求

    iOS 中,关于网络请求的接口自下至上有如下几层:

    CFSocket
    CFNetwork       ->ASIHttpRequest
    NSURLConnection ->AFNetworking
    NSURLSession    ->AFNetworking2, Alamofire
    

    • CFSocket 是最底层的接口,只负责 socket 通信。
    • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
    • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
    • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。


    ![WechatIMG3.jpeg](https://img.haomeiwen.com/i1921775/481505c61831d87c.jpeg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件

    RunLoop开发中实际应用

    AFNetworking 2.0

    AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:

    + (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;
    }
    

    RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 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];
    }
    

    当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

    FDTemplateLayoutCell

    利用RunLoop空闲时间执行预缓存任务

    FDTemplateLayoutCell 的高度预缓存是一个优化功能,它要求页面处于空闲状态时才执行计算,当用户正在滑动列表时显然不应该执行计算任务影响滑动体验。
    一般来说,这个功能要耦合 UITableView 的滑动状态才行,但这种实现十分不优雅且可能破坏外部的 delegate 结构,但好在我们还有RunLoop这个工具,了解它的运行机制后,可以用很简单的代码实现上面的功能。

    空闲RunLoopMode

    当用户正在滑动 UIScrollView 时,RunLoop 将切换到 UITrackingRunLoopMode 接受滑动手势和处理滑动事件(包括减速和弹簧效果),此时,其他 Mode (除 NSRunLoopCommonModes 这个组合 Mode)下的事件将全部暂停执行,来保证滑动事件的优先处理,这也是 iOS 滑动顺畅的重要原因。
    当 UI 没在滑动时,默认的 Mode 是 NSDefaultRunLoopMode(同 CF 中的 kCFRunLoopDefaultMode),同时也是 CF 中定义的 “空闲状态 Mode”。当用户啥也不点,此时也没有什么网络 IO 时,就是在这个 Mode 下。

    用RunLoopObserver找准时机

    注册 RunLoopObserver 可以观测当前 RunLoop 的运行状态,并在状态机切换时收到通知:

    RunLoop开始
    RunLoop即将处理Timer
    RunLoop即将处理Source
    RunLoop即将进入休眠状态
    RunLoop即将从休眠状态被事件唤醒
    RunLoop退出
    因为“预缓存高度”的任务需要在最无感知的时刻进行,所以应该同时满足:

    RunLoop 处于“空闲”状态 Mode
    当这一次 RunLoop 迭代处理完成了所有事件,马上要休眠时
    使用 CF 的带 block 版本的注册函数可以让代码更简洁:

    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFStringRef runLoopMode = kCFRunLoopDefaultMode;
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
    (kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
        // TODO here
    });
    CFRunLoopAddObserver(runLoop, observer, runLoopMode);
    

    在其中的 TODO 位置,就可以开始任务的收集和分发了,当然,不能忘记适时的移除这个 observer

    分解成多个RunLoop Source任务

    假设列表有 20 个 cell,加载后展示了前 5 个,那么开启估算后 table view 只计算了这 5 个的高度,此时剩下 15 个就是“预缓存”的任务,而我们并不希望这 15 个计算任务在同一个 RunLoop 迭代中同步执行,这样会卡顿 UI,所以应该把它们分别分解到 15 个 RunLoop 迭代中执行,这时就需要手动向 RunLoop 中添加 Source 任务(由应用发起和处理的是 Source 0 任务)
    Foundation 层没对 RunLoopSource 提供直接构建的 API,但是提供了一个间接的、既熟悉又陌生的 API:

    - (void)performSelector:(SEL)aSelector
                   onThread:(NSThread *)thr
                 withObject:(id)arg
              waitUntilDone:(BOOL)wait
                      modes:(NSArray *)array;
    

    这个方法将创建一个 Source 0 任务,分发到指定线程的 RunLoop 中,在给定的 Mode 下执行,若指定的 RunLoop 处于休眠状态,则唤醒它处理事件,简单来说就是“睡你xx,起来嗨!”
    于是,我们用一个可变数组装载当前所有需要“预缓存”的 index path,每个 RunLoopObserver 回调时都把第一个任务拿出来分发:

    NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy;
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
        if (mutableIndexPathsToBePrecached.count == 0) {
            CFRunLoopRemoveObserver(runLoop, observer, runLoopMode);
            CFRelease(observer); // 注意释放,否则会造成内存泄露
            return;
        }
        NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject;
        [mutableIndexPathsToBePrecached removeObject:indexPath];
        [self performSelector:@selector(fd_precacheIndexPathIfNeeded:)
                     onThread:[NSThread mainThread]
                   withObject:indexPath
                waitUntilDone:NO
                        modes:@[NSDefaultRunLoopMode]];
    });
    

    这样,每个任务都被分配到下个“空闲” RunLoop 迭代中执行,其间但凡有滑动事件开始,Mode 切换成 UITrackingRunLoopMode,所有的“预缓存”任务的分发和执行都会自动暂定,最大程度保证滑动流畅。

    本文主要参考文献:
    http://blog.sunnyxx.com/2015/05/17/cell-height-calculation/
    http://blog.ibireme.com/2015/05/18/runloop/
    http://opensource.apple.com/source/CF/CF-1151.16/)
    http://www.cnblogs.com/scorpiozj/
    http://blog.csdn.net/csj1987/article/details/7527590

    相关文章

      网友评论

          本文标题:RunLoop

          本文链接:https://www.haomeiwen.com/subject/jycxzttx.html