iOS 解开 RunLoop 神秘面纱之谜
原谅我这次做了一个标题党。</br>
我相信在很多做 iOS 开发不久的同学都或多或少知道在
cocoa Touch
框架里面有 RunLoop 这个东西。但是不知道它在实际开发中如何具体体现。我在学习 RunLoop 之前也是只知道有这个东西,具体如何运用在实际开发中我也不知道,所以就趁周末学习下神秘的 RunLoop,尽管在网上已经有很多关于 RunLoop 的文章了(在文末我会贴出相关文章),而且写得都不错,不过,我还是将自己的发表出来吧,记录一下自己曾经学习了这个神秘的东西。</br>建议先看倒数第二点
RunLoop 的实际应用
再倒回来看理论知识点,这样更利于知识点的吸收!
RunLoop 的作用
- 保持程序的持续运行
- 处理 App 中的各种事情,Touch Event、 NSTimer、Selector 事件
- 节省 CPU 资源,提高程序性能,有事做的时候做事儿,没事儿做的时候休息。
RunLoop 对象
iOS 中有 2 套API 来访问和使用 RunLoop
- Foundation-> NSRunLoop
- Core Foundation -> CFRunLoopRef
NSRunLoop 和 CFRunLoopRef 都代表着 RunLoop 对象
NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装
RunLoop 与线程
- 每个线程都对应一个 RunLoop 对象,在主线程默认开启,子线程需要自己手动开启
- RunLoop 在第一次获取时创建,在线程结束时销毁
获取RunLoop对象
Foundation
-
[NSRunLoop currentRunLoop];
获取当前线程的 RunLoop 对象 -
[NSRunLoop mainRunLoop];
获取主线程 RunLoop 对象
Core Foundation
-
CFRunLoopGetCurrent();
获取当前线程的 RunLoop 对象 -
CFRunLoopGetMain();
获取主线程的RunLoop对象
RunLoop 相关类
Core Foundation 中相关 RunLoop 的 5 个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
CFRunLoopModeRef
CFRunLoopModeRef 代表 RunLoop 的运行模式
-
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
图片来至_ibireme Blog - 每次 RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。
- 如果需要切换 Mode,只能退出 RunLoop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响
系统默认注册了5个 Mode:
-
KCFRunLoopDefaultMode
,App 的默认 Mode,通常主线程在这个 Mode 下运行。 -
UITrackingRunLoopMode
,界面跟着 Mode,用于 ScrollView 追踪触摸滑动,保证节目滑动时不受其他 Mode 影响。 -
UIInitializationRunLoopMode
,在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。 -
GSEventReceiveRunLoopMode
,接受系统事件的内部 Mode,通常用不到。 -
KCFRunLoopCommonModes
,这是一个占位用的 Mode,不是一种真正的 Mode。
CFRunLoopSourceRef
- Source 是 RunLoop 的数据源抽象类(protocol)
- RunLoop 定义了两个 Version 的Source</br>
- Source0:处理 App 内部事件、App 自己负责管理(触发),如 UIEvent,CFSocket,Cocoa Perform Selector Source
- Source1:由 RunLoop 和内核管理,Mach port 驱动,如 CFMachPort、CFMessagePort
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于时间的触发器,基本上说的就是 NSTimer。
CFRunLoopObserverRef
CFRunLoopObserverRef 是观察者,能够监听 RunLoop 的状态变化。
可以监听的时间点有以下几个。
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),//即将退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU//监听 RunLoop 的所有情况
};
RunLoop 处理逻辑
官方版 图片来至_ibireme BlogRunLoop 的实际应用
1. NSTimer
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(testTimer) userInfo:nil repeats:YES];
这种情况下的 NSTimer 是默认加在 NSDefaultRunLoopMode
下的。假如此时有一个继承自 ScrollView
的控件,例如 tableView,正在滑动它,那么你的 NSTimer 就会停止工作,因为此时从 NSDefaultRunLoopMode
切换到了 UITrackingRunLoopMode
。</br>
此时停止滑动 tableView,NSTimer 又开始工作,因为又从 UITrackingRunLoopMode
切换到了 NSDefaultRunLoopMode
。
解决这个问题只需要在开启 NSTimer 的时候设置好 RunLoop 的 Mode 设置为 NSRunLoopCommonModes
即可。如下:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(testTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSRunLoopCommonModes 这个 Mode 是一个不确定的 Mode,指在 NSDefaultRunLoopMode
和 UITrackingRunLoopMode
下,类似是这两个 Mode 的集合,都会执行执行这个 NSTimer。
还有一种场景就是在子线程去开启一个 NSTimer,你说这个 timer 会不会执行?我说这个 timer 不会执行,因为 timer 的执行是依靠与 RunLoop 的,子线程都没有开启 RunLoop,所以也就不会执行。解决这个问题只需要开启 RunLoop 即可,在后面第5点 线程保活/常驻线程
会讲怎么开启一个子线程的 RunLoop。
2. imageView 的显示
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1KR2C08-9"] afterDelay:2.0f];
这个同上面的 NSTimer
类似,Mode 默认为 NSDefaultRunLoopMode
, 在滑动一个 ScrollView 控件的时候,那么这个 image 对象就不会显示在 imageView 控件上。</br>要想在滑动 ScrollView 控件的时候,也能让 image 对象显示在 imageView 控件上,使用另外一个带有 Modes 的 performSelector 即可,如下:
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1KR2C08-9"] afterDelay:2.0 inModes:@[NSRunLoopCommonModes]];
3. 显示大图
这是我在看叶孤城_在斗鱼直播讲 RunLoop 时候知道的,场景是这样的,有一个 tableView,每个 cell 都显示了三张图,一屏大概能显示18张图,每张图的大小是2034 × 1525 pixels
,如果在一次 RunLoop 的时候绘制 18 张图到 cell 上面会其卡无比。如果用 CFRunLoopObserverRef
来监听 RunLoop 在 kCFRunLoopEntry
状态的时候就代表进入了一个新的 RunLoop,那么在这时候进行一次绘制,就解决了卡顿问题。但是我看源代码是在kCFRunLoopBeforeWaiting
状态进行绘制的。Demo_RunLoopWorkDistribution,视频地址 密码:ennf
4. 监测 iOS 卡顿
这个也是在叶孤城_在斗鱼直播中看到的,核心还是使用 CFRunLoopObserverRef
来监听两次 RunLoop 之间的时差,如果在一个新的 RunLoop 开启的时候,这两个之间的时差超过你设置的某个值就表示有卡顿了。然后将这次 RunLoop 执行的所有方法打印出来,差不多就可以定位在某个函数执行的时候发生了卡顿。Demo_PerformanceMonitor
5. 线程保活/常驻线程
都知道在子线程执行完毕任务之后,这个线程就 dealloc 了。不信可以自己创建一个 class 继承至 NSThread
实现 dealloc
函数来看看是否会在线程执行完毕任务之后就会 dealloc,因为在子线程,RunLoop 默认是没有开启的,所以执行完任务之后就会 dealloc 。
有时候我们需要让这个线程再继续执行其他任务应该怎么做呢? 我们已经知道了子线程为什么会在执行完任务之后就会 dealloc,那么我们就从根本开始,开启其 RunLoop,将下面两行代码写在子线程函数里面。
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
再使用 performSelector 在开启 RunLoop 的线程执行其任务。如果你只写了[[NSRunLoop currentRunLoop] run];
而没有为 RunLoop 添加其 Mode,或者说添加了 Mode,Mode 没有值(source/NSTimer/observer),那么就没有开启 RunLoop。
[self performSelector:@selector(testAgainThread) onThread:_thread withObject:nil waitUntilDone:YES];
假如这个子线程没有开启 RunLoop,而waitUntilDone 为 YES
,那么在 perforSelector
处就会出现一个类似死循环的东西,因为这个线程已经销毁了,而 waitUntilDone 为 YES(也就是需要执行完这个线程之后,才能执行之后的操作),然而现在无法执行这个线程,所以也就会死在这里。
6. AutoreleasePool
在 ibireme 的文章中说,在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
在EFObjc52
中,说到主线程或是“大中抠派发”(GCD)机制中的线程,这些线程都哦默认都有自动释放池,每次执行“事件循环”(Event Loop)时,就会将其清空。</br>
RunLoop 在其进入之前先创建一个自动释放池,在休眠的时候清空自动释放池,在唤醒之前会再创建一个自动释放池。在 RunLoop 退出的时候会销毁自动释放池。
总结:
1、子线程需要手动开启 RunLoop
2、RunLoop 离不开 Mode,Mode 离不开 Source/Timer/Observer。
3、可以利用 Observer 监听 RunLoop 的各个状态来做一些非常规操作。
经典面试题:
- Q:AutoreleasePool 在什么时候执行?A: UIKit 通过 RunLoopObserver 在 RunLoop 两次 sleep 间对 AutoreleasePool 进行 Pop 和 Push,将这次 RunLoop 中产生的Autorelease 对象进行释放。
相关文章&视频
深入理解RunLoop
走进Run Loop的世界 (一):什么是Run Loop?
iOS线下分享《RunLoop》by 孙源@sunnyxx