[TOC]

NSRunLoop

一. 什么是RunLoop

RunLoop

RunLoop是运动循环,不断跑圈,无限循环。NSRunLoop对应线程,从线程start到线程end,一直在循环检测:

  • 检测inputsource(如点击,双击等操作)同步事件,检测timesource同步事件;

  • 检测到后,会执行处理函数,首先会产生通知,Core Foundation向线程添加RunLoop Observers来监听事件,意在监听事件发生时来做处理。

RunLoop的三大基本作用:

  • 他是App持续运行的保证, 保持程序的持续运行 (iOS程序一直活着的原因)
  • RunLoop会在循环中处理App的各种事件:触摸事件, 定时器事件, Selector事件
  • RunLoop最大的优势就是能节省CPU的资源, 提高程序的性能, 他会在需要执行任务的时候被唤醒, 当没有任务执行的时候进入休眠状态

主程序的RunLoop

Main函数中的RunLoop, 被称为主运行循环, 而主运行循环在整个App的声明周期中都不会被销毁, 他是程序运行的保证。

程序启动时,系统已经在主线程中加入了Run Loop。它保证了我们的主线程在运行起来后,就处于一种“等待”的状态,这个时候如果有接收到的事件(Timer的定时到了或是其他线程的消息),就会执行任务,否则就处于休眠状态。

  • 首先, 重温一遍App的启动原理

  • 当Main函数执行到UIApplicationMain时, 就开启了RunLoop运行循环

  • 在运行循环开启时, 就会保证程序的持续运行并且处理App的各种事件, 不会退出

         // 程序在启动时,第一步就会执行main函数,在main函数中会执行以下操作:
         int main(int argc, char * argv[]) {
             @autoreleasepool {
                 /*
                  nil:UIApplication类名或者子类名称,如果为nil,就等于@"UIApplication"
                  NSStringFromClass([AppDelegate class]:UIApplication代理的名称
                  */
                 return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
             }
         }
    
         程序启动的完整流程
    1. 执行main函数
    2. 执行UIApplicationMain函数
         1> 指定UIApplication对象
         2> 指定UIApplication的代理
    3. 创建UIApplication对象,并且指定他的代理
    //4. 创建一个事件循环:主循环(RunLoop),并且是一个死循环,保证程序的持续运行
    5. 加载配置了所有应用程序信息的info.plist文件
         1> 判断 Main storyboard file base name中有没有指定Main,即需要加载的StoryBoard文件
         2> 如果指定了,就加载Main.storyboard
         3> 如果没有指定的话,就会黑屏
    6. 应用程序启动完毕
    

二. NSRunLoop和CFRunLoopRef

在iOS开发中有两套api来访问Runloop:

  • Foundation框架:NSRunloop (ObjC)
  • Core Foundation框架:CFRunloopRef (C)

NSRunLoop和CFRunLoopRef都代表着RunLoop对象,它们是等价的,可以互相转换;

NSRunLoop是基于CFRunLoopRef的一层ObjC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API。

//Foundation框架【NSRunloop】创建Runloop对象
    // 1. 获取当前线程下的Runloop, 懒加载的形式创建
    NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];
    NSLog(@"%p", curRunLoop);
    // 2. 获取主线程对应的RunLoop对象
    NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
    NSLog(@"%p", mainRunLoop);


//Core Foundation框架【CFRunloopRef】创建Runloop对象
    // 1. 获取当前线程下的Runloop, 懒加载的形式创建
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    NSLog(@"%p", runloop);
    // 2. 获得主线程的RunLoop对象
    CFRunLoopRef cfMainRunLoop = CFRunLoopGetMain();
    NSLog(@"%p", cfMainRunLoop);


// 从这里可以看出这两种运行循环是完全不同的对象
    // NSRunLoop --> CFRunLoopRef
    // 调用getCfRunLoop方法, 将NSRunLoop转化为CFRunLoop
    NSLog(@"%p---%p", cfMainRunLoop, mainRunLoop.getCFRunLoop);

RunLoopMode

RunLoopMode的运行模式图

RunLoopMode的运行模式图

RunLoopMode: 即RunLoop的运行模式,Runloop要想跑起来,它的内部必须要有一个mode,mode中必须有source/observer/timer,至少要有其中的一个.

  • ==每个RunLoop必须设置一个运行模式==, 当指定(显式或隐式)run loop的运行模式以后,只有与该模式对应的input sources才被监控并允许run loop对事件进行处理, 也只有与该模式对应的observers才会被通知

  • RunLoop启动之后, ==只能指定一个运行模式==, 可以使用currentMode来获取。

  • 如果要切换RunLoop的运行模式, 就要先退出当前的RunLoop, 重新指定Mode再次进入运行。

Core Foundation系统默认注册的5个Mode:

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

  • UITrackingRunLoopMode: 界面跟踪Mode, 用于ScrollView/TableView等追踪触摸滑动, 保证界面滑动的时候不受其他Mode影响

  • UIInitializationRunLoopMode: 当App启动时, 第一个进入的Mode, 启动完成之后就不会再使用这个Mode

  • GSEventReceiveRunLoopMode: 接收系统事件的内部Mode, 通常由系统自动管理

  • kCFRunLoopCommonMode: 一个类似于占位的Mode, 并不是一个真正的Mode

ObjC系统默认注册的5个Mode:

  • NSDefaultRunLoopMode:默认,几乎包括所有输入源(除NSConnection)
  • NSRunLoopCommonMode:这是一组可配置的通用模式。将input sources与该模式关联则同时也将input sources与该组中的其它模式进行了关联。
  • mode模式:处理modal panels
  • connection模式:处理NSConnection事件,属于系统内部,用户基本不用
  • event tracking模式:如组件拖动输入源 UITrackingRunLoopModes 不处理定时事件

底层相关类

CFRunloopRef
CFRunloopModeRef【Runloop的运行模式】
    CFRunloopSourceRef【Runloop要处理的事件源】
    CFRunloopTimerRef【Timer事件】
    CFRunloopObserverRef【Runloop的观察者(监听者)】
  1. CFRunLoopSourceRef: 事件源, 事件, 输入等都属于事件源, 他有两个分类

    • Source0, 非基于Port, 用户触发的事件, 例如点击事件等
    • Source1, 基于Port的事件, 他用于系统内部与线程之间交互
  2. CFRunLoopTimerRef: 定时器事件:<见下文>

定时器事件

  1. NSTimer:

    • 每一个线程都有一个NSRunLoop对象,然而定时器也是在这个对象上面运行的; 但是,当一个线程运行完成了过后,会自动关闭线程,自然NSRunLoop也会被销毁,自然定时器就不会运行;为了不让其线程关闭,用此语句 [[NSRunLoop currentRunLoop]run];那么线程就会保持活跃状态,不会被关闭。
    • 如果将NSTimer添加到子线程中, 需要先创建一个RunLoop, 然后再启动RunLoop
    • NSTimer受到RunLoop的影响, 一般会有一些轻微的误差, 所以对于精密计时, GCD定时器较为精准(GCD定时器不受RunLoop约束,比NSTimer更加准时) ```objective-c NSTimer默认添加到当前NSRunLoop中,也可以手动制定添加到自己新建的NSRunLoop。 //第1种方式:

      //此种方式创建的timer默认添加到当前NSRunLoop中 [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

      //保持线程为活动状态,才能保证定时器执行 [[NSRunLoop currentRunLoop] run];//已经将nstimer添加到NSRunloop中了

 //第2种方式
 //此种方式创建的timer没有添加至runloop中
 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

 //将定时器添加到runloop中
 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

 //保持线程为活动状态,才能保证定时器执行
 [[NSRunLoop currentRunLoop] run];
 ```
  1. GCD定时器: 精确到纳秒, 较为精准
    • GCD定时器不受RunLoop约束,比NSTimer更加准时。
    • GCD定时器的创建步骤: 创建定时器 -> 设置定时器 -> 设置定时器的回调方法 -> 恢复定时器
    • GCD定时器一定要添加一个强引用, 否则会被立即释放
@interface ViewController ()
/** 定时器属性:要强引用定时器,否则创建出来就会被释放*/
@property (nonatomic, strong) dispatch_source_t timer;
@end


int count = 0;

//1.创建定时器
  // 获得队列
  dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

  // 创建一个定时器
    /*
     DISPATCH_SOURCE_TYPE_TIMER 定时器
     uintptr_t handle  描述信息
     unsigned long mask  传入0
     dispatch_queue_t queue  定时器运行的队列,决定定时器在哪个线程中运行。
     */
  self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
  //注意:这里的定时器(dispatch_source_t类型)其实是个OC对象,所以必须强引用"self.timer"。

//2.设置定时器的开始时间,间隔时间
  dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)); //晚3秒
  uint64_t interval = 1.0 * NSEC_PER_SEC; // 间隔时间 1s

  /*设置定时器
   dispatch_source_t source,  定时器的对象
   dispatch_time_t start,     定时器什么时候开始
   uint64_t interval,         定时器多长时间执行一次
   uint64_t leeway            精准度,0为绝对精准
   */
  dispatch_source_set_timer(self.timer, start, interval, 0);

//3.设置回调
  dispatch_source_set_event_handler(self.timer, ^{
      NSLog(@"%@", [NSThread currentThread]);
      count++;

      if (count == 4) {
          // 取消定时器
          dispatch_cancel(self.timer);
          self.timer = nil;
      }
  });

//4.启动定时器
 dispatch_resume(self.timer);

  1. CFRunLoopObserverRef: 观察者

    • 观察者可以观察到RunLoop不同的运行状态

    • 通过判断RunLoop的运行状态, 可以执行一些操作

        // 1. 创建监听者
        /*
         CFAllocatorRef allocator 分配存储空间
         CFOptionFlags activities 要监听哪个状态,kCFRunLoopAllActivities监听所有状态
         Boolean repeats 是否持续监听RunLoop的状态
         CFIndex order 优先级,默认为0
         Block activity RunLoop当前的状态
         */
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      
            /*
             kCFRunLoopEntry = (1UL << 0),          进入工作
             kCFRunLoopBeforeTimers = (1UL << 1),   即将处理Timers事件
             kCFRunLoopBeforeSources = (1UL << 2),  即将处理Source事件
             kCFRunLoopBeforeWaiting = (1UL << 5),  即将休眠
             kCFRunLoopAfterWaiting = (1UL << 6),   被唤醒
             kCFRunLoopExit = (1UL << 7),           退出RunLoop
             kCFRunLoopAllActivities = 0x0FFFFFFFU  监听所有事件
             */
      
            // 当activity处于什么状态的时候,调用一次
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"进入");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"即将处理Timer事件");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"即将处理Source事件");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"即将休眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"被唤醒");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"退出RunLoop");
                    break;
                default:
                    break;
            }
        });
      
        // 2. 给对应的RunLoop添加一个监听者,并制定监听的是那种运行模式
        /*
         CFRunLoopRef rl  要添加监听者的RunLoop
         CFRunLoopObserverRef observer,  要添加的监听者
         CFStringRef mode  RunLoop的运行模式
         */
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
      

三. RunLoop与线程

  1. ==每一条线程, 都有一个与之相对应的RunLoop对象, 负责处理线程中的任务。==
  2. 线程的创建
    • 主线程: RunLoop是在程序已经启动的时候就创建好了, 当程序关闭的时候主线程才被销毁
    • 子线程: 子线程==需要手动创建RunLoop==, 并且手动开启, 当没有任务执行时, 该线程会被关闭, RunLoop被销毁
  3. 子线程和RunLoop
    • 子线程会单独开启RunLoop去执行任务
    • 子线程和RunLoop是一一对应的关系, 每个子线程都有自己的RunLoop(但需要主动创建)
    • ==创建子线程的RunLoop: [NSRunloop currentRunLoop]==通过对CFRunLoop原码的分析可以判断出, 这个方法是懒加载获取RunLoop对象的, 当第一次调用这个方法时, 他就会在对应的线程中创建一个RunLoop, 并且保存到一个字典中便于随时取出。也就是说, 如果不主动去获取RunLoop, 那么默认是不会给子线程创建一个RunLoop的。
    • ==子线程的RunLoop需要手动开启: [[NSRunLoop currentRunLoop] run]==
    • 如果RunLoop内部没有任何任务需要去处理时, 就会被关闭
// 开启子线程,执行task方法
[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];

- (void)task {
 每一个子线程,都对应一个自己的RunLoop:
  [NSRunLoop currentRunLoop]  //获取当前子线程的RunLoop
  [[NSRunLoop currentRunLoop] run]  //开启当前子线程的RunLoop

  // 1. 获取当前子线程的RunLoop,主线程的RunLoop
  NSLog(@"%p--%p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);

  // 2. 开启RunLoop
  [[NSRunLoop currentRunLoop] run];
}



// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
   static CFMutableDictionaryRef loopsDic;
   // 访问 loopsDic 时的锁
   static CFSpinLock_t loopsLock;

   // 获取一个 pthread 对应的 RunLoop
   CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
       OSSpinLockLock(&loopsLock);

       if (!loopsDic) {
           // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
           loopsDic = CFDictionaryCreateMutable();
           CFRunLoopRef mainLoop = _CFRunLoopCreate();
           CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
       }

       // 直接从 Dictionary 里获取。
       CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

       if (!loop) {
           // 取不到时,创建一个
           loop = _CFRunLoopCreate();
           CFDictionarySetValue(loopsDic, thread, loop);
           // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop
           _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
       }

       OSSpinLockUnLock(&loopsLock);
       return loop;
   }

   CFRunLoopRef CFRunLoopGetMain() {
       return _CFRunLoopGet(pthread_main_thread_np());
   }

   CFRunLoopRef CFRunLoopGetCurrent() {
       return _CFRunLoopGet(pthread_self());
   }

results matching ""

    No results matching ""