iOS RunLoop(三)应用与实践

status
category
date
summary
slug
icon
tags
password
前两篇介绍了 RunLoop 的相关概念以及从创建到运行的具体逻辑,本篇主要介绍系统使用 RunLoop 的实例场景,以及日常开发中自己使用的注意事项。
  • iOS RunLoop(一)概念与构成
  • iOS RunLoop(二)生命周期与运行逻辑
  • iOS RunLoop(三)应用与实践
版本:CF-1153.18
为了学习 RunLoop 的基础知识,本系列在截取源码时省略了很多部分,例如结构体定义、锁的操作、指针的释放等等,省略的代码也非常重要,但是比较影响阅读速度,暂时忽略也并不影响学习整体概念。

RunLoop 的调用栈

前面简单介绍了事件驱动系统,整个 iOS 应用实际上运行在一个庞大的循环体中,既然如此,所有方法的执行和调用应该都会和 RunLoop 相关。在主线程的任何一个断点,都可以在堆栈信息中看到相关的回调,这些回调一般是以下几个:

AutoReleasePool

iOS App 启动后,通过打印主线程的 RunLoop 可以发现,主线程的 RunLoop 中自动注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler(),在第一篇中我们还介绍了 Observer 监听事件的类型和优先级:
第一个 Observer:activities = 0x1, order = -2147483647,它监听的事件是即将进入 RunLoop,优先级最高,保证 AutoReleasePool 的创建和重置发生在其他所有回调之前。
第二个 Observer:activities = 0xa0, order = 2147483647,0xa0 转换成二进制即 1 << 5 && 1 << 7,因此它监听的事件是即将进入休眠和即将退出 RunLoop,优先级最低,保证 AutoReleasePool 的释放发生在其他所有回调之后。
由于在 main 函数中创建了 AutoReleasePool,我们编写的代码尤其是事件回调、计时器回调等等,都会被 RunLoop 创建好的 AutoReleasePool 环绕,保证了 RunLoop 进入、休眠、退出时管理对象的内存状态,所以不会出现内存泄漏。

事件响应

关于事件生成 UIEvent 之前,有两个概念需要了解:
  • IOHIDFamily 是一个内核扩展,相当于系统的 I/O Kit,是硬件和用户界面设备的桥梁。
  • SpringBoard 相当于 iOS 的 launcher,负责管理主屏幕,也负责接受由 IOKit.framework 产生并发送的 IOHIDEvent 事件。SpringBoard 只能接受四种事件:按键/锁屏/摇晃/触摸,这也符合 UIEvent 的类型。
当一个事件产生后,IOKit 产生事件并发送给 SpringBoard,然后 SpringBoard 负责发送给对应的 App 进程。苹果在 RunLoop 中注册了一个基于 mach port 的 Source1 用来接受系统事件,其回调为 __IOHIDEventSystemClientQueueCallback()。这个回调将事件包装成 UIEvent,再发送给 UIWindow 等等,手势事件的处理都是在这个回调中完成的。

手势识别

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

界面更新

当操作 UI 或者手动调用了 setNeedsLayout 等方法后,UIView/CALayer 就被标记为待处理,并被添加到一个全局的容器中去。苹果在 RunLoop 中注册了一个 Observer 监听即将进入休眠和即将退出,回调为 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),它会遍历所有待处理的 UIView/CALayer,执行相应的调整或绘制,然后更新 UI 界面。
所以如果主线程 RunLoop 的某一次循环中执行了大量任务,造成这个回调的执行被推迟,亦或者这个回调本身的绘制任务就非常耗时,那么这一次 RunLoop 循环就不能预期结束,如果时间超过了屏幕刷新率允许的最大时间限度(60 Hz 屏幕的最大限度为 1/60 秒),就会造成界面卡顿。

定时器

NSTimer 就是对 CFRunLoopTimerRef 的封装,一个 NSTimer 注册到 RunLoop 之后,RunLoop 会计算好它触发的时间点,并为其注册好事件。到了时间点后,RunLoop 就被唤醒去执行定时器的回调。
Timer 有个属性是 tolerance,表示允许的最大时间误差。例如执行了很耗时的任务,导致某个时间点被错过,如果超时的时间没有超过 tolerance,那么也会执行定时器任务。同时有了 tolerance,RunLoop 的唤醒频率可以稍微降低,以节省资源。
由于 Timer 是被 RunLoopMode 管理,所以切换 Mode 时要注意 Timer 的失效问题,或者直接标记为 Common。

常驻线程

应用中的耗时操作应该放到子线程中运行,但子线程中的任务执行完毕后,子线程就会被销毁,因此有些场景要求我们为子线程开启 RunLoop 让它成为常驻的后台子线程。前面的文章提到,RunLoop 在 Source/Timer/Observer 为空或者时间到期就会退出,线程也会跟着销毁。为了阻止 RunLoop 退出,需要添加一个假资源,于是出现了许多模仿旧版本 AFNetWorking 的做法:
值得注意的是,这里 port 的作用是让 RunLoop 成功启动,而不是永远不退出。RunLoop 启动时如果没有 Source/Timer/Observer 会立即退出,所以这里添加了一个 port 也就是 source。通常情况下需要外部通过 port 发送消息到 RunLoop 内,但是这里只是为了让 RunLoop 不退出。
真正实现线程常驻的是 RunLoop 自己的 run 方法,NSRunLoop 封装了三个启动方法:
第一个没有指定结束时间,第二个指定了结束时间,但前两个方法实际上都是调用了第三个方法启动 RunLoop。

PerformSelector

RunLoop 会在没有 source 和 timer 时自动退出:
最终控制台会打印111222,而不会打印333。performSelector afterDelay 会注册一个 timer 到当前线程的 RunLoop,紧接着创建并启动 RunLoop。输出111的任务执行完之后,RunLoop 即使被设定了很长的运行时间,仍然会因为没有 source 则自动退出,因此222也可以很快输出。
Loading...

© 刘口子 2018-2025