美文网首页
OC_Runloop

OC_Runloop

作者: figure_ai | 来源:发表于2016-11-29 16:41 被阅读0次

    RunLoop简介

    RunLoop 实际上是一个对象,用来处理程序运行过程中出现的各种事件(比如:触摸、UI刷新、定时器、selector等),从而保持程序的持续运行;而且在没有事件处理时,会进入睡眠模式,节省资源,提高性能。

    1.RunLoop和线程

    线程的作用使用来执行一个或多个任务,线程执行完之后就会退出,不能再执行任务。而RunLoop能够让线程处理任务。

    • 每个线程都有唯一一个与之对应的RunLoop对象。
    • 我们只能操作当前线程的RunLoop,而不能操作其他线程的RunLoop。
    • RunLoop对象再第一次获取RunLoop时创建,销毁则是在线程结束的时候。
    • 主线程的RunLoop对象系统会自动创建,而子线程的RunLoop对象需要我们主动创建。

    2.RunLoop相关类

    RunLoop 有5个类,只有弄懂这几个类的含义,才能深入了解RunLoop运行机制。

    1. CFRunLoopRef : 代表RunLoop对象;
    2. CFRunLoopModeRef : RunLoop的运行模式;
    3. CFRunLoopSourceRef : RunLoop输入源/事件源;
    4. CFRunLoopTimerRef : RunLoop定时源;
    5. CFRunLoopObserverRef : 观察者,能够监听RunLoop的状态改变;

    一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而一个运行模式又包含若干个输入源(CFRunLoopSourceRef)、定时源(�CFRunLoopTimerRef)、观察者(CFRunLoopObserveRef)。

    2.1CFRunLoopRef

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

    • Core Foundation
      • CFRunLoopGetCurrent();//获取当前线程的RunLoop对象。
      • CFRunLoopGetMain();//获取主线程的RunLoop对象。

    Foundation框架下获取RunLoop对象类的方法:

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

    2.2CFRunLoopModeRef

    系统默认定义了以下几种运行模式(CFRunLoopModeRef):

    1. kCFRunLoopDefaultMode : App默认运行模式,通常主线程实在这个运行模式下运行。
    2. **UITrackingRunLoopMode ** :跟踪用户的交互时间(用于追踪Scrollview触摸滑动,保证界面滑动时不受其他Mode影响);
    3. kCFRunLoopCommonModes : 伪模式,不是一种真正的运行模式,后续有介绍;
    4. UIInitializationRunLoopMode : App启动时进入的第一个Mode,启动完成后就不再使用;
    5. GSEventReceiveRunLoopMode : 接受系统内部时间,通常不会用到;

    : 前三种模式是我们开发中需要用到的模式。

    2.3CFRunLoopTimerRef

    CFRunLoopRef是定时源,可以理解为基于时间的触发器,基本上可以说是NSTimer;

    示例项目:

    1. 新建一个IOS项目,在Main.storyboard中拖入一个TextView;
    2. 在ViewController.m文件中写入一下代码;
    - (void)viewDidLoad{
      [super viewDidLoad];
    
      //定义一个定时器,每两秒调用run方法
      NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:Yes];
      
    //将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
    [ [NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefauRunLoopMode];
    }
    
    - (void)run{
      NSLog(@"--run");
    }
    

    3.然后运行,当拖动Text View时,会发现run方法不打印了,也就是说NSTimer不工作了。而当鼠标松开时,NSTimer又开始正常工作了。

    原因:

    • 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下。
    • 当我们拖动Text View的时候,RunLoop就切换到了UITrackingRunLoop模式下工作了。而在这个项目中,NSTimer没有添加到UITrackingRunLoop模式下。所以拖动Text View的时候NSTimer停止了。
    • 当我们松开鼠标的时候,RunLoop又切换到了NSDefaultRunLoopMode模式下工作了,所以NSTimer又开始工作了。

    解决方法:

    • 把NSTimer添加到伪模式(kCFRunLoopCommonModes)下工作;(伪模式(kCFRunLoopCommonModes):这实际上是一种标记模式,意思就是可以在打上Common Modes标记的模式下运行;)因为NSDefaultRunLoopMode和UITrackingRunLoopMode两种模式已经被打上了Common Modes 标记了。
    • 具体做法:将添加语句改为:[ [NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes ];

    扩展:

    • NSTimer中的schduledTimerWithTimeInterval方法和RunLoop的关系,添加这句代码:
      `[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    `

    • 这句代码调用了scheduledTimer返回的定时器,NSTimer会自动被加入到RunLoop的NSDefaultRunLoopMode模式下。

    2.4CFRunLoopSourceRef

    CFRunLoopSourceRef是事件源,CFRunLoopSourceRef有两种分类方法:

    • 第一种按照官方文档来分类(就像RunLoop模型图中那样):
      • Port-Based Source (基于端口)
      • Custom Input Source (自定义)
      • Cocoa Perform Selector Source
    • 第二种按照函数调用栈来分类:
      • Source0:非基于Port;
      • Source1:基于Port,通过内核和其他线程通信,接收、分发系统事件;

    注:

    这两种分类其实没有区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。

    示例项目:

    1. 在Main.storyboard中添加一个Button按钮,并添加点击动作。

    2. 然后在点击方法的代码中加入一句输出语句,并打上断点,如下图所示


      Paste_Image.png
    3. 然后运行程序,点击按钮;

    4. 然后在项目中单机下图红色部分:


      Paste_Image.png
    5. 可以看到如下图所示就是点击事件产生的函数调用栈:


      Paste_Image.png

    示例解析:

    1. 首先程序启动,调用16行的main函数,main函数调用15行UIApplicationMain函数,然后一直往上调用函数,最终调用到0行的BtnClick函数,即点击函数。
    1. 同时,我们可以看到11行中有Source0,也就是说我们点击事件是属于Source0函数的,点击事件就是在Sources0中处理的。
    2. 而至于Source1,则是用来接收、分发系统事件,然后分发到Source0中处理的。

    2.5CFRunLoopObserverRef

    • CFRunLoopObserver是观察者,用来监听RunLoop的状态改变。

    CFRunLoopObserverRef可以监听的状态改变有以下几种:

    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),              //即将从Loop中退出:64
    
       kCFRunLoopExit = (1UL << 7),                            //即将从Loop中退出:128
    
       kCFRunLoopAllActivities = 0x0FFFFFFFU          //监听全部状态改变
    };
    

    代码演示:

    1. 在ViewController.m中添加如下代码:
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //创建观察者
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            NSLog(@"监听到RunLoop状态发生改变-----%zd",activity);
        });
        
        //添加观察者到当前RunLoop中
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        
        //释放observer;最后添加完需要释放掉
        CFRelease(observer);
    }
    

    2.运行,打印结果如下图:

    Paste_Image.png

    解析:

    • 可以看到,RunLoop状态在不断改变,最终变成了32,也就是即将进入睡眠状态,说明RunLoop之后就会进入睡眠状态。

    3.RunLoop原理

    Paste_Image.png

    官方文档说明的RunLoop逻辑:

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

    4.RunLoop 实战应用


    4.1 NSTimer的使用

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

    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 后台常驻线程

    • 在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(如:下载文件、后台播放音乐等),最好的解决方法就是能让这条线程永远常驻内存。
    • 实现思路:添加一条用于常驻内存的强引用子线程,在该线程的RunLoop下添加一个Source,开启RunLoop;

    实现过程如下:

    1. 在ViewController.m 添加一条抢引用的thread线程属性,代码如下:
    @interface ViewController ()
    
    @property (strong, nonatomic) NSThread *thread;
    
    @end
    

    2.在viewDidLoad中创建线程self.thread,使线程启动并执行run1方法,代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        //创建线程,并调用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");
    }
    

    3.运行结果如图,说明这时已经开启了一条常驻线程。


    Paste_Image.png

    4.接下来我们在touchesBegan中调用PerformSelector,实现点击屏幕的时候往常驻线程添加run2任务。具体代码如下:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        //利用performselector,在self.thread的线程中调用run2方法
        [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    - (void)run2
    {
        NSLog(@"----------run2---------");
    }
    

    5.运行,点击屏幕之后打印结果如下,这样我们就实现了常驻内存的需求了。


    Paste_Image.png

    相关文章

      网友评论

          本文标题:OC_Runloop

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