RunLoop深入理解(一)理论

2015-12-26  本文已影响182人  王道钦

RunLoop

从看苹果文档,了解runloop就看到是这个图:

runloop接受输入源显示图

这个图只是说runloop利用内核mach的通信示意图 ,其只是告诉我们runloop接受消息,但是消息到底是什么(mach port消息体),处理后怎么发哪尼,更不涉及runLoop内部的东西,所以不要被此图迷惑了。此貌似只能初步直观的告诉我们runloop是一个循环。

既然说到这里,我们在这脑补点这方面的东西,传进出runLoop处理的完整过程。

首先我们先看IOS/OSX系统的系统架构

系统架构图

划分为4个层次:

1.应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。

2.应用框架层即开发人员接触到的 Cocoa 等框架。

3.核心框架层包括各种核心框架、OpenGL 等内容。

4.Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在opensource.apple.com里找到。

在Darwin这个核心的架构:

硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核

BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。

Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

mach发送和接受消息是通过同一个 mach_msg() 进行的。

说这么多mach是什么意思尼?

RunLoop 的核心就是一个 mach_msg() (文章尾部的runloop源代码,来说明此点),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。(至于Port消息结构体什么样,请查找源代码)

俯瞰后runloop后,咱们平视runloop内部:

在 CoreFoundation 里面关于 RunLoop 有5个类:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

这里特别说下CFRunLoopModeRef类 这个类并没有对外暴漏,在runloop源代码里,通过 CFRunLoopRef 的接口进行了封装,如图:

runloop封装CFRunLoopModeRef

一个RunLoop包含有N个Mode 每个Mode又包含N个Source/Timer/Observer(比如你在主线程runloop可以建立无数个timer,observer,以及系统自己无数输入源点)。

每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。(这句话对实战可是有意义的,当scroolview竖直滑动过程,如和保证其上有timer横行滚动焦点图尼?)

至于CFRunLoopSourceRef,CFRunLoopTimerRef,CFRunLoopObserverRef,这个不用多说了吧。

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

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

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

RunLoop的内部逻辑

这张图可比第一张图可能才是我们深入runloop的第一步,咱们分析下

1.App 启动后 RunLoop 的状态:

系统默认注册了5个Mode:

1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。

2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。

5: kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

你可以在这里看到更多的苹果内部的 Mode,但那些 Mode 在开发中就很难遇到了。

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

1.static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();//observe

2.static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();//block

3.static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();//main_dispatch

4.static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();//timer

5.static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();//source0事件

6.static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();//source1事件

1.Observer事件,runloop中状态变化时进行通知。(微信卡顿监控就是利用这个事件通知来记录下最近一次main runloop活动时间,在另一个check线程中用定时器检测当前时间距离最后一次活动时间过久来判断在主线程中的处理逻辑耗时和卡主线程)。这里还需要特别注意,CAAnimation是由RunloopObserver触发回调来重绘,接下来会讲到。

2.Block事件,非延迟的NSObject PerformSelector立即调用,dispatch_after立即调用,block回调。

3.Main_Dispatch_Queue事件:GCD中dispatch到main queue的block会被dispatch到main loop执行。

4.Timer事件:延迟的NSObject PerformSelector,延迟的dispatch_after,timer事件。

5.Source0事件:处理如UIEvent,CFSocket这类事件。需要手动触发。触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。

6.Source1事件:处理系统内核的mach_msg事件。(推测CADisplayLink也是这里触发)。

关于以前的框架 你会发现main函数里有@autoreleasePool{}

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

综合多篇文章整理所有

参考:深入理解RunLoop

苹果runLoop文档

苹果文档中文翻译版懒人版

上一篇下一篇

猜你喜欢

热点阅读