iOSiOS性能调优iOS积累用之

iOS 解开 RunLoop 神秘面纱之谜

2016-11-13  本文已影响2544人  LaiYoung_
神秘面纱之谜
原谅我这次做了一个标题党。</br>
我相信在很多做 iOS 开发不久的同学都或多或少知道在 cocoa Touch 框架里面有 RunLoop 这个东西。但是不知道它在实际开发中如何具体体现。我在学习 RunLoop 之前也是只知道有这个东西,具体如何运用在实际开发中我也不知道,所以就趁周末学习下神秘的 RunLoop,尽管在网上已经有很多关于 RunLoop 的文章了(在文末我会贴出相关文章),而且写得都不错,不过,我还是将自己的发表出来吧,记录一下自己曾经学习了这个神秘的东西。</br>
建议先看倒数第二点RunLoop 的实际应用再倒回来看理论知识点,这样更利于知识点的吸收!

RunLoop 的作用

  1. 保持程序的持续运行
  2. 处理 App 中的各种事情,Touch Event、 NSTimer、Selector 事件
  3. 节省 CPU 资源,提高程序性能,有事做的时候做事儿,没事儿做的时候休息。

RunLoop 对象

iOS 中有 2 套API 来访问和使用 RunLoop

  1. Foundation-> NSRunLoop
  2. Core Foundation -> CFRunLoopRef

NSRunLoop 和 CFRunLoopRef 都代表着 RunLoop 对象
NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装

RunLoop 与线程

  1. 每个线程都对应一个 RunLoop 对象,在主线程默认开启,子线程需要自己手动开启
  2. RunLoop 在第一次获取时创建,在线程结束时销毁

获取RunLoop对象

Foundation
  1. [NSRunLoop currentRunLoop];获取当前线程的 RunLoop 对象
  2. [NSRunLoop mainRunLoop];获取主线程 RunLoop 对象
Core Foundation
  1. CFRunLoopGetCurrent();获取当前线程的 RunLoop 对象
  2. CFRunLoopGetMain();获取主线程的RunLoop对象

RunLoop 相关类

Core Foundation 中相关 RunLoop 的 5 个类
  1. CFRunLoopRef
  2. CFRunLoopModeRef
  3. CFRunLoopSourceRef
  4. CFRunLoopTimerRef
  5. CFRunLoopObserverRef

CFRunLoopModeRef

CFRunLoopModeRef 代表 RunLoop 的运行模式
  1. 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。


    图片来至_ibireme Blog
  2. 每次 RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。
  3. 如果需要切换 Mode,只能退出 RunLoop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响
系统默认注册了5个 Mode:
  1. KCFRunLoopDefaultMode,App 的默认 Mode,通常主线程在这个 Mode 下运行。
  2. UITrackingRunLoopMode,界面跟着 Mode,用于 ScrollView 追踪触摸滑动,保证节目滑动时不受其他 Mode 影响。
  3. UIInitializationRunLoopMode,在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
  4. GSEventReceiveRunLoopMode,接受系统事件的内部 Mode,通常用不到。
  5. KCFRunLoopCommonModes,这是一个占位用的 Mode,不是一种真正的 Mode。

CFRunLoopSourceRef

  1. Source0:处理 App 内部事件、App 自己负责管理(触发),如 UIEvent,CFSocket,Cocoa Perform Selector Source
  2. 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 Blog

RunLoop 的实际应用

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,指在 NSDefaultRunLoopModeUITrackingRunLoopMode 下,类似是这两个 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 的各个状态来做一些非常规操作。

经典面试题:

  1. Q:AutoreleasePool 在什么时候执行?A: UIKit 通过 RunLoopObserver 在 RunLoop 两次 sleep 间对 AutoreleasePool 进行 Pop 和 Push,将这次 RunLoop 中产生的Autorelease 对象进行释放。

相关文章&视频

深入理解RunLoop
走进Run Loop的世界 (一):什么是Run Loop?
iOS线下分享《RunLoop》by 孙源@sunnyxx

上一篇下一篇

猜你喜欢

热点阅读