欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

RunLoop 的深入学习

程序员文章站 2022-07-15 14:34:28
...

RunLoop 是什么?

RunLoop 是和线程紧密相关的一个基础组件。顾名思议就是循环运行。按照 OC 的思路,RunLoop 其实就是一个对象,这个对象管理了其需要处理的事件和消息并提供一个入口函数来循环执行事件。平常,一般的 while 循环会让 CPU 处于忙等状态,而 RunLoop 则是一种“闲等”,当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会找对应的 Handler 处理事件。
逻辑代码如下:

+ (void) loop {
    [self initialize];
    do {
        id message = [self get_next_Message];
        [self process_message:message];

    } while (message != quit);
}

RunLoop 可以保持程序的正常运行,可以处理 APP 的各种事件(比如触摸、定时器等)。同时也节省了 CPU 的资源、 提高性能。

OS X/iOS 系统中,提供了两个对象:
- CFRunLoopRef : 在 Core Foundation 框架内,提供了纯 C函数且线程安全的 API
- NSRunLoop: 基于 CFRunLoop 的封装,提供了面向对象的 API,但这些线程是不安全的。

这两类 API 都可以访问和使用 RunLoop,但相对来说,CFRunLoopRef 的性能更高。

首先,看一下官方 RunLoop 结构图(下图的Input Source Port 对应的是 Source1)

RunLoop 的深入学习

注意:图中出现的 Input Source 和 Timer Source 都是 RunLoop 事件的来源。但是不同之处在于所有的 Timer 都共用一个端口 “Mode Timer Port” ,而每个Source1 都有不同的对应端口。

RunLoop 与线程

CFRunLoop 是基于 pthread 来管理的。每个线程都有一个对应的 RunLoop 对象。它们之间的关系保存在一个全局的 Dictionary 中。

苹果不允许直接创建 RunLoop,它提供了 CFRunLoopGetMain()CFRunLoopGetCurrent() 这两个 API 来获取 RunLoop 对象。

主线程的 RunLoop 会在应用启动的时候完成启动,其他线程的 RunLoop 默认并不会开启,需要我们主动获取。

RunLoop 的创建是发生在第一次获取时,销毁是在线程结束。并且你只能在一个线程的内部获取其 RunLoop(主线程外)。

RunLoop 相关类

在 Core Foundation 里有5个关于 RunLoop 的类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRed

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

RunLoop 的深入学习

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source、Observer、Timer。每次调用 RunLoop 的主函数 __CFRunLoopRun() 时必须且只能指定一个 Mode,这个 Mode 就被称为 CurrentMode;如果需要切换 Mode,只能退出 Loop,在重新指定一个 Mode 进入。

CFRunLoopSourceRef 是事件产生的地方。有两种Source:
- Source0: 仅包含一个回调(函数指针),不能主动触发事件;使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后在调用 CFRunLoopWakeUp(runloop)来唤醒 RunLoop,让其处理这个事件。它负责 APP 内部事件,由 APP 负责管理触发,例如 UITouch 事件。
- Source1: 包含一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程互相发送消息。能够主动唤醒 RunLoop 的线程。它由操作系统内核进行管理,例如 CFMessagePort 消息。

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

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。我们可以观测的状态有:

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
        kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
        kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
        kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
    }

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

RunLoop 的 Mode

首先,我们先来了解一下 CFRunLoopMode 和 CFRunLoop 的结构:

    struct __CFRunLoopMode {
        CFString _name;     // mode name
        CFMutableSetRef _sources0;
        CFMutableSetRef -sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        ···
    };
    struct __CFRunLoop {
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
        CFRunLoopModeRef _currentMode;    // Current RunLoop Mode
        CFMutableSetRef _modes;          
    }

CommonModes

CommonModes: 一个 Mode 可以通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中,从而将自己标记为”Common” 属性。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将_commonModeItems 里的 Source/Observer/Timer 同步到具有”Common”标记的所有 Mode 里。

应用场景:主线程的 RunLoop 里有两个预置的 Mode:KCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 APP 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时, Timer 会得到重复回调,当你滑动 TableView 时,Mode 就会切换成为 TrackingRunLoopMode ,这个时候 Timer 就不会回调,同时也不会影响到滑动操作。

有时候你需要一个 Timer在两个 Mode 中都可以得到回调,方法一就是将这个 Timer 分别加入到这两个 Mode;方法二就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。

Mode 相关的接口

CFRunLoop 对外暴露管理 Mode 接口:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFString modeName);
CFRunLoopRunInMode(CFStringRef modeName,...);

Mode 对外暴露的管理 item 的接口:

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

我们只能通过 modeName 来操作内部的 Mode,如果你传入一个新的 modeName 。但是 RunLoop 内部不存在时,RunLoop 会自动创建一个对应的 CFRunLoopModeRef;对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

我们在上面提到了两种预置的 Mode,当我们切换到对应的 Mode 时,我们只需要传入对应的名称即可。然而,还存在
KCFRunLoopCommonModes(NSRunloopCommonModes),它是一种组合模式,在 iOS 默认包含了
NSDefaultRunLoopModeUITrackingRunLoopMode(注意:并不是说Runloop会运行在 kCFRunLoopCommonModes 这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。当然你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes 组合)。

注意:
我们常常还会碰到一些系统框架自定义Mode,例如Foundation中NSConnectionReplyMode。还有一些系统私有Mode,例如:GSEventReceiveRunLoopMode接受系统事件,UIInitializationRunLoopMode App启动过程中初始化Mode。

系统默认注册了5个Mode:
- kCFRunLoopDefaultMode: APP 默认的Mode,通常主线程是在这个 Mode 下运行。
- UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动不受其他 Mode 影响。
- UIInitializationRunLoopMode: 在刚启动 APP 时进入的第一个 Mode,启动完之后就不会在使用。
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
- kCFRunLoopCommonModes: 占位 Mode,没有实际作用。

当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去(call out),当你在你的代码中断点调试时,通常能在调用栈上看到这些函数。

RunLoop 的内部逻辑

首先,通过一张图,来了解一下 RunLoop 的运行流程:
RunLoop 的深入学习

内部代码整理如下;

// 用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);
}

从源码很容易看出,Runloop 总是运行在某种特定的 CFRunLoopModeRef 下(每次运行__CFRunLoopRun()函数时必须指定 Mode )。它就是一个带有一个 do-while 循环的一个函数。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,只有超时、被手动停止或者 item 为空时,该函数才会返回。

RunLoop 的底层实现

其实,对于 RunLoop 而言,最核心的就是保证线程在没有消息时休眠从而避免占用系统资源,有消息传入时能够及时唤醒。而这个机制完全依靠系统内核来完成。

从上一节的源码中可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是mach_msg()。RunLoop 调用这个函数去接受消息,如果没有外部发来的 port消息,内核会一直将线程置于等待状态。

RunLoop 的应用

定时器

开头就提到的Timer Source 作为事件源,它的上层对应的就是 NSTimer(CFRunLoopTimerRef)。NSTimer 定时器的触发基于 RunLoop 运行,使用 NSTimer 之前必须注册到 RunLoop,但是 RunLoop 为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等待下一个时间点执行,并不会延后执行(NSTimer 提供了一个 tolerance 属性用于设置宽容度,可以通过设置此属性来尽可能的使 NSTimer 准确)。

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

AutoreleasePool

AutoreleasePool是另一个与RunLoop相关讨论较多的话题。其实从RunLoop源代码分析,AutoreleasePool与RunLoop并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在 APP 启动后,苹果在主线程 RunLoop 里注册了两个 Observer 管理和维护 AutorealeasePool,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视的事件是 Entry(即将进入 Loop), 其回调内会调用_objc_autoreleasePoolPush() 创建自动释放池。它的优先级最高,保证创建释放池发生在其他所有回调之前。

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

主线程中的其他操作通常均在这个 AutorelsePool 之内(main函数),以尽可能减少内存维护操作。

事件响应

苹果注册了一个 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 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

打印App启动之后的主线程 RunLoop 可以发现另外一个callout为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到全局容器。

这个 Observers 监听了主线程 RunLoop 的 BeforeWaiting(即将进入休眠)和 Exit (即将退出 Loop)状态,一旦进入到这两种状态则会遍历所有的 UI 更新并提交进行实际绘制更新。

该函数内部的调用栈大致如下:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

通常情况下这种方式是完美的,因为除了系统的更新,还可以利用 setNeedsDisplay 等方法手动触发下一次 RunLoop 运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook推出了AsyncDisplayKit来解决这个问题。有关 AsyncDiskplayKit ,后文会具体讲到。

PerformSelecter

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

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

关于 GCD

在 RunLoop 的源代码中可以看到 GCD 的相关东西,但是它俩本质是没有直接关系。
当调用dispatch_async(dispatch_get_main_queue(), block)时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

关于网络请求

iOS 中,关于网络请求的接口自下而上有如下几层:
- CFSocket:最底层的接口,只负责 socket 通信。
- CFNetwork: 基于 CFSocket 等接口的上层封装。
- NSURLConnection:基于 CFNetwork 的更高层封装,提供面向对象的接口
- NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSNRLConnection 的部分功能。

NSURLConnection 的工作过程

通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停的收到事件回调。实际上,start 这个函数的内部会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0(需要手动触发的 Source)。

  • CFHTTPCookieStorage: 用于处理 cookie
  • CFMultiplexerSource: 负责各种 Delegate 回调,并在回调中唤醒 Delegate 内部的 RunLoop(通常是主线程)来执行实际操作。

当开始网络传输时, NSURLConnection 会创建两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 sokcet 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
RunLoop 的深入学习
NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

具体实例举例

AFNetworking 2.x

AF 2.x 基于NSURLConnection包装的重要对象,由于iOS9-NSURLConnection已经不能使用,AFNetworking在3.x版本中删除了基于 NSURLConnection API的所有支持。

因此,我们要研究的就是 AFNetworking 2.x 。

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的。AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop,在后台接收 Delegate 回调。具体代码如下:

+ (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 中。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:

UI 线程中的任务通常分为三类:排版、绘制、UI 对象操作,当这些任务过于繁重的话就会导致界面卡顿。

排版通常包括计算视图大小、计算文本高度、重新计算子视图的排版等操作。

绘制一般有文本绘制(coreText)、图片绘制(例如预先解压)、元素绘制(Quartz)等操作。

UI 对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法放到后台去执行,而最后一项操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果(TextView 创建时可能需要提前计算出文本的大小)。

AsyncDisplayKit 所做的就是尽量将能放入到后台的任务放入后台,不能的则尽量推迟(例如视图的创建、属性的调整)。因此, AsyncDisplayKit 创建了一个名为 AsyDisplayNode 的对象,并在其内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如frame、backgroundColor 等。所有这些属性都可以在后台线程更改,开发者只可以通过 Node 来操作其内部的 UIView/CALayer .,这些就可以将排版和绘制放入了后台线程。但是无论怎样操作,但是属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

AsyncDisplayKit 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

相关标签: ios开发 ios