RunLoop 了解一下

RunLoop , 运行循环, App 可以在程序运行过程中做一些事情.

RunLoop 是什么?

为了说明, 我们分别用 Xcode 创建两个项目, 一个是 Command Tool, 一个是Single View App, 众所周知, 运行 Command Tool 程序, 只会在控制台输出结果, 并且只是一次性的, 运行 App, 程序会借助 模拟器/真机 运行.

这两者最大的区别在于, 在 main.m 文件中

Command Tool
int main(int argc, char * argv[]) {
   return 0
}
App
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

App 之所以能在模拟器/真机中长期保持运行 状态, 而不会终止, 在于

UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))

原因:

  • UIApplicationMain() 内部会创建一个 runloop. 使得程序不会马上退出, 而是保持保持运行状态.
  • 这里面会处理App的各种事件(定时器事件, 用户交互事件等)

RunLoop对象

iOS中有两套API来访问和使用Runloop.

  1. Foundation: NSRunLoop
  2. Core Foundation: CFRunLoopRef
// viewDidLoad 这个方法是在主线程中调用的, 当前线程就是主线程
// 所以 mainRunLoop, currentRunLoop获得的 runloop 对象的地址是一样的.
NSLog(@"%p, %p", [NSRunLoop mainRunLoop], [NSRunLoop currentRunLoop]);
// 0x600003748600, 0x600003748600
    
NSLog(@"%p, %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());
// 0x600002f4c900, 0x600002f4c900

NSRunLoop 是基于 CFRunLoopRef 的一层OC包装, 官方开源了Core Foundation 的源码实现.

在源码中, 我们查看一下 CFRunLoopGetCurrent() 到底做了什么?



过程:

  1. 调用 _CFRunLoopGet0(), 并传入参数 当前线程.
  2. 其中 __CFRunLoops , 是存放以 pthread 为key, RunLoop 为 value 的字典.
  3. 如果从字典中未找到 Runloop对象, 则 调用 __CFRunLoopCreate 为这条线程创建新的RunLoop , 并存储到字典中.

由此我们知道了Runloop 和 线程 的关系

  • 每条线程都有与之对应的 RunLoop 对象.
  • 线程刚创建的时候是没有 Runloop 的, 程序在运行的过程中, 会为这条线程创建对应的 RunLoop 对象, RunLoop 随着线程结束而销毁
  • 线程runloop 分别以键值对的形式存储在字典中, 方便程序管理.

Core Foundation中关于RunLoop的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

这是 CFRunLoopRef 的实现, 图中摘取了几个比较在意的成员变量.


CFRunLoopModeRef 代表 RunLoop 的运行模式
常用到的有两种

  • kCFRunLoopDefaultMode (Mode的名字)
    App的默认Mode, 通常主线程是在这个Mode下运行的

  • UITrackingRunLoopMode (Mode的名字)
    界面追踪Mode, 用于ScrollView 追踪触摸滑动, 保证界面滑动时不受其他Mode影响

  1. RunLoop 启动时只能选择其中的一个 Mode, 作为 currentMode.
  2. 如果需要切换 Mode, 只能退出当前 Loop, 再重新选择一个 Mode 进入.
  3. 不同 Model 的 Source0/Source1/Timer/Observer 分隔开来, 互不影响.
  4. 如果 Mode 中没有任何 Source0/Source1/Timer/Observer, RunLoop会立马退出.
  • Source0: 触摸事件处理, performSelector: OnThread 等.
  • Source1: 基于 Port 的线程间通信, 处理系统事件捕捉等.
  • Timers: NSTimer操作, performSelector:withObject:afterDelay:等
  • Observers: 监听RunLoop的状态, UI刷新(BeforeWaiting), Autorelease pool(BeforeWaiting).
    当设置完view的背景色时, 这段代码不会立即生效, 而是等待 RunLoop 即将休眠的时候, 刷新界面
Mode Name Description
Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 默认模式是用于大多数操作的模式. 大多数情况下,您应该使用此模式启动运行循环并配置输入源.
Connection NSConnectionReplyMode (Cocoa) Cocoa将此模式与NSConnection对象结合使用以监视回复. 很少使用此模式.
Modal NSModalPanelRunLoopMode (Cocoa) Cocoa使用此模式来识别用于模态面板的事件.
Event tracking NSEventTrackingRunLoopMode (Cocoa) Cocoa使用此模式在鼠标拖动循环和其他种类的用户界面跟踪循环期间限制传入事件. (拖动scrollView)
Common modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 这是一组可配置的常用模式. 将输入源与此模式相关联也会将其与组中的每个模式相关联. 对于Cocoa应用程序, 此集合默认包括默认, 模态和事件跟踪模式. Core Foundation最初只包含默认模式. 您可以使用CFRunLoopAddCommonMode函数将自定义模式添加到集合中.

详解RunLoop

前面我们从源码层面了解RunLoop, 现在我们从整体再来看.


运行循环和各种源的概念结构

有几点我们需要注意:

    1. RunLoop 从两种不同类型的源接收事件
    • Input sources 提供异步事件, 通常来自另外一个线程的消息,
    • Timer sources 提供同步事件, 发生在预定时间, 或重复间隔.
    1. RunLoop Modes 是要监视的 Input sources 和 Timer sources 的集合,以及要通知的RunLoop observer的集合.
    • 每次运行 RunLoop 时, 都显示/隐式 指定特定的运行模式.
    • 模式是根据事件的来源而不是事件的类型进行区分的, 比如不会使用模式仅匹配鼠标按下事件或仅匹配键盘事件.
    1. Input sources 中通常有两类.
    • 基于端口的 Input source 监视应用程序的 Mach 端口, 它是由内核自动发出信号.
    • 自定义 Input source 处理自定义事件源, 它必须由另一个线程手动发信号给自定义源.
  • Cocoa 还定义了一个自定义输入源, Cocoa Perform Selector Sources, 它允许我们在任何线程上执行选择器, 并且执行其选择器后将其自身从 RunLoop 中移除.
    1. runUtilDate: 是 NSRunLoop 类的对象方法, 用来运行 RunLoop.
    1. handlePort:, customSrc:, mySelector:, timeFired 是来自不同的源的事件(消息).
  • Timer sources 在将来的预设时间将事件同步传递给你的线程。定时器是线程通知自己做某事的一种方式.

补充说明: Loop Observer
与在发生适当的异步或同步事件时触发的源不同,RunLoop observer 在执行 RunLoop 期间, 在特殊位置触发.


RunLoop的多种状态:

  • kCFRunLoopEntry: 即将进入 RunLoop
  • kCFRunLoopBeforeTimers: 即将处理 Timer
  • kCFRunLoopBeforeSources: 即将处理 Sources
  • kCFRunLoopBeforeWaiting: RunLoop 即将休眠
  • kCFRunLoopAfterWaiting: RunLoop 即将唤醒
  • kCFRunLoopExit: 即将退出RunLoop

RunLoop的事件处理

每次运行 RunLoop 时, 线程的RunLoop都会处理挂起的事件, 并且为任何附加的观察者生成通知. (App一启动, 会自动在主线程设置并运行RunLoop, 称之为 主循环)

  1. Notify observers: 进入运行循环.
  2. Notify observers: 即将处理 Timer.
  3. Notify observers: 即将处理Sources
  4. 处理Source0: 触发任何准备触发的基于非端口的输入源, 跳到第 9 步:
  5. 处理Source1: (如果基于端口的输入源准备就绪并等待触发), 就跳到第 9 步:
  6. Notify observers: 线程即将休眠(等待消息唤醒)
  7. Notify observers: 线程结束休眠(被下面的消息唤醒)
    • 处理Timer
    • 处理Source1: 事件到达基于端口的输入源
    • RunLoop 被明确唤醒
    • 为 RunLoop 设置的超时值到期
  8. Notify observers: 线程刚刚醒来.
  9. 处理 Blocks:
    • 如果输入源被触发,则传递事件.
    • 如果触发了用户定义的计时器,则处理计时器事件并重新RunLoop。转到第2步.
    • 如果运行循环被明确唤醒但尚未超时,请重新RunLoop, 转到第2步
  10. Notify observers: RunLoop 已退出

使用 RunLoop

我们需要显示运行 RunLoop 的唯一时机是为应用程序创建辅助线程, 对于辅助线程, 如果确定需要运行循环, 那么需要配置并运行它.

  • 在线程上使用 Timer.
  • 保持线程以执行定期任务(线程保活).
  • 使用端口或自定义输入源与其他线程通信.

1. 解决NSTimer在滑动时停止工作的问题

NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"==>%d",_count++);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

原因:
RunLoop 处理事件的默认 Mode 是 Default, 当 app 同时有计时器事件和scrollView滚动事件时, 优先处理 scrollView 滚动事件(Event tracking Mode), 处理完才会再来处理计时器事件.
解决办法:
计时器事件Common Mode 绑定, RunLoop 内部会自动切换 Tracking Mode 和 Default Mode, 来处理计时器事件 和 scrollView 滚动事件, 使得两者看似同时在工作.

2. 线程保活
LCThread 类是一个继承自 NSThread 的类, 在里面我们实现了 dealloc 方法, 为了监测线程是否被销毁的情况.

self.thread = [[LCThread alloc] initWithBlock:^{
        // 一直在运行. 线程保活
        NSLog(@"----begin----%s", __func__);

        // 当前runloop开始睡眠, 当前线程被阻塞了       
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
        NSLog(@"----end----%s", __func__);
}];
// 启动此线程
[self.thread start];

保证线程不立刻被销毁, 我们在此期间制定任务
比如: 点击屏幕. 打印此线程

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{    
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}

-(void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

手动释放线程

- (void)stopThread{
    [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)stop
{    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 清空线程
    self.thread = nil;
}

3. 监控界面卡顿
通过 RunLoop observer 来监控目标 RunLoop 的状态, 如果频繁出现 kCFRunLoopBeforeSources, kCFRunLoopAfterWaiting, 检测出现次数, timeCount, 超过指定次数可认为App卡顿 .
因为这两个状态是要去处理事件的状态.

参考
Apple官方文档-RunLoop

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

推荐阅读更多精彩内容