runloop

1.不开启RunLoop的线程
在遇到一些耗时操作时,为了避免主线程阻塞导致界面卡顿,影响用户体验,往往我们会把这些耗时操作放在一个临时开辟的子线程中。操作完成了,子线程线性的执行了代码也就退出了,就像下面一样。
其中MyThread为一个重写了dealloc的NSThread的子类
-(void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@----开辟子线程",[NSThread currentThread]);
self.subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];
self.subThread.name = @"subThread";
[self.subThread start];
// Do any additional setup after loading the view from its nib.
}
-(void)subThreadTodo
{
NSLog(@"%@----执行子线程任务",[NSThread currentThread]);
}
{number = 1, name = main}----开辟子线程
2018-05-16 15:00:32.842971+0800 demo[4624:273971] {number = 3, name = subThread}----执行子线程任务
2.开启RunLoop的线程
如果这个操作需要频繁执行,那么按照上面那样的逻辑,我们就需要频繁创建子线程,这是很消耗资源的。我们在设计类的时候会把需要频繁使用的对象保持起来,而不是频繁创建一样。我们试试把线程“保持”起来,让它在需要的时候执行任务。
这里没有对线程进行引用,也没有让线程内部的任务进行显式的循环。为什么子线程的里面的任务没有执行到输出任务结束这一步,为什么子线程没有销毁?就是因为[runLoop run];这一行的存在。
RunLoop本质就是个Event Loop的do while循环,所以运行到这一行以后子线程就一直在进行接受消息->等待->处理的循环。所以不会运行[runLoop run];之后的代码(这点需要注意,在使用RunLoop的时候如果要进行一些数据处理之类的要放在这个函数之前否则写的代码不会被执行),也就不会因为任务结束导致线程死亡进而销毁。这也就是我们最常使用RunLoop的场景之一,就如小节标题保持线程的存活,而不是线性的执行完任务就退出了。
-(void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@----开辟子线程",[NSThread currentThread]);
self.subThread = [[MyThread alloc] initWithTarget:self selector:@selector(runloopSubThreadTodo) object:nil];
self.subThread.name = @"subThread";
[self.subThread start];
// Do any additional setup after loading the view from its nib.
}
-(void)runloopSubThreadTodo
{
NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
//获取当前子线程的RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//下面这一行必须加,否则RunLoop无法正常启用。我们暂时先不管这一行的意思,稍后再讲。
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
//让RunLoop跑起来
[runLoop run];
NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
}
{number = 1, name = main}----开辟子线程
2018-05-16 15:16:42.493707+0800 demo[4894:293247] {number = 3, name = subThread}----开始执行子线程任务

RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。它是寄生于线程的,所以提到RunLoop必然会涉及到线程。
3创建RunLoop
苹果不允许直接创建 RunLoop,它只提供了四个自动获取的函数
[NSRunLoop currentRunLoop];//获取当前线程的RunLoop
[NSRunLoop mainRunLoop];
CFRunLoopGetMain();
CFRunLoopGetCurrent();
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)


runloop_第1张图片
image.png

现在我们结合刚才的打印结果来理解。

图中RunLoop蓝色部分就对应我们打印结果中,整个RunLoop部分的打印结果
多个绿色部分共同被包含在RunLoop内就对应,打印结果中modes中同时包含多个Mode(这里可是看打印结果中标注出来的第一行往上再数两行。modes = ... count = 1。一个RunLoop可以包含多个Mode,每个Mode的Name不一样,只是在这个打印结果当中目前刚好Mode个数为1)
每一个绿色部分Mode整体就对应,打印结果中被标注出来的整体。
黄色部分Source对应标注部分source0+source1
黄色部分Observer对应标注部分observer部分
黄色部分Timer对应标注部分timers部分
RunLoop在kCFRunLoopDefaultMode下run了,但是因为该Mode下所有东西都为null(不包含任何内容),所以RunLoop什么都没做又退出来了,然后线程就结束任务最后销毁。之所以要有Mode的存在是为了让RunLoop在不同的”行为模式“之下执行不同的”动作“互不影响。RunLoop同一时间只能运行在一种Mode下,当前运行的这个Mode叫currentMode。
一般我们常用的Mode有三种

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时不可以填这种模式否则会导致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为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。
observer它相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
上面的 Source/Timer/Observer 被统称为 mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item 都没有(只有Observer也不行),则 RunLoop 会直接退出,不进入循环。
RunLoop正常运行的条件是:1.有Mode。2.Mode有事件源。3.运行在有事件源的Mode下

还有那些方法启动RunLoop?

NSRunLoop中总共包装了3个方法供我们使用
1.- (void)run;
除非希望子线程永远存在,否则不建议使用,因为这个接口会导致Run Loop永久性的运行NSDefaultRunLoopMode模式,即使使用 CFRunLoopStop(runloopRef);也无法停止RunLoop的运行,那么这个子线程也就无法停止,只能永久运行下去。
2.- (void)runUntilDate:(NSDate *)limitDate;
比上面的接口好点,有个超时时间,可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行RunLoop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行,因此最好自己设置一个合理的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会退出返回YES。同样可以像上面那样用while包起来使用。
结论
①.RunLoop是寄生于线程的消息循环机制,它能保证线程存活,而不是线性执行完任务就消亡。

②.RunLoop与线程是一一对应的,每个线程只有唯一与之对应的一个RunLoop。我们不能创建RunLoop,只能在当前线程当中获取线程对应的RunLoop(主线程RunLoop除外)。

③.子线程默认没有RunLoop,需要我们去主动开启,但是主线程是自动开启了RunLoop的。

④.RunLoop想要正常启用需要运行在添加了事件源的Mode下。

⑤.RunLoop有三种启动方式run、runUntilDate:(NSDate *)limitDate、runMode:(NSString *)mode beforeDate:(NSDate *)limitDate。第一种无条件永远运行RunLoop并且无法停止,线程永远存在。第二种会在时间到后退出RunLoop,同样无法主动停止RunLoop。前两种都是在NSDefaultRunLoopMode模式下运行。第三种可以选定运行模式,并且在时间到后或者触发了非Timer的事件后退出。

3 要把Timer加到一个叫RunLoop的东西里面才能正常运行

  • (void)viewDidLoad {
    [super viewDidLoad];

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(wantTodo) userInfo:nil repeats:YES];
    //timerWith开头的方法创建的Timer如果不加下面一句无法运行。
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    注意:GCD的timer与NStimer不是一个东西。他俩中只有NSTimer是与RunLoop相关的
    scheduedTimerWith开头的方法创建的NSTimer就不需要添加到RunLoop中就可以运行。事实上,这一系列方法的真实逻辑是,创建一个定时器并自动添加到当前线程RunLoop的NSDefaultRunLoopMode中。在声明一次,不添加到RunLoop中的NSTimer是无法正常工作的

  • (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
  • (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
  • (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法
  • (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
  • (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
  • (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法
    在进行Scrollview的滚动操作时Timer不进行响应,滑动结束后timer又恢复正常了。现在对RunLoop有一定了解了,我们不妨来分析一下以便加深理解。

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会处理它,所以又正常响应了。
如果想Timer在两种Mode中都得到响应怎么办?前面提到过,一个item可以被同时加入多个mode。让Timer同时成为两种Mode的item就可以了(分别添加或者直接加到commonMode中),这样不管RunLoop处于什么Mode,timer都是当前Mode的item,都会得到处理。

当tableview的cell上有需要从网络获取的图片的时候,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,有可能会造成卡顿。可以让设置图片的任务在CFRunLoopDefaultMode下进行,当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 下进行,不去设置图片,而是当停止的时候,再去设置图片。(这个场景的核心还是利用不同Mode的切换的思想,可以拓展其他地方)
[self.myImageView performSelector:@selector(setImage:)
withObject:[UIImage imageNamed:@""]
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
当然不是说这种方法就一定好,毕竟他在滑动的时候不会显示图片,万一你的需求跟这刚好相反呢,而且现在SDWebImage处理的已经很好了,已经很少有人用这种方法了。但是这个利用Mode切换的思想可以借鉴,万一其他地方用上就很合适呢。

4.Timer和Source以及一些回调block等等,都需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。

在主线程中

当RunLoop开启时,会自动创建一个自动释放池。
当RunLoop在休息之前会释放掉自动释放池的东西。
然后重新创建一个新的空的自动释放池。
当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中。
重复2-4。
所以主线程中,有关RunLoop的释放问题不需要我们关心。

注意:这里说的是主线程(关于子线程的autoreleasepool是否需要手动创建还有个研究过程,因为网上众说纷纭,有的说不需要创建有的说需要。)这部分的资料也比较少,总结了有限的资料加上自己的一些理解我认为RunLoop正确的写法
1)需要用while循环控制的RunLoop
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isNeedStopRunLoop) {
//这里RunLoop不需要添加autoreleasepool
//每个RunLoop内部都会自动管理autoreleasepool
//事件源等一些autorelease对象会在RunLoop的迭代中自动释放。
[runLoop runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
//[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
2) 不需要用while循环控制的RunLoop
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
//[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
http://www.cocoachina.com/ios/20180515/23380.html

你可能感兴趣的:(runloop)