美文网首页
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

    原文链接: runloop RunLoop简介 RunLoop 实际上是一个对象,用来处理程序运行过程中出现的各种...

网友评论

      本文标题:OC_Runloop

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