美文网首页
RunLoop 了解一下

RunLoop 了解一下

作者: Lin__Chuan | 来源:发表于2018-11-07 22:58 被阅读8次

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

相关文章

  • RunLoop 了解一下

    RunLoop , 运行循环, App 可以在程序运行过程中做一些事情. RunLoop 是什么? 为了说明, 我...

  • Runloop了解一下

    RunLoop是什么,有什么作用,如何获取? 定义RunLoop的实质是一个事件循环(do..while),用于保...

  • 当tableview/scrollview滚动时定时器NSTim

    需要了解的知识 要解决这个问题需要了解一下runloop的知识。runloop可以理解为cocoa下的一种消息循环...

  • 19 | 热点问题答疑(二):基础模块问题答疑

    首先,你可以看一下孙源的一个线下分享《RunLoop》,对 RunLoop 的整体有个了解。 然后,你可以再看官方...

  • 【OC梳理】RunLoop了解一下

    RunLoop的作用 使程序一直运行并接受用户输入 决定程序在何时应处理哪些Event 解耦主调方(发起调用)与被...

  • Runloop源码分析(1)——初探

    首先了解一下Runloop,Run是运行,Loop是循环。 默认情况下主线程的RunLoop原理:我们在启动一个i...

  • Runloop

    NSRunLoop简介 一. 什么是RunLoop 1、RunLoop 从字面上了解, RunLoop即是运行循环...

  • NSRunLoop简介

    NSRunLoop简介 一. 什么是RunLoop RunLoop从字面上了解, RunLoop即是运行循环, 就...

  • RunLoop总结

    什么是RunLoop Runloop视频 这个视频讲得很好,看完收获很大,对RunLoop有概念了,并且了解了一...

  • RunLoop深入理解(一)理论

    RunLoop 从看苹果文档,了解runloop就看到是这个图: 这个图只是说runloop利用内核mach的通信...

网友评论

      本文标题:RunLoop 了解一下

      本文链接:https://www.haomeiwen.com/subject/ocvpxqtx.html