RunLoop 了解一下

2018-11-07  本文已影响8人  Lin__Chuan

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]))

原因:

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 和 线程 的关系

Core Foundation中关于RunLoop的5个类

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


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

  1. RunLoop 启动时只能选择其中的一个 Mode, 作为 currentMode.
  2. 如果需要切换 Mode, 只能退出当前 Loop, 再重新选择一个 Mode 进入.
  3. 不同 Model 的 Source0/Source1/Timer/Observer 分隔开来, 互不影响.
  4. 如果 Mode 中没有任何 Source0/Source1/Timer/Observer, 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, 现在我们从整体再来看.


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

有几点我们需要注意:

  • Cocoa 还定义了一个自定义输入源, Cocoa Perform Selector Sources, 它允许我们在任何线程上执行选择器, 并且执行其选择器后将其自身从 RunLoop 中移除.

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


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 的唯一时机是为应用程序创建辅助线程, 对于辅助线程, 如果确定需要运行循环, 那么需要配置并运行它.

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

上一篇 下一篇

猜你喜欢

热点阅读