Objective-C:RunLoop

深入理解RunLoop

Posted by kunnan on November 28, 2016

前言

  • Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop.
    苹果利用 RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的。
    

目录

  • RunLoop 与线程的关系
  • RunLoop 对外的接口
  • RunLoop 的 Mode
  • RunLoop 的内部逻辑
  • RunLoop 的底层实现
  • 苹果用 RunLoop 实现的功能
    • AutoreleasePool
    • 事件响应
    • 手势识别
    • 界面更新
    • 定时器
    • PerformSelecter
    • 关于GCD
    • 关于网络请求
  • RunLoop 的实际应用举例
    • AFNetworking
    • AsyncDisplayKit

RunLoop 的概念

  • A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events.
    The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
    
  • 如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
    The programmatic interface to objects that manage input sources.
    1) RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行Event Loop 的逻辑。
    2) 线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(激活触发退出循环的条件,函数返回。
    3) OSX/iOS 系统中,提供了两个这样的对象:CFRunLoopRef(CoreFoundation,提供了纯 C 函数的 API,所有这些 API 都是线程安全的)。
    NSRunLoop(基于 CFRunLoopRef 的封装,提供的 API不是线程安全的)
    
  • CFRunLoopRef 的代码开源
  • 可以在这里下载到整个 CoreFoundation 的源码来查看。
  • note: Swift 开源后,苹果又维护了一个跨平台的 CoreFoundation 版本适配了 Linux/Windows.

I、 多线程

II、RunLoop 与线程的关系

  • Run loops are part of the fundamental infrastructure associated with threads
    A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
    
  • CFRunLoop是基于pthread来管理的。
  • 苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()CFRunLoopGetCurrent()

III、RunLoop 对外的接口

3.0 NSRunLoop

The programmatic interface to objects that manage input sources.

3.0.1 Overview

A NSRunLoop object processes input for sources such as mouse and keyboard events from the window system, NSPort objects, and NSConnection objects. A NSRunLoop object also processes NSTimer events

  • Your application neither creates or explicitly manages NSRunLoop objects.
    Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.
    
  • Note that from the perspective of NSRunLoop
    NSTimer objects are not "input"—they are a special type, and one of the things that means is that they do not cause the run loop to return when they fire.
    
  • Warning: The NSRunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread.
3.0.2 Topics
  • Accessing Run Loops and Modes
  • Managing Timers
    1)addTimer:forMode:
    Registers a given timer with a given input mode.
    2) Discussion:
    You can add a timer to multiple input modes
    
  • Managing Ports
  • Running a Loop

  • Scheduling and Canceling Messages
  • Constants Instance Methods ```
  • performBlock:
  • performInModes:block: ```
3.0.3 See Also
  • Run Loop Scheduling
    NSTimer: A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.
    

3.1 CFRunLoop.h

  • CFRunLoop.c
    https://github.com/iossrc/swift-corelibs-foundation
    
  • #import <CoreFoundation/CFRunLoop.h>关于RunLoop有5个类: RunLoop 有5个类的关系
  • CFRunLoopSourceContextCFRunLoopTimerContextCFRunLoopObserverContextstruct
    1)一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
    2)每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。---这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
    

3.2 Input Sources

  • Input sources deliver events asynchronously to your threads
    1)  Port-based input sources monitor your application’s Mach ports.
    2)  Custom input sources monitor custom sources of events. 
    
  • The only difference between the two sources is how they are signaled.
    1)Port-based sources are signaled automatically by the kernel, 
    2) and custom sources must be signaled manually from another thread
    
3.2.1 CFRunLoopSourceRef—perform
  • CFRunLoopSourceRef 是事件产生的地方。
     void	(*perform)(void *info);//Source0手动调用 `CFRunLoopWakeUp(runloop)` 来唤醒 RunLoop,让其处理这个事件perform
    
  • Source 有两个版本:Source0 和 Source1。
    1)Source0 只包含了一个回调(函数指针)perform,它并不能主动触发事件。使用时,你需要先调用 `CFRunLoopSourceSignal(source)`,将这个 Source 标记为待处理,然后手动调用 `CFRunLoopWakeUp(runloop)` 来唤醒 RunLoop,让其处理这个事件perform。
    2)Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程
    
3.2.2 Port-Based Sources: Source1
  • For example, in Cocoa, you never have to create an input source directly at all.
    You simply create a port object and use the methods of NSPort to add that port to the run loop. 
    The port object handles the creation and configuration of the needed input source for you.
    
  • In Core Foundation, you must manually create both the port and its run loop source.

  • In both cases, you use the functions associated with the port opaque type (CFMachPortRef, CFMessagePortRef, or CFSocketRef) to create the appropriate objects
  • Configuring a Port-Based Input Source.
3.2.3 Custom Input Sources: Source0
  • To create a custom input source, you must use the functions associated with the CFRunLoopSourceRef opaque type in Core Foundation.
    You configure a custom input source using several callback functions.
    Core Foundation calls these functions at different points to configure the source, handle any incoming events, and tear down the source when it is removed from the run loop.
    
  • For an example of how to create a custom input source

  • see also CFRunLoopSource Reference
3.2.4 Cocoa Perform Selector Sources
  • Cocoa defines a custom input source that allows you to perform a selector on any thread
    a perform selector source removes itself from the run loop after it performs its selector.
    
  • Performing selectors on other threads

3.3 CFRunLoopTimerRef: Timer Sources

  • Timer sources deliver events synchronously to your threads at a preset time in the future. Timers are a way for a thread to notify itself to do something
    Like input sources, timers are associated with specific modes of your run loop.
    You can configure timers to generate events only once or repeatedly
    
  • CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。
    当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
    

3.4 CFRunLoopObserverRef: Run Loop Observers

  • To create a run loop observer, you create a new instance of the CFRunLoopObserverRef opaque type. This type keeps track of your custom callback function and the activities in which it is interested
  • CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针)

  • You might use run loop observers to prepare your thread to process a given event or to prepare the thread before it goes to sleep. You can associate run loop observers with the following events in your run loop:

3.5 mode item

  • Source/Timer/Observer 被统称为 mode item
    一个 item 可以被同时加入多个 mode。
    但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环
    
  • 小结
    1)一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
    2)每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。---这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
    

IV、 RunLoop 的 Mode

A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified.

  • In your code, you identify modes by name.
    You can define custom modes by simply specifying a custom string for the mode name.
    You must be sure to add one or more input sources, timers, or run-loop observers to any modes you create for them to be useful.
    
  • Note: Modes discriminate based on the source of the event
    You could use modes to listen to a different set of ports, suspend timers temporarily, or otherwise change the sources and run loop observers currently being monitored
    
  • 重要概念CommonModes
    NSRunLoopCommonModes (Cocoa)
    kCFRunLoopCommonModes (Core Foundation)
    This is a configurable group of commonly used modes. Associating an input source with this mode also associates it with each of the modes in the group. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially. You can add custom modes to the set using the CFRunLoopAddCommonMode function.
    1)一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。
    2)每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。
    

4.1 CFRunLoop对外暴露的管理 Mode 接口只有下面2个:

  • CFRunLoop对外暴露的管理 Mode 接口只有下面2个

4.2 Mode 暴露的管理 mode item 的接口有下面几个:

  • Mode 暴露的管理 mode item 的接口
  • 苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。
4.2.1 Predefined run loop modes
  • Default
    NSDefaultRunLoopMode (Cocoa)
    kCFRunLoopDefaultMode (Core Foundation)
    The default mode is the one used for most operations. Most of the time, you should use this mode to start your run loop and configure your input sources.
    
  • Connection
    NSConnectionReplyMode (Cocoa)
    Cocoa uses this mode in conjunction with NSConnection objects to monitor replies. You should rarely need to use this mode yourself.
    
  • Modal
    NSModalPanelRunLoopMode (Cocoa)
    Cocoa uses this mode to identify events intended for modal panels.
    
  • Event tracking
    NSEventTrackingRunLoopMode (Cocoa)
    Cocoa uses this mode to restrict incoming events during mouse-dragging loops and other sorts of user interface tracking loops.
    
  • Common modes
    NSRunLoopCommonModes (Cocoa)
    kCFRunLoopCommonModes (Core Foundation)
    

4.3 同时苹果还提供了一个操作 Common 标记的字符串:

  • kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。

V、RunLoop的内部逻辑:The Run Loop Sequence of Events

  • Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events

5.1 Anatomy of a Run Loop

It is a loop your thread enters and uses to run event handlers in response to incoming events.

5.1.0 以runUntilDate: 为例子
  • runUntilDate
- (void)runUntilDate:(NSDate *)limitDate;
  • Structure of a run loop and its sources
  • A run loop receives events from two different types of sources.
    1)  Input sources deliver asynchronous events, usually messages from another thread or from a different application.
    2) Timer sources deliver synchronous events, occurring at a scheduled time or repeating interval.
    
5.1.1 以int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) 为例子,即: CFRunLoopRun方法

RunLoop的实现:CFRunLoopRunSpecific其内部代码整理

实际上CFRunLoopRun 就是这样一个函数,其内部是一个 do-while 循环。
当你调用 `CFRunLoopRun()` 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
  • your thread’s run loop processes pending events and generates notifications for any attached observers
  1. Notify observers that the run loop has been entered.

  2. Notify observers that any ready timers are about to fire.

  3. Notify observers that any input sources that are not port based are about to fire.

  4. Fire any non-port-based input sources that are ready to fire.

  5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.

  6. Notify observers that the thread is about to sleep.

  7. Put the thread to sleep until one of the following events occurs:

    • An event arrives for a port-based input source.

    • A timer fires.

    • The timeout value set for the run loop expires.

    • The run loop is explicitly woken up.

  8. Notify observers that the thread just woke up.

  9. Process the pending event.

    • If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.

    • If an input source fired, deliver the event.

    • If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.

  10. Notify observers that the run loop has exited.

  11. </section></blockquote>

VI、 When Would You Use a Run Loop?

  • For example, you need to start a run loop if you plan to do any of the following

VII、Using Run Loop Objects

  • A run loop object provides the main interface for adding input sources, timers, and run-loop observers to your run loop and then running it.
    Every thread has a single run loop object associated with it. 
    In Cocoa, this object is an instance of the NSRunLoop class. 
    In a low-level application, it is a pointer to a CFRunLoopRef opaque type.
    

7.1 Getting a Run Loop Object

  • In a Cocoa application, use the currentRunLoop class method of NSRunLoop to retrieve an NSRunLoop object.
  • Use the CFRunLoopGetCurrent function.
  • Although they are not toll-free bridged types, you can get a CFRunLoopRef opaque type from an NSRunLoop object when needed.
    The NSRunLoop class defines a getCFRunLoop method that returns a CFRunLoopRef type that you can pass to Core Foundation routines. 
    Because both objects refer to the same run loop, you can intermix calls to the NSRunLoop object and CFRunLoopRef opaque type as needed.
    

7.2 Configuring the Run Loop

  • Before you run a run loop on a secondary thread, you must add at least one input source or timer to it.

7.3 Starting the Run Loop

A run loop must have at least one input source or timer to monitor. If one is not attached, the run loop exits immediately.

  • There are several ways to start the run loop, including the following:
    1、Unconditionally
    2、With a set time limit
    3、In a particular mode
    
  • unconditionally
    You can add and remove input sources and timers, but the only way to stop the run loop is to kill it
    
  • it is better to run the run loop with a timeout value
    When you use a timeout value, the run loop runs until an event arrives or the allotted time expires.
    1) If an event arrives, that event is dispatched to a handler for processing and then the run loop exits. Your code can then restart the run loop to handle the next event.
    2)If the allotted time expires instead, you can simply restart the run loop or use the time to do any needed housekeeping
    
  • you can also run your run loop using a specific mod
    more detail in Run Loop Modes: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW12
    
  • example: Running a run loop

7.4 Exiting the Run Loop

  • There are two ways to make a run loop exit before it has processed an event:
    1) Configure the run loop to run with a timeout value.
    2) Tell the run loop to stop.
    
  • Using a timeout value is certainly preferred, if you can manage it
    Specifying a timeout value lets the run loop finish all of its normal processing, including delivering notifications to run loop observers, before exiting.
    
  • topping the run loop explicitly with the CFRunLoopStop function produces a result similar to a timeout.
    The run loop sends out any remaining run-loop notifications and then exits
    
  • Although removing a run loop’s input sources and timers may also cause the run loop to exit, this is not a reliable way to stop a run loop

7.5 Thread Safety and Run Loop Objects

  • The functions in Core Foundation are generally thread-safe and can be called from any thread
  • The Cocoa NSRunLoop class is not as inherently thread safe as its Core Foundation counterpart
    Adding an input source or timer to a run loop belonging to a different thread could cause your code to crash or behave in an unexpected way.
    

VIII、 Configuring Run Loop Sources

8.1 Defining a Custom Input Source

  • Creating a custom input source involves defining the following:
    1)The information you want your input source to process.
    2) A scheduler routine to let interested clients know how to contact your input source.
    3) A handler routine to perform requests sent by any clients.
    4) A cancellation routine to invalidate your input source.
    
  • Operating a custom input source

8.2 Defining the Input Source

  • uses an Objective-C object to manage a command buffer and coordinate with the run loop.
  • Scheduling a run loop source

  • Performing work in the input source
  • Invalidating an input source

8.3 Installing the Input Source on the Run Loop

  • Installing the run loop source

8.4 Coordinating with Clients of the Input Source

  • Registering and removing an input source with the application delegate

8.5 Signaling the Input Source

After it hands off its data to the input source, a client must signal the source and wake up its run loop.

  • shows the fireCommandsOnRunLoop method of the RunLoopSource object

IX Configuring Timer Sources

  • create a timer object and schedule it on your run loop
    1)  In Cocoa, you use the NSTimer class to create new timer objects,
    2) and in Core Foundation you use the CFRunLoopTimerRef opaque type. 
    
  • In Cocoa, you can create and schedule a timer all at once using either of these class methods:
    scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
    scheduledTimerWithTimeInterval:invocation:repeats:
    These methods create the timer and add it to the current thread’s run loop in the default mode (NSDefaultRunLoopMode).
    
  • adding it to the run loop using the addTimer:forMode: method of NSRunLoop

9.1 how to create timers using both techniques: NSTimerCore Foundation

9.1.1 Creating and scheduling timers using NSTimer
9.1.2 Creating and scheduling a timer using Core Foundation

X、Configuring a Port-Based Input Source

Both Cocoa and Core Foundation provide port-based objects for communicating between threads or between processes.

10.0 Configuring an NSMachPort Object

  • Main thread launch method
  • Handling Mach port messages
  • Launching the worker thread using Mach ports
  • Sending the check-in message using Mach ports

10.1 Configuring an NSMessagePort Object

  • Registering a message port

10.2 Configuring a Port-Based Input Source in Core Foundation

  • Attaching a Core Foundation message port to a new thread
  • Receiving the checkin message
  • Setting up the thread structures

XI、RunLoop 的底层实现

RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。

11.1 苹果官方将整个系统大致划分为上述4个层次:

  • 应用层包括用户能接触到的图形应用: Spotlight、Aqua、SpringBoard
  • 应用框架层即开发人员接触到的 Cocoa 等框架。
  • 核心框架层包括各种核心框架、OpenGL 等内容。
  • Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码在 opensource.apple.com

11.2 深入看一下 Darwin 这个核心的架构:

  • 在硬件层上面的三个组成部分:
    Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
    
  • XNU 内核的内环被称作 Mach,
    其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
    1) 在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。
    2) Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。
    3) "消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 `IPC (进程间通信)` 的核心。
    
  • BSD 层可以看作围绕 Mach 层的一个外环,
    其提供了诸如进程管理、文件系统和网络等功能。
    
  • IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。
  • Mach 的消息定义是在 <mach/message.h> 头文件的,很简单:

11.3 mach_msg()

  • RunLoop 的核心就是一个mach_msg()
    RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。
    1)例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 `mach_msg_trap()` 这个地方。
    

XII、苹果用 RunLoop 实现的功能

  • App 启动后 RunLoop 的状态:

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

12.1 AutoreleasePool

  • App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
  • 第一个 Observer 监视的事件是 Entry(即将进入Loop),
    1) 其回调内会调用 `_objc_autoreleasePoolPush()` 创建自动释放池。
    2) 其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
    
  • 第二个 Observer 监视了两个事件:
    1) BeforeWaiting(准备进入休眠) 时调用`_objc_autoreleasePoolPop()` 和 `_objc_autoreleasePoolPush()` 释放旧的池并创建新池;
    2) Exit(即将退出Loop) 时调用 `_objc_autoreleasePoolPop()` 来释放自动释放池。这个 Observer 的 order 是 2147483647(`2^31-1`
    ),优先级最低,保证其释放池子发生在其他所有回调之后。
    
  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。
    这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
    

12.2 事件响应

  • 苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()
  • 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。
    1) SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 `_UIApplicationHandleEventQueue()` 进行应用内部的分发。
    2) `_UIApplicationHandleEventQueue()` 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。
    3) 通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
    

12.3 手势识别

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

12.4 界面更新

  • 当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
  • 苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CALayer 以执行实际的绘制和调整,并更新 UI 界面。
  • 这个函数内部的调用栈大概是这样的:

12.5 定时器

  • NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。
    一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。
    1) RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
    2) 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。
    
  • CADisplayLink 是一个和屏幕刷新率一致的定时器
    如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop
    

12.6 GCD

  • 实际上 RunLoop 底层也会用到 GCD 的东西,
    虽然 NSTimer 是用了 XNU 内核的 mk_timer,我也仔细调试了一下,发现 NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
    
  • 当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。

12.7 PerformSelecter

  • 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。同样的调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,

12.8 网络请求

  • iOS 中,关于网络请求的接口自下至上有如下几层:
  • NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。
    当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。
    CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。
    

XIII、RunLoop 的实际应用举例

13.1 AFNetworking

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

13.2 AsyncDisplayKit

  • AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:
  • ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:
    即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
    
  • 具体的代码可以看这里_ASAsyncTransactionGroup

see also

转载请注明: > Objective-C:RunLoop