RunLoop学习笔记

本文我主要是学习如下文章做的记录,同时还有其他优秀的文章没有粘贴出来,在这里均表示感谢。
iOS RunLoop入门小结
RunLoop入门 看我就够了

一、简介

1、作用

  1. 保持程序运行
  2. 处理app的各种事件(比如触摸,定时器等等)
  3. 节省CPU资源,提高性能。

2、与线程的关系

  1. RunLoop是寄生于线程的消息循环机制,它能保证线程存活,而不是线性执行完任务就消亡。
  2. RunLoop与线程是一一对应的,每个线程只有唯一与之对应的一个RunLoop。RunLoop在第一次获取时创建,在线程结束时销毁。(这就相当于 线程是一个类,RunLoop是类里的实例变量,这样便于理解)。
  3. 子线程默认没有RunLoop,需要我们去主动开启,但是主线程是自动开启了RunLoop的。

3、RunLoop三种启动方式

1、- (void)run;

这种启动方式:RunLoop永久性的运行NSDefaultRunLoopMode模式,即使用 CFRunLoopStop(runloopRef)也无法停止RunLoop的运行,那么只能永久运行下去.

2、- (void)runUntilDate:(NSDate *)limitDate;

这种启动方式:可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式下。RunLoop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行。

while (!Stop){
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
3、- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

. 有一个超时时间限制,而且可以设置运行模式
. 在非Timer事件触发、显式的用CFRunLoopStop停止RunLoop、到达limitDate后会退出返回
. 如果仅是Timer事件触发并不会让RunLoop退出返回,但是如果是PerfromSelector事件或者其他Input Source事件触发处理后,RunLoop会退出返回.

4、RunLoop正常运行的条件:三个要同时满足

1、至少要包含一个Mode:RunLoop默认包含DefaultMode
2、该Mode下需要有至少一个的事件源:对于NSRunLoop 可以往mode中添加两类事件源(Timer/Source)

. NSTimer:[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
. NSPort(对应的是source1):[runLoop addPort:[NSMachPort port] forMode:UITrackingRunLoopMode];

3、运行在有事件源的Mode下。
注意:只有Observer的RunLoop也是无法正常运行的。

5、获取RunLoop对象

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

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

6、RunLoop结构

image.png
  1. Mode代表的是RunLoop的运行模式。
  2. 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
  3. 每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
  4. 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
. Mode

Mode就是”行为模式“,就像我们说到上学这个行为模式,它就应该包含起床,出门,去学校,上课,午休等等。但是,如果上学这个行为模式什么都不包含,那么即使我们进行上学这个行为,我们也一直睡在床上什么都不会做。
Mode的存在是为了让RunLoop在不同的”行为模式“之下执行不同的”动作“互不影响。
比如执行上学这个行为模式就不能进行娱乐这个行为模式下的游戏这个动作。

1.kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。一般情况下App都是运行在这个mode下的
2.(CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
一般作用于ScrollView滚动的时候的模式,保证滑动的时候不受其他事件影响。
3.kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
这个并不是某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程中只包含NSDefaultRunLoopMode。
注意:
①在选择RunLoop的runMode: beforeDate时不可以填这种模式否则会导致RunLoop运行不成功。
②在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。
③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合。
. Source

输入源事件,分为source0和source1这两种。

1.source0:诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作。
2.source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。
一般来说日常开发中我们需要关注的是source0,source1只需要了解。
之所以说source0更重要是因为日常开发中,我们需要对常驻线程进行操作的事件大多都是source0,稍后的实验会讲到。
. Timer

定时源事件。通俗来讲就是我们很熟悉的NSTimer,其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行。

. Observer

它相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建。

PS: Source/Timer/Observer 被统称为 mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item 都没有(只有Observer也不行),则 RunLoop 会直接退出,不进入循环。

二、实验代码

新建一个继承自NSThread的子类

.h文件

#import <Foundation/Foundation.h>

@interface WGThread : NSThread

@end

.m文件
#import "WGThread.h"

@implementation WGThread

-(void)dealloc{
    NSLog(@"%@---子线程销毁了",self.name);
}

@end

1、开启一个子线程

- (void)nsthread01{
    NSLog(@"%@---开启子线程",[NSThread currentThread]);
    WGThread *thread = [[WGThread alloc] initWithTarget:self selector:@selector(nsthreadMedthod) object:nil];
    thread.name = @"nsthread01";
    [thread start];
}

- (void)nsthreadMedthod{
    NSLog(@"%@---开始->子线程在执行任务",[NSThread currentThread]);
}

结果:

image.png

分析:
子线程任务执行完就自动销毁了。

2、需要一个不被销毁的子线程

. 上述代码中的thread是局部变量,函数走完就销毁了,那么如果用属性来接受会怎么样呢?子线程还会被销毁吗?
@property (nonatomic, strong) WGThread *thread;

- (void)nsthread02{
    NSLog(@"%@---开启子线程",[NSThread currentThread]);
    self.thread = [[WGThread alloc] initWithTarget:self selector:@selector(nsthreadMedthod) object:nil];
    _thread.name = @"nsthread02";
    [_thread start];
}

结果:

image.png

分析:
并没有被释放,好像成功持有了子线程。
我们在[_thread start]后面再添加上一句[_thread start]再运行试试看结果。

image.png

分析:崩溃了
因为执行完任务后,虽然Thread没有被释放,还处于内存中,但是它处于死亡状态,苹果不允许在线程死亡后再次开启。

. 让线程一直处于有任务的状态

我们在子线程中加入一个死循环

- (void)nsthread03{
    NSLog(@"%@---开启子线程",[NSThread currentThread]);
    self.thread = [[WGThread alloc] initWithTarget:self selector:@selector(nsthreadMedthod) object:nil];
    _thread.name = @"nsthread03";
    [_thread start];
}

- (void)nsthreadMedthod{
    do {
        NSLog(@"%@---开始->子线程在执行任务",[NSThread currentThread]);
    } while (1);
}

结果:
子线程确实不会进入死亡状态,但是子线程却在不分时间地点场合的一直执行任务。和我们想要的不一样。

. 使用RunLoop
- (void)nsthread03{
    NSLog(@"%@---开启子线程",[NSThread currentThread]);
    WGThread *thread = [[WGThread alloc] initWithTarget:self selector:@selector(nsthread03Medthod) object:nil];
    thread.name = @"nsthread03";
    [thread start];
}

- (void)nsthread03Medthod{
    NSLog(@"%@---开始->子线程在执行任务",[NSThread currentThread]);
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    [runloop run];
    NSLog(@"%@---结束->子线程在执行任务",[NSThread currentThread]);
}

结果:即使不用对线程用self进行引用,子线程也没有被销毁。

image.png

分析:
因为[runLoop run];这一行的存在。
RunLoop本质就是个Event Loop的do while循环,所以运行到这一行以后子线程就一直在进行“接受消息->等待->处理”的循环。所以不会运行[runLoop run];之后的代码。
可以看到下面代码一直不会被执行。

    NSLog(@"%@---结束->子线程在执行任务",[NSThread currentThread]);
阶段总结: RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。它是寄生于线程的,所以提到RunLoop必然会涉及到线程。
. 验证RunLoop运行的条件

1、注释掉 [runloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];

- (void)nsthread03{
    NSLog(@"%@---开启子线程",[NSThread currentThread]);
    WGThread *thread = [[WGThread alloc] initWithTarget:self selector:@selector(nsthread03Medthod) object:nil];
    thread.name = @"nsthread03";
    [thread start];
}

- (void)nsthread03Medthod{
    NSLog(@"%@---开始->子线程在执行任务",[NSThread currentThread]);
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
   // [runloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    [runloop run];
    NSLog(@"%@---结束->子线程在执行任务",[NSThread currentThread]);
}

结果:子线程还是被销毁了

image.png

分析:
因为没有给Mode添加事件源(Timer/Source);

2、添加事件源,但是改变Mode

- (void)nsthread03{
    NSLog(@"%@---开启子线程",[NSThread currentThread]);
    WGThread *thread = [[WGThread alloc] initWithTarget:self selector:@selector(nsthread03Medthod) object:nil];
    thread.name = @"nsthread03";
    [thread start];
}

- (void)nsthread03Medthod{
    NSLog(@"%@---开始->子线程在执行任务",[NSThread currentThread]);
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSMachPort port] forMode:UITrackingRunLoopMode];
    [runloop run];
    NSLog(@"%@---结束->子线程在执行任务",[NSThread currentThread]);
}

结果:销毁了

image.png

分析:
虽然添加了事件源,但是是给UITrackingRunLoopMode模式添加的
但是[runloop run]是在NSDefaultRunLoopMode下运行RunLoop
也就是说在NSDefaultRunLoopMode还是没有事件源,不会成功启用RunLoop。

3、成功使用RunLoop保持线程存活,让线程在需要的时候执行任务

. 常用在某线程执行某任务的接口有:
//在主线程中响应指定Selector。这两个方法给你提供了选项来阻断当前线程(当前线程不是执行Selector的线程而是调用上述方法的线程)直到selector被执行完毕。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

//在某个子线程(NSThread对像)中响应指定Selector。这两个方法同样给你提供了选项来阻断当前线程直到Selector被执行完毕。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

//在当前线程中执行Selector,并附加了延迟选项。多个排队的Selector会按照顺序一个一个的执行。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

其实,这几个方法都是向线程中的RunLoop发送了消息,然后RunLoop接收到了消息就唤醒线程,去做对应的事情。所以想要正常使用这几个方法,响应selector的线程必须开启了RunLoop。

. 前四个接口属于source0的事件源,后两种属于Timer源
//我用weak修饰,当子线程释放的时候可以看到日志。
@property (nonatomic, weak) WGThread *myThread;

- (void)nsthread04{
    NSLog(@"%@---开启子线程",[NSThread currentThread]);
    WGThread *thread = [[WGThread alloc] initWithTarget:self selector:@selector(nsthread04Medthod) object:nil];
    self.myThread = thread;
    _myThread.name = @"nsthread04";
    [_myThread start];
}

//开启一个子线程 self.myThread,并不被销毁
- (void)nsthread04Medthod{
    NSLog(@"%@---开始->子线程在执行任务",[NSThread currentThread]);
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    NSLog(@"%@---结束->子线程在执行任务",[NSThread currentThread]);
}

//给这个子线程 self.myThread发消息,让这个子线程 self.myThread执行任务
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//断点1
    [self performSelector:@selector(nsthread04Func) onThread:self.myThread withObject:nil waitUntilDone:NO];
}

//这个是子线程 self.myThread需要执行的任务
- (void)nsthread04Func{
//断点2
    NSLog(@"当前线程:%@正在处理", [NSThread currentThread]);
}

断点1:UIEvent事件属于source0,从这里的堆栈就可以得到印证。我们在主线程中触发了touchesBegan,然后主线程的RunLoop就开始响应source0事件源,然后去调用对应的方法

image.png

断点2:performSelector也是source0依然可以从堆栈得到印证。放过断点1后调用了performSelector,然后subThread的RunLoop开始响应source0事件源,然后去调用对应的方法,所以来到了断点2

image.png

结果:self.myThread这个子线程的任务被执行了,但是行完后,这个子线程被销毁了。

image.png

分析:
销毁是因为:runMode:(NSString *)mode beforeDate:(NSDate *)limitDate这种启动RunLoop的方式有一个特性,那就是这个接口在非Timer事件触发(此处是达成了这个条件)、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出。而例子当中也没有用while把RunLoop包围起来,所以RunLoop退出后子线程完成了任务最后退出了。

. 后两种接口
- (void)nsthread05{
    NSLog(@"%@---开启子线程",[NSThread currentThread]);
    WGThread *thread = [[WGThread alloc] initWithTarget:self selector:@selector(nsthread05Medthod) object:nil];
    self.myThread = thread;
    _myThread.name = @"nsthread05";
    [_myThread start];
}

- (void)nsthread05Medthod{
    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//这么做主要是为了下一步在子线程执行afterDelay方法
    [self performSelector:@selector(nsthread04Func) onThread:self.myThread withObject:nil waitUntilDone:NO];
}

- (void)nsthread04Func{
//当前线程执行任务afterDelayTodo
    [self performSelector:@selector(afterDelayTodo) withObject:nil afterDelay:0];
}

- (void)afterDelayTodo{
//断点
    NSLog(@"当前线程:%@正在处理", [NSThread currentThread]);
}

断点:当调用 performSelecter:afterDelay: 后,其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中,所以这个方法是属于Timer源的。

image.png

结果:销毁是因为如果是PerfromSelector事件,RunLoop会退出返回。

image.png

4、 验证子线程中开启NSRunLoopCommonModes是否包含NSDefaultRunLoopMode和UITrackingRunLoopMode

. 主线程中的NSRunLoopCommonModes

NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop。
如下代码:

NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerToDo) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

但是上面代码会有一个问题,就是:在进行Scrollview的滚动操作时Timer不进行响应,滑动结束后timer又恢复正常了。
这是因为:
1.在之前讲Mode的时候提到过,RunLoop每次只能运行在一个Mode下,其意义是让不同Mode中的item互不影响。

2.NSTimer是一个Timer源(item),在上面哪个例子中不管是[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];还是scheduedTimerWith我们都是把Timer加到了主线程RunLoop的NSDefaultRunLoopMode中。一般情况下主线程RunLoop就运行在NSDefaultRunLoopMode下,所以定时器正常运行。

3.当Scrollview开始滑动时,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了UITrackingRunLoopMode。所以现在RunLoop要处理的就是UITrackingRunLoopMode中item。

4.我们的timer是添加在NSDefaultRunLoopMode中的,并没有添加到UITrackingRunLoopMode中。即我们的timer不是UITrackingRunLoopMode中的item。

5.本着不同Mode中的item互不影响的原则,RunLoop也就不会处理非当前Mode的item,所以定时器就不会响应。

6.当Scrollview滑动结束,主线程RunLoop自动切换了当前运行的Mode(currentMode),变成了NSDefaultRunLoopMode。我们的Timer是NSDefaultRunLoopMode的item,所以RunLoop会处理它,所以又正常响应了。

解决:
一个item可以被同时加入多个mode,所以可以让Timer同时成为两种Mode的item就可以了(分别添加或者直接加到commonMode中),这样不管RunLoop处于什么Mode,timer都是当前Mode的item,都会得到处理。

@interface RootViewController ()<UITextViewDelegate>

@property (nonatomic, weak) NSThread *subThread;//子线程
@property (nonatomic, weak) NSRunLoopMode runLoopMode;//想设置的RunLoop的Mode
@property (nonatomic, assign) BOOL isNeedRunLoopStop;//控制是否需要停止RunLoop
@property (nonatomic, strong) UITextView *myTextView;//

@end

- (void)mainThread{
    NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerToDo) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

-(UITextView *)myTextView{
    if (!_myTextView) {
        _myTextView = [[UITextView alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
        _myTextView.backgroundColor = [UIColor grayColor];
        _myTextView.textColor = [UIColor whiteColor];
        _myTextView.text = @"这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-";
    }
    return _myTextView;
}

结果:
当滑动文本框时timer也在执行,解决问题。同时也说明NSRunLoopCommonModes包含了UITrackingRunLoopMode和NSDefaultRunLoopMode。

. 子线程中的NSRunLoopCommonModes

- (void)buildSubThread{
    self.isNeedRunLoopStop = NO;
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    
    WGThread *thread = [[WGThread alloc] initWithTarget:self selector:@selector(subThreadToDo) object:nil];
    self.subThread = thread;
    thread.name = @"subThread";
    [thread start];
}

- (void)subThreadToDo{
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerToDo) userInfo:nil repeats:YES];
    [runLoop addTimer:timer forMode:NSRunLoopCommonModes];
    //runloop在UITrackingRunLoopMode下运行
    self.runLoopMode = UITrackingRunLoopMode;
    while (!self.isNeedRunLoopStop) {
        [runLoop runMode:self.runLoopMode beforeDate:[NSDate distantFuture]];
    }
}

结果:
没有触发timer方法。

其他不变,仅把
self.runLoopMode = UITrackingRunLoopMode;
改为
self.runLoopMode = NSDefaultRunLoopMode;

结果:

333.gif

分析:
1、timer都是添加在NSRunLoopCommonModes下的,但是当该子线程的runloop运行在UITrackingRunLoopMode时并不能触发timer方法,但是运行在UITrackingRunLoopMode时可以。

结论:主线程的NSRunLoopCommonModes默认是包含UITrackingRunLoopMode和NSDefaultRunLoopMode。而子线程的NSRunLoopCommonModes默认是只包含NSDefaultRunLoopMode的。

解决:

- (void)subThreadToDo{
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerToDo) userInfo:nil repeats:YES];
    [runLoop addTimer:timer forMode:NSRunLoopCommonModes];
    //其他代码都没变,就加了下面这一句
    CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), (CFStringRef)UITrackingRunLoopMode);
    self.runLoopMode = UITrackingRunLoopMode;
    while (!self.isNeedRunLoopStop) {
        [runLoop runMode:self.runLoopMode beforeDate:[NSDate distantFuture]];
    }
}

结果就和上图一样了,可以触发timer方法。

还有一种解决办法,根据一个场景来体现

. 场景:在子线程里可以自由切换NSDefaultRunLoopMode和UITrackingRunLoopMode(类似于主线程)当滑动时才会触发timer事件,当滑动结束停止timer事件

@interface RootViewController ()<UITextViewDelegate>

@property (nonatomic, weak) NSThread *subThread;//子线程
@property (nonatomic, weak) NSRunLoopMode runLoopMode;//想设置的RunLoop的Mode
@property (nonatomic, assign) BOOL isNeedRunLoopStop;//控制是否需要停止RunLoop
@property (nonatomic, strong) UITextView *myTextView;//

@end

@implementation RootViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"RootViewController";
    self.view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.myTextView];
    [self buildSubThread];
}

- (void)buildSubThread{
    self.myTextView.delegate = self;
    self.isNeedRunLoopStop = NO;
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    
    WGThread *thread = [[WGThread alloc] initWithTarget:self selector:@selector(subThreadToDo) object:nil];
    self.subThread = thread;
    thread.name = @"subThread";
    [thread start];
}

- (void)subThreadToDo{
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerToDo) userInfo:nil repeats:YES];
    [runLoop addTimer:timer forMode:UITrackingRunLoopMode];
    //这里已经注释下面代码了
    //CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), (CFStringRef)UITrackingRunLoopMode);
    self.runLoopMode = NSDefaultRunLoopMode;
    while (!self.isNeedRunLoopStop) {
        [runLoop runMode:self.runLoopMode beforeDate:[NSDate distantFuture]];
    }
}

- (void)timerToDo{
    NSLog(@"NSTimer方法执行--->当前Model为:%@",[[NSRunLoop currentRunLoop] currentMode]);
}

//当滑动视图滑动时runloop的mode切换成UITrackingRunLoopMode
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
    if (self.runLoopMode != UITrackingRunLoopMode) {
        [self performSelector:@selector(changeRunLoopMode:) onThread:self.subThread withObject:UITrackingRunLoopMode waitUntilDone:NO];
    }
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (!decelerate) {
        if (self.runLoopMode != NSDefaultRunLoopMode) {
            [self performSelector:@selector(changeRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO modes:@[UITrackingRunLoopMode]];
        }
    }
}

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    if (self.runLoopMode != NSDefaultRunLoopMode) {
        [self performSelector:@selector(changeRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO modes:@[UITrackingRunLoopMode]];
    }
}

- (void)changeRunLoopMode:(NSRunLoopMode)mode{
    NSLog(@"当前线程:%@ RunLoop即将将Mode改变成:%@\n", [NSThread currentThread], mode);
    self.runLoopMode = mode;
}
-(UITextView *)myTextView{
    if (!_myTextView) {
        _myTextView = [[UITextView alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
        _myTextView.backgroundColor = [UIColor grayColor];
        _myTextView.textColor = [UIColor whiteColor];
        _myTextView.text = @"这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-这是文本-";
    }
    return _myTextView;
}

结果:

333.gif

实现了当滑动滚动视图时触发timer当结束滑动时停止的效果。
同时解决了子线程NSRunLoopCommonModes中没有UITrackingRunLoopMode造成的滑动视图滑动时不能在子线程触发timer方法的问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容