iOS RunLoop

什么是RunLoop?

  • 可以理解为字面意思:Run表示运行,Loop表示循环。结合在一起就是运行的循环的意思。哈哈,我更愿意翻译为『跑圈』。直观理解就像是不停的跑圈
  • RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能
  • RunLoop保存在一个全局的字典里,线程作为key,RunLoop作为value

RunLoop内部结构

  • _modes:存放所有的模式
  • _pthread:存放的线程
  • _currentMode:当前的模式


    c3d9bf03.jpg

RunLoopMode内部结构

  • _soutce0, _soutce1:存放在CFRunLoopSourceRef
  • _observers:存放在CFRunLoopObserverRef
  • _timers:存放在CFRunLoopTimerRef


    3df6dff2.jpg

RunLoop和线程

  • RunLoop和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以,我们就有了RunLoop
    • 一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的RunLoop对象。
    • 我们只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。
    • RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候。
    • 主线程的RunLoop对象系统自动帮助我们创建好了(原理如下),而子线程的RunLoop对象需要我们主动创建。
    • 子线程的runLoop里面至少要有一个source或者是timer,observer是不行的。

默认情况下主线程的RunLoop原理

  • 我们在启动一个iOS程序的时候,系统会调用创建项目时自动生成的main.m的文件。main.m文件如下所示:

    int main(int argc, char * argv[]) {
       @autoreleasepool {
           return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
       }
    }
    
    
  • 其中UIApplicationMain函数内部帮我们开启了主线程的RunLoop,UIApplicationMain内部拥有一个无线循环的代码。上边的代码中开启RunLoop的过程可以简单的理解为如下代码:

     int main(int argc, char * argv[]) {        
         BOOL running = YES;
         do {
             // 执行各种任务,处理各种事件
             // ......
         } while (running);
    
         return 0;
     }
    
    

    从上边可看出,程序一直在do-while循环中执行,所以UIApplicationMain函数一直没有返回,我们在运行程序之后程序不会马上退出,会保持持续运行状态。

    下图是苹果官方给出的RunLoop模型图。

    1877784-6ab632fc118e31f3.jpg

从上图中可以看出,RunLoop就是线程中的一个循环,RunLoop在循环中会不断检测,通过Input sources(输入源)和Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息

RunLoop相关类

  • 下面我们来了解一下Core Foundation框架下关于RunLoop的5个类,只有弄懂这几个类的含义,我们才能深入了解RunLoop运行机制
    • CFRunLoopRef:代表RunLoop的对象
    • CFRunLoopModeRef:RunLoop的运行模式
    • CFRunLoopSourceRef:就是RunLoop模型图中提到的输入源/事件源
      • source0:
        • 触摸事件,
        • performSelector:onThread
      • source1:
        • 基于Port的线程间的通信
        • 系统事件的捕捉
    • CFRunLoopTimerRef:就是RunLoop模型图中提到的定时源
      • NSTimer
      • performSelector:withObject:afterDelay
    • CFRunLoopObserverRef:观察者,能够监听RunLoop的状态改变
      • 用于监听RunLoop的状态
      • UI刷新(BeforeWaiting)
      • AutoreleasePool


        1877784-2177aa2828b1ad34.png

CFRunLoopRef

  • CFRunLoopRef就是Core Foundation框架下RunLoop对象类。我们可通过以下方式来获取RunLoop对象:

    • Core Foundation

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

      当然,在Foundation框架下获取RunLoop对象类的方法如下:

    • Foundation

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

CFRunLoopModeRef

  • CFRunLoopModeRef代表着RunLoop的运行模式
  • 一个RunLoop有若干个Mode,每个模式又包含若干个Source0,Source1,Timers,Observer
  • RunLoop启动时只能选择其中一个Mode,作为currentMode
  • 如果需要切换运行模式,只能退出Loop,再重新选择一个运行模式进入。
  • 这样做主要是为了分隔开不同组的Source0,Source1,Timers,Observer,让其互不影响 。
  • 如果Mode里面没有Source0,Source1,Timers,Observer,RunLoop会马上退出

常见的几种模式

  • kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行
  • UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
  • UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
  • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式(后边会用到)

CFRunLoopObserverRef

  • 创建Observer
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
       kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop:1
       kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理Timer:2    
       kCFRunLoopBeforeSources = (1UL << 2),       // 即将处理Source:4
       kCFRunLoopBeforeWaiting = (1UL << 5),       // 即将进入休眠:32
       kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒:64
       kCFRunLoopExit = (1UL << 7),                // 即将从Loop中退出:128
       kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
    };
    
    
     //创建一个监听对象
     //    第一个参数:分配存储空间
     //    第二个参数:要监听的状态
     //    第三个参数: 是否要持续监听
     //    第四个参数:优先级
     //    第五个参数:回调
      CFRunLoopObserverRef obser =  CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
     //        状态判断
             switch (activity) {
                 case kCFRunLoopEntry:
    
                     break;
                 case kCFRunLoopBeforeTimers:
    
                     break;
                 case kCFRunLoopBeforeSources:
    
                     break;
                 case kCFRunLoopBeforeWaiting:
    
                     break;
                 case kCFRunLoopAfterWaiting:
    
                     break;
                 case kCFRunLoopExit:
    
                     break;
                 case kCFRunLoopAllActivities:
    
                     break;
                 default:
                     break;
             }
         });
    
     //    第一个参数:要监听哪个Loop
     //    第二个参数:监听者
     //    第三个参数: 要监听runloop在哪种运行模式下的状态
     CFRunLoopAddObserver(CFRunLoopGetCurrent(), obser, kCFRunLoopDefaultMode);
    
    
  • CFRunLoopTimerRef
    • 创建定时器
      NSTimer *time =[NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
      //设置runloop,添加到默认模式下
      [[NSRunLoop currentRunLoop] addTimer:time forMode:NSDefaultRunLoopMode];
      //设置runloop,添加到拖拽模式下
       [[NSRunLoop currentRunLoop] addTimer:time forMode:UITrackingRunLoopMode];
      //NSRunLoopCommonModes标记,在两种模式下都会运行
      [[NSRunLoop currentRunLoop] addTimer:time forMode:NSRunLoopCommonModes];
      
      

RunLoop运行逻辑

2dbc55cb.jpg
  • 具体的顺序如下:
    • 1.通知观察者RunLoop已经启动
    • 2.通知观察者即将要开始的定时器
    • 3.通知观察者任何即将启动的非基于端口的源
    • 4.启动任何准备好的非基于端口的源
    • 5.如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
    • 6.通知观察者线程进入休眠状态
    • 7.将线程置于休眠知道任一下面的事件发生:
      • 某一事件到达基于端口的源
      • 定时器启动
      • RunLoop设置的时间已经超时
      • RunLoop被显示唤醒
    • 8.通知观察者线程将被唤醒
    • 9.处理未处理的事件
      • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
      • 如果输入源启动,传递相应的消息
      • 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
    • 10.通知观察者RunLoop结束

RunLoop运用场景详情

  • 4.1 NSTimer的使用

NSTimer的使用方法在讲解CFRunLoopTimerRef类的时候详细讲解过,具体参考上边 2.3 CFRunLoopTimerRef。

  • 4.2 ImageView推迟显示

有时候,我们会遇到这种情况:
当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这时候当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象。

怎么解决这个问题呢?

这时候,我们应该推迟图片的显示,也就是ImageView推迟显示图片。有两种方法:

* 1. 监听UIScrollView的滚动
  因为UITableView继承自UIScrollView,所以我们可以通过监听UIScrollView的滚动,实现UIScrollView相关delegate即可。

* 2. 利用PerformSelector设置当前线程的RunLoop的运行模式

 利用performSelector方法为UIImageView调用setImage:方法,并利用inModes将其设置为RunLoop下NSDefaultRunLoopMode运行模式。代码如下:
  • 4.3 后台常驻线程(很常用)
    - (void)viewDidLoad {
       [super viewDidLoad];
    
       // 创建线程,并调用run1方法执行任务
       self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
       // 开启线程
       [self.thread start];    
    }
    
    - (void) run1
    {
       // 这里写任务
       NSLog(@"----run1-----");
    
       // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
       [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
       [[NSRunLoop currentRunLoop] run];
    
       // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
       NSLog(@"未开启RunLoop");
    }
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {   
       // 利用performSelector,在self.thread的线程中调用run2方法执行任务
       [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    - (void) run2
    {
       NSLog(@"----run2------");
    }
    
    
  • 4.3 封装常驻线程
    //MJPermenantThread.h
    typedef void (^MJPermenantThreadTask)(void);
    
    @interface MJPermenantThread : NSObject
    
    /**
    开启线程
    */
    //- (void)run;
    
    /**
    在当前子线程执行一个任务
    */
    - (void)executeTask:(MJPermenantThreadTask)task;
    
    /**
    结束线程
    */
    - (void)stop;
    
    @end
    
    //MJPermenantThread.m
    
    /** MJThread **/
    @interface MJThread : NSThread
    @end
    @implementation MJThread
    - (void)dealloc
    {
      NSLog(@"%s", __func__);
    }
    @end
    
    /** MJPermenantThread **/
    @interface MJPermenantThread()
    @property (strong, nonatomic) MJThread *innerThread;
    @property (assign, nonatomic, getter=isStopped) BOOL stopped;
    @end
    
    @implementation MJPermenantThread
    #pragma mark - public methods
    - (instancetype)init
    {
      if (self = [super init]) {
          self.stopped = NO;
    
          __weak typeof(self) weakSelf = self;
    
          self.innerThread = [[MJThread alloc] initWithBlock:^{
              [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    
              while (weakSelf && !weakSelf.isStopped) {
                  [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
              }
          }];
    
          [self.innerThread start];
      }
      return self;
    }
    
    //- (void)run
    //{
    //    if (!self.innerThread) return;
    //
    //    [self.innerThread start];
    //}
    
    - (void)executeTask:(MJPermenantThreadTask)task
    {
      if (!self.innerThread || !task) return;
    
      [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
    }
    
    - (void)stop
    {
      if (!self.innerThread) return;
    
      [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
    }
    
    - (void)dealloc
    {
      NSLog(@"%s", __func__);
    
      [self stop];
    }
    
    #pragma mark - private methods
    - (void)__stop
    {
      self.stopped = YES;
      CFRunLoopStop(CFRunLoopGetCurrent());
      self.innerThread = nil;
    }
    
    - (void)__executeTask:(MJPermenantThreadTask)task
    {
      task();
    }
    
    @end
    
    //调用
    (void)viewDidLoad {
        [super viewDidLoad];
    
        self.thread = [[MJPermenantThread alloc] init];
    }
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
        [self.thread executeTask:^{
            NSLog(@"执行任务 - %@", [NSThread currentThread]);
        }];
    }
    
    - (IBAction)stop {
        [self.thread stop];
    }
    
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
    }
    
  • RunLoop休眠实现原理

b2b20b35.jpg

你可能感兴趣的:(iOS RunLoop)