iOS 解开 RunLoop 神秘面纱之谜

神秘面纱之谜

原谅我这次做了一个标题党。</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

  • Source 是 RunLoop 的数据源抽象类(protocol)
  • RunLoop 定义了两个 Version 的Source</br>
  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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容

  • Run loop 剖析:Runloop 接收的输入事件来自两种不同的源:输入源(intput source)和定时...
    Mitchell阅读 12,429评论 17 111
  • 先贴下 apple doc, 本文基本是对照 doc 的翻译:https://developer.apple.co...
    brownfeng阅读 6,857评论 8 111
  • 一、什么是runloop 字面意思是“消息循环、运行循环”。它不是线程,但它和线程息息相关。一般来讲,一个线程一次...
    WeiHing阅读 8,118评论 11 111
  • 基本概念 进程 进程是指在系统中正在运行的一个应用程序,而且每个进程之间是独立的,它们都运行在其专用且受保护的内存...
    小枫123阅读 891评论 0 1
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    SOI阅读 21,798评论 3 63