浅谈 iOS 中的 RunLoop

1)作用

  • 让线程能随时处理事件但并不退出
    平常:一个线程一次只能执行一个任务,执行完成后线程就会退出
  • 处理 APP 中的各种事件「触摸、定时器、Selector时间」
  • 节省 CPU 资源,提高性能:让CPU该做事时做事,该休息时休息
  • main 函数启动了 RunLoop「UIApplicationMain 函数里启动,一直没有返回」 程序不会马上退出,保持持续运行状态

2)RunLoop 对象

iOS 有两套 API 访问和使用 RunLoop

  • Foundation
    类:NSRunLoop「基于 CFRunLoopRef 的一层 OC包装」
  • Core Foundation
    类:CFRunLoopRef
  • 桥接 __bridge
    Foundation 框架 和 Core Foundation框架类型的转换需要桥接
    • F类型 → CF类型:CFStringRef CFDataType = (__bridge NSString*)FDataType
    • CF类型 → F类型:NSString *FDataType = (__bridge CFStringRef)CFDataType

3)RunLoop 和 线程

  • 每一条线程都有 唯一 一个与之对应的 RunLoop 对象
  • 主线程的 RunLoop自动创建好了,子线的 RunLoop 程需要自己创建
  • RunLoop 在第一次获取时创建,在线程结束时销毁
    线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直不会创建 RunLoop

4)RunLoop 相关类

I. CFRunLoopModeRef「RunLoop的运行模式」

  • 一个 RunLoop 包含多个 Mode,每个 Mode 里有多个 Source「Set 存储」、Timer、Observer「Array 存储」
    如果RunLoop 所有的 Mode 里没有 Source、Timer,RunLoop 会退出「有Observer没用」

  • 每次启动 RunLoop,只能指定一种 Mode,这个 Mode 被称作 CurrentMode

  • 要切换 Mode 只能退出 Loop,在重新指定一个 Mode 进入
    这是为了分隔开不同组的 Source、Timer、Observer,让其互不干扰

系统默认注册了 5 个Mode

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  1. kCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode
  2. UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
  3. GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到

II. CFRunLoopTimerRef「基于时间的触发器,基本上就是 NSTimer」

1.已经自动添加到 RunLoop 中,默认模式是 kCFRunLoopDefaultMode

// 由于 CFRunLoopTimerRef 和 NSTimer 可以混用,这里使用 NSTimer
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 以下 2 中的代码等价于上面的代码

2.修改模式

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// case1:定时器只运行在 NSDefaultRunLoopMode 下,一旦RunLoop进入其他模式,这个定时器就不会工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
// case2:定时器会跑在标记为 common modes 的模式下
// 标记为common modes的模式:UITrackingRunLoopMode 和 kCFRunLoopDefaultMode
[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

III. CFRunLoopSourceRef「事件源,输入源」

按照官方文档分类:

  • Port-Based Sources 基于端口,和其他线程交互内核消息
  • Custom Input Sources 自定义
  • Cocoa Perform Selector Sources 用于处理 performSelector 函数

按照函数调用栈分类:

  • Source0:非基于 Port,不能主动触发事件,接收 Source1 分发的事件
  • Source1:基于 Port,能主动触发事件,通过内核和其他线程通讯、接收、分发系统事件

IV. CFRunLoopObserverRef「观察者,监听 RunLoop的状态改变」

可以监听的时间点:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入 Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出 Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

CF「CoreFundation」的内存管理

  1. 凡事带有 Create、Copy、Retain等字眼的函数,创建出来的对象,最后都要做一次 release
  2. release函数:CFRelease(要释放的对象);
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});

// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
// CF开头的函数不受 ARC控制,带 Create的 要释放
// 释放Observer
CFRelease(observer);

5)RunLoop 处理事件的步骤

每次运行 RunLoop,线程的RunLoop对自动处理之前未处理的消息,并通知相关的观察者。

I. 步骤如下

  1. 看 Mode是否为空,若不空,通知 Observer RunLoop 已经启动<b>(之后创建一个自动释放池)</b>

  2. 通知 Observer「观察者」

    • 即将开始的 定时器「Timer」
    • 即将启动的 非基于端口的源「Source0」
  3. 启动准备好的任何 非基于端口的源「Source0」

  4. 如果 基于端口的源「Source1」 准备好并处于等待状态,立即启动 → 步骤 8

  5. 通知 Observer线程 → 休眠 <b>(休眠前会 销毁自动释放池,然后在创建自动释放池)</b>

  6. 以下任意事件 可以唤醒 已经休眠的程序

    • 基于端口的源「Source1」 接收到事件
    • 定时器启动
    • RunLoop 设置的循环时间超时
    • RunLoop 被唤醒
  7. 通知 Observer线程 → 唤醒

  8. 处理 未处理的 事件

    • 定义的定时器启动,处理定时器事件,重启RunLoop → 步骤2
    • 输入源/时间源 启动,传递信息
    • RunLoop 被显式唤醒 且 时间没超过RunLoop固定循环的时间,重启RunLoop → 步骤2
  9. 通知 Observer RunLoop 结束<b>(销毁自动释放池)</b>

II. 步骤图例

Paste_Image.png

III. 自动释放池什么时候释放?

通过 Observer监听 RunLoop的状态,一旦监听到RunLoop即将进入睡眠等待状态「kCFRunLoopBeforeWaiting」就释放自动释放池

6)RunLoop 应用

I. 某些事件「行为、任务」在特定模式下执行

大图渲染耗时,这时候多线程多任务可能会造成卡顿,下载完后不急于显示,而是等其他线程不忙时显示

// 只在 NSDefaultRunLoopMode 主线程模式下显示图片,一旦RunLoop进入其他模式,这个函数不会执行
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

II. 常驻线程

适用于:经常要做后台操作,频繁开启线程的情况,让一个线程常驻,可以避免频繁的开启使用线程的麻烦。等待其他线程发消息,处理事件

  • 在子线程中开启一个定时器
  • 在子线程中进行行为的长期监控
@autoreleasepool{
    // 方法一
    // Mode 里没有 任何东西的 RunLoop 会马上退出,这里随便加点东西,防止退出
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    // 开启 RunLoop 线程
    [[NSRunLoop currentRunLoop] run];
}

// 方法二、不推荐
while(flag){ [[NSRunLoop currentRunLoop] run]; }

III. 添加 Observer监听 RunLoop的状态

比如,监听点击事件的处理,在所有点击事件之前做一些处理

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

推荐阅读更多精彩内容

  • 一、什么是runloop 字面意思是“消息循环、运行循环”。它不是线程,但它和线程息息相关。一般来讲,一个线程一次...
    WeiHing阅读 8,107评论 11 111
  • 前言 最近离职了,可以尽情熬夜写点总结,不用担心第二天上班爽并蛋疼着,这篇的主角 RunLoop 一座大山,涵盖的...
    zerocc2014阅读 12,367评论 13 67
  • Runloop是iOS和OSX开发中非常基础的一个概念,从概念开始学习。 RunLoop的概念 -般说,一个线程一...
    小猫仔阅读 979评论 0 1
  • Run loop 剖析:Runloop 接收的输入事件来自两种不同的源:输入源(intput source)和定时...
    Mitchell阅读 12,407评论 17 111
  • 现在的手机有什么用,一般都不打电话了,只用QQ和微信就能聊天,而且是和不认识的人聊! 为什么不和熟人聊天呢?因为都...
    一只羊1237阅读 185评论 0 0