iOS Runtime学习iOS底层技术总结runloop

RunLoop - 知识点总结

2016-07-26  本文已影响446人  devZhang

RunLoop 初窥:

基本作用

  1. 保持程序的持续运行
  2. 处理 APP 中的各种事件(触摸事件, 定时器事件, Selector 事件等)
  3. 节省 CPU 资源, 提高程序性能, 该做事时做事, 该休息时休息

在程序中的体现

RunLoop 对象

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

NSRunLoop 和 CFRunLoopRef 都代表 RunLoop 对象, NSRunLoop是在基于CFRunLoopRef的一层 OC 封装

RunLoop 与线程

获得 RunLoop 对象

Foundation

    [NSRunLoop currentRunLoop];     // 获得当前线程的 RunLoop 对象
    [NSRunLoop mainRunLoop];     // 获得主线程的 RunLoop 对象

Core Foundation

   CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象
   CFRunLoopGetMain();  // 获得主线程的 RunLoop 对象

在代码中是这样的

// 1. 获得主线程对应的 RunLoop
   NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
   
   // 2. 获得当前线程对应的 RunLoop
   NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
   
   // 两者内存地址是一样的
   NSLog(@"%p - %p", mainRunLoop, currentRunLoop);
   //NSLog(@"%@", mainRunLoop);
   
   // 3. core
   NSLog(@"%p - %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());

通过打印结果我们可以发现, 当前线程的 RunLoop 和主线程的 RunLoop 内存地址是一样的

RunLoop 和线程的关系

两者是一一对应的. 只是主线程的 RunLoop 已经创建, 子线程的 RunLoop 需要手动创建.

[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];

实现run 方法

- (void)run {
    // 创建子线程对应的 RunLoop.   currentRunLoop是懒加载方法,当第一次调用时先判断对应的 RunLoop 是否存在,如果不存在,自己创建并返回,如果存在就直接返回
    NSLog(@"%@", [NSRunLoop currentRunLoop]);
    
    NSLog(@"run - %@", [NSThread currentThread]);
}

RunLoop 相关类

Core Foundation 中关于 RunLoop 的5个类

CFRunLoopModeRef

系统默认注册了5个 Mode:

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

我们平时主要用到的是前两种

每个 RunLoop 中可能有两个或者多个 Mode,

mode 元素:

注意:

当 RunLoop 中添加定时器时, 选择不同的 mode 会有不同的效果

NSDefaultRunLoopMode

// 1. 创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run2) userInfo:nil repeats:YES];
    
    // 2. 添加到 RunLoop 中, 指定 RunLoop 的运行模式为默认模式
    /*
     第一个参数: 定时器
     第二个参数: RunLoop 的运行模式
     */
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 

但是这时候有个问题, 当拖拽界面其他控件(例如 scrollView 和 textView)的时候,定时器不工作. 松开点击后才工作

UITrackingRunLoopMode

    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

这个模式下只有拖拽界面其他控件的时候,定时器才工作,但停止拖拽, 依然不工作

想要实现无论在什么情况下, 定时器都能正常工作,可以这样

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

确实能达到效果,但看起来并不是那么完美. 这是时候可以用另一种 mode
NSRunLoopCommonModes

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

我们先来看下NSRunLoopCommonModes里默认保存是什么 mode

NSLog(@"%@", [NSRunLoop currentRunLoop]);

打印结果:


由此可知,NSRunLoopCommonModes里面就有前面前面需要的两个 mode,这个时候在运行工程, 无论是拖拽还是不拖拽, 定时器都能正常工作了.

CFRunLoopTimerRef

 // 1. 创建 GCD 中的定时器
    /*
     第一个参数:source 的类型,DISPATCH_SOURCE_TYPE_TIMER 表示定时器
     第二个参数:描述信息,线程 ID
     第三个参数:更详细的描述信息
     第四个参数:队列, 决定 GCD 定时器中的任务在哪个线程中执行
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); // 程序并发队列
    
    // 2. 设置定时器-- 起始时间和间隔时间以及精准度
    /*
     第一个参数:timer, 定时器对象
     第二个参数:起始时间,DISPATCH_TIME_NOW 从现在开始几时
     第三个参数:间隔时间 GCD 中的时间单位是纳秒
     第四个参数:精准度 绝对精准 0
     */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    
    // 3. 设置定时器要执行的任务
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"GCD - %@", [NSThread currentThread]);
    });
    
    // 4. 启动执行
    dispatch_resume(timer);

这个时候调用该方法,发现定时器并没有工作,需要先生命一个 定时器属性

@property (nonatomic, strong) dispatch_source_t timer;

然后在上面代码的最后给属性赋值

self.timer = timer;

这个时候定时器就正常工作了.

GCD 与 NSTimer 比较
优点: GCD 不受 RunLoop 运行运行模式的影响.

CFRunLoopSourceRef

CFRunLoopSourceRef 是事件源, 也就是输入源

以前的方法:

现在的方法:

用户点击按钮事件触发的就是Source0, 应征了上面所说的


CFRunLoopObserverRef

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
};

声明一个方法

- (void)observer {
    
    // 1. 创建观察者
    /*
     第一个参数: 怎么分配存储空间
     第二个参数: 要监听 RunLoop 的哪些状态
     第三个参数: 是否要持续监听
     第四个参数: 优先级 0 就可以
     第五个参数: 当状态改变的时候会回调 block
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler( CFAllocatorGetDefault(),  kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"即将进入 Loop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理 Timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理 Source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"刚从休眠中唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"即将退出 Loop");
                break;
        }
        
    });
    
    // 2. 给 RunLoop 添加观察者
    /*
     第一个参数: 要监听哪个 RunLoop
     第二个参数: 观察者
     第三个参数: 运行模式, 需要传 C语言方法
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); 
}

在给 RunLoop 添加观察者的时候有提到, 运行mode 需要传 C语言方法,其实
NSDefaultRunLoopMode 就等同于 kCFRunLoopDefaultMode
NSRunLoopCommonModes 就等同于 kCFRunLoopCommonModes

运行结果:


我们可以看到 RunLoop 运行的各个状态,加上定时器,效果更加明显一些

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];

会看到 RunLoop 状态会重复变化,说明 RunLoop 不会彻底退出,只要有任务,会一直执行


所以,我们可以得出一条结论: 通过监听 RunLoop 的状态可以让其进行相应的操作

RunLoop 处理逻辑

官方版文字介绍


这里找到了网上一个处理流程图文介绍


可以清晰的看出 RunLoop 的整个处理流程, 而 RunLoop 最终的状态就是 休眠 状态, 一旦有任务就会唤醒

RunLoop 应用

RunLoop 介绍的差不多了,来看下它都有哪些应用:

开启常驻线程

常驻线程就是让一个子线程不进入消亡状态, 等待其他线程发来消息,及时处理其他任务事件

下图是没有开启常驻线程时, 线程直接就 end 了,显然不能满足我们的需求


我们可以创建一个定时器,以保证线程不会结束,但这样显然也不完美,因为无论有没有任务,定时器都一直在工作


这时候,我们就可以让 RunLoop 调用 addPort: forMode:方法,这个时候在运行, 线程就不会 end 了,在有新任务要执行的时候也能及时处理,也就达到了我们的目的.

总结

  1. RunLoop 概念:
  1. RunLoop自动释放池什么时候创建
  1. RunLoop自动释放池什么时候释放
  1. 在开发中如何使用 RunLoop? 什么应用场景?
上一篇下一篇

猜你喜欢

热点阅读