谨以此篇总结归纳记录iOS中一大知识点 RunLoop 的相关知识,以作备忘。
iOS中,提供了两种runloop:NSRunLoop 和 CFRunLoopRef
- CFRunLoopRef : 是CoreFoundation框架内的纯C函数的API,所有API都是线程安全的;
- NSRunLoop : 是基于CFRunLoopRef的封装,提供了面向对象的API,但这些API不是线程安全的;
RunLoop与线程的关系
- runloop寄生于线程。RunLoop是保证线程不会退出,并且能在不处理消息的时候让线程休眠,节约资源,在接收到消息的时候唤醒线程做出对应处理的消息循环机制。
runloop的创建
苹果不允许直接创建 RunLoop,它只提供了四个自动获取的函数
[NSRunLoop currentRunLoop];//获取当前线程的RunLoop
[NSRunLoop mainRunLoop];
CFRunLoopGetMain();
CFRunLoopGetCurrent();
这些函数内部的大概逻辑(注:这并不是源码,而是大神为了方便我们理解,对源码进行了一些可读性优化后的结果):
/// 全局的Dictionary,key 是 线程, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
通过看这部分代码我们可以产生一个概念,那就是,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
RunLoop的结构
- 图中蓝色部分表示RunLoop整体;
- 图中绿色部分表示RunLoop中的mode,一个RunLoop中可以包含多个mode;
- 图中黄色部分表示mode下对应的各类任务或者说动作;
RunLoop就像一个工厂的车间,mode是车间里的不同生产线,而该车间同时只能开启一条生产线;mode下的动作就像这条生产线上的各个工序,工序必须存在,否则开启生产线就没有意义:所以如果runloop中没有加入任务,RunLoop就会退出,从而对应的线程就会销毁;而mode的存在是为了让RunLoop在不同的模式下执行不同的任务时互不影响:因为RunLoop同时只能在一种mode下运行。
-
常用的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组合。
-
mode的item:源事件
- source输入源事件:
1.source0:诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作。
2.source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。
一般来说日常开发中我们需要关注的是source0,source1只需要了解。
之所以说source0更重要是因为日常开发中,我们需要对常驻线程进行操作的事件大多都是source0,稍后的实验会讲到。
- Timer定时源事件:
Timer即为定时源事件。通俗来讲就是我们很熟悉的NSTimer,其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoo
p,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会
延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。
- observer:
消息循环中的监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建。注:它不能作为让RunLoop正常运行的条件,只有Observer的RunLoop也是无法正常运行的。
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
上面的 Source/Timer/Observer 被统称为 mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item 都没有(只有Observer也不行),则 RunLoop 会直接退出,不进入循环。
-
runLoop的内部逻辑
总结:runLoop正常运行的条件:1.有mode;2.mode有事件源(非observer);3.运行在有事件源的mode下.
RunLoop应用
一.线程保活
在平时开发中,遇到一些耗时操作往往是要开辟子线程去处理,当处理结束,子线程自动退出:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@---开辟子线程",[NSThread currentThread]);
MyThread *thread1 = [[MyThread alloc]initWithTarget:self selector:@selector(thread1Method) object:nil];
thread1.name = @"thread-1";
[thread1 start];
}
- (void)thread1Method {
NSLog(@"%@---执行子线程任务",[NSThread currentThread]);
}
执行结果:
2018-09-06 16:53:26.868150+0800 RunLoop[19234:963726] {number = 1, name = main}---开辟子线程
2018-09-06 16:53:26.868683+0800 RunLoop[19234:963785] {number = 3, name = thread-1}---执行子线程任务
2018-09-06 16:53:26.869586+0800 RunLoop[19234:963785] thread-1---线程被释放了
如果我们像其他变量一样使用强引用将该线程保持起来,试试是否可以重复使用该线程:
@interface ViewController ()
@property (nonatomic, strong) MyThread *myThread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@---开辟子线程",[NSThread currentThread]);
MyThread *thread1 = [[MyThread alloc]initWithTarget:self selector:@selector(thread1Method) object:nil];
thread1.name = @"thread-1";
self.myThread = thread1;
[self.myThread start];
//重复使用该线程
[self.myThread start];
}
- (void)thread1Method {
NSLog(@"%@---执行子线程任务",[NSThread currentThread]);
}
执行结果:
2018-09-06 17:24:28.797712+0800 RunLoop[19532:983497] {number = 1, name = main}---开辟子线程
2018-09-06 17:24:28.798080+0800 RunLoop[19532:983542] {number = 3, name = thread-1}---执行子线程任务
2018-09-06 17:24:28.815303+0800 RunLoop[19532:983497] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[MyThread start]: attempt to start the thread again'
可以看到,程序崩溃了,虽然持有了线程以后,该对象没有被释放,但是已经不能被重复使用,因为这个线程执行完任务以后已经死亡,不允许重新开启。
由上可知,想要让线程不死亡,就必须让他有任务一直在运行,而想要重复利用该线程,就要让他在有任务是进行处理,而没任务是进入类似休眠等待状态,而要达到这种目的,就要开启线程的runloop。
- 以下方法用于在某个线程内相应某个方法:
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在主线程中响应指定Selector。这两个方法给你提供了选项来阻断当前线程(不是执行Selector的线程而是调用上述方法的线程)直到selector被执行完毕。
属于source0事件
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在某个子线程(NSThread对像)中响应指定Selector。这两个方法同样给你提供了选项来阻断当前线程直到Selector被执行完毕。
属于source0事件
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在当前线程中执行Selector,并附加了延迟选项。多个排队的Selector会按照顺序一个一个的执行
属于Timer事件
其实,这几个方法都是向线程中的RunLoop发送了消息,然后RunLoop接收到了消息就唤醒线程,去做对应的事情。所以想要正常使用这几个方法,响应selector的线程必须开启了RunLoop。
- 接下来尝试在子线程中开启runloop:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@---开辟子线程",[NSThread currentThread]);
MyThread *thread1 = [[MyThread alloc]initWithTarget:self selector:@selector(thread1Method) object:nil];
thread1.name = @"thread-1";
// self.myThread = thread1;
[thread1 start];
// [self.myThread start];
//重复使用该线程
// [self.myThread start];
}
- (void)thread1Method {
NSLog(@"%@---执行子线程任务",[NSThread currentThread]);
//获取当前线程的runloop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//向runloop中添加任务,如果不添加,runloop会直接退出循环
[runloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
//启动runloop,runloop默认运行在NSDefaultRunLoopMode模式下,所以上边的任务要添加到default模式下或者common模式下,否则RunLoop仍然会退出
[runloop run];
NSLog(@"%@---执行子线程任务结束",[NSThread currentThread]);
}
运行结果:
2018-09-08 16:17:57.952431+0800 RunLoop[43303:1820366] {number = 1, name = main}---开辟子线程
2018-09-08 16:17:57.953460+0800 RunLoop[43303:1820728] {number = 3, name = thread-1}---执行子线程任务
RunLoop本质就是个Event Loop的do while循环,所以运行到 [runloop run]这一行以后子线程就一直在进行接受消息->等待->处理的循环。所以不会运行[runLoop run]之后的代码,也就不会因为任务结束导致线程死亡进而销毁。
- runLoop启动方法:
1.- (void)run;
这个接口会导致Run Loop永久性的运行NSDefaultRunLoopMode模式,即使使用 CFRunLoopStop(runloopRef);也无法停止RunLoop的运行,那么这个子
线程也就无法停止,只能永久运行下去。
2.- (void)runUntilDate:(NSDate *)limitDate;
可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行,因此最
好自己设置一个合理的RunLoop运行时间。
3.- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
有一个超时时间限制,而且可以设置运行模式这个接口在非Timer事件触发、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出返回。如果仅
是Timer事件触发并不会让RunLoop退出返回,但是如果是PerfromSelector事件或者其他Input Source事件触发处理后,RunLoop会退出返回YES。
- 让线程响应消息
@interface ViewController ()
@property (nonatomic, weak) MyThread *myThread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@---开辟子线程",[NSThread currentThread]);
MyThread *thread1 = [[MyThread alloc]initWithTarget:self selector:@selector(thread1Method) object:nil];
thread1.name = @"thread-1";
self.myThread = thread1;
[thread1 start];
// [self.myThread start];
//重复使用该线程
// [self.myThread start];
}
- (void)thread1Method {
NSLog(@"%@---执行子线程任务",[NSThread currentThread]);
//获取当前线程的runloop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//向runloop中添加任务,如果不添加,runloop会直接退出循环
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
//启动runloop
[runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"%@---执行子线程任务结束",[NSThread currentThread]);
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
//在子线程中执行方法
[self performSelector:@selector(dealMessage) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
//子线程响应的消息
- (void)dealMessage {
NSLog(@"%@---响应消息",[NSThread currentThread]);
}
@end
运行结果:
2018-09-17 18:31:57.110042+0800 RunLoop[12719:4850481] {number = 1, name = main}---开辟子线程
2018-09-17 18:31:57.110862+0800 RunLoop[12719:4850754] {number = 4, name = thread-1}---执行子线程任务
2018-09-17 18:32:11.045773+0800 RunLoop[12719:4850754] {number = 4, name = thread-1}---响应消息
2018-09-17 18:32:11.046215+0800 RunLoop[12719:4850754] {number = 4, name = thread-1}---执行子线程任务结束
2018-09-17 18:32:11.047035+0800 RunLoop[12719:4850754] thread-1---线程被释放了
最后子线程任务结束然后被释放是因为之前提到的,runMode:(NSString *)mode beforeDate:(NSDate *)limitDate这种启动RunLoop的方式有一个特性,那就是这个接口在非Timer事件触发(此处是达成了这个条件)、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出。而例子当中也没有用while把RunLoop包围起来,所以RunLoop退出后子线程完成了任务最后退出了。
- 主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起:
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
CFRunloop is calling out to an abserver callback function
用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation
CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
CFRunloop is calling out to a block
消息通知、非延迟的perform、dispatch调用、block回调、KVO
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
CFRunloop is servicing the main desipatch queue
CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
CFRunloop is calling out to a timer callback function
延迟的perform, 延迟dispatch调用
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
CFRunloop is calling out to a source 0 perform function
处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
CFRunloop is calling out to a source 1 perform function
由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
二.让线程执行定时任务:Timer
- Timer创建方法
+ (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以后新加的方法
timer创建后要正常启动必须加入runloop
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(wantTodo) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
使用scheduledTimer...方法创建的Timer默认加入到了当前线程的runloop的NSDefaultRunLoopMode下,所以创建后可以直接运行。
- 使用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会处理它,所以又正常响应了。
7.如果想Timer在两种Mode中都得到响应怎么办?前面提到过,一个item可以被同时加入多个mode。让Timer同时成为两种Mode的item就可以了(分别添加或者直接加到commonMode中),这样不管RunLoop处于什么Mode,timer都是当前Mode的item,都会得到处理。
三.监听RunLoop observer
//创建监听者
/*
第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
第三个参数 Boolean repeats:YES:持续监听 NO:不持续
第四个参数 CFIndex order:优先级,一般填0即可
第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
*/
/*
所有事件
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 给RunLoop添加监听者
/*
第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
第二个参数 CFRunLoopObserverRef observer 监听者
第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的内存管理(Core Foundation)
凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
*/
CFRelease(observer);
关于RunLoop的释放问题(RunLoop带autoreleasepool的正确写法)
-
分析 :Timer和Source以及一些回调block等等,都需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。
在主线程中:
- 当RunLoop开启时,会自动创建一个自动释放池;
- 当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池;
- 当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中;
- 重复2-3;
- 当 RunLoop 退出时,会释放低掉自动释放池的对象;
在子线程中:
RunLoop是需要我们手动获取的,就更不可能自动创建了autoreleasepool(自动管理对象的释放)。所以子线程的autoreleasepool需要我们手动创建。
-
结论:NSThread和NSOperationQueue开辟子线程需要手动创建autoreleasepool。GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool。
写法:
- 需要用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]];
}
}
- 不需要用while循环控制的RunLoop
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
//[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
子线程 RunLoop 切换 mode 实现
#import "ViewController.h"
#import "MyThread.h"
@interface ViewController ()
@property (nonatomic, weak)NSThread *subThread;//子线程
@property (nonatomic, weak)NSRunLoopMode runLoopMode;//想设置的RunLoop的Mode
@property (nonatomic, assign)BOOL isNeedRunLoopStop;//控制是否需要停止RunLoop
@property (weak, nonatomic) IBOutlet UITextView *myTextView;//只要是Scrollview及其子类都行
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myTextView.delegate = self;
self.isNeedRunLoopStop = NO;
NSLog(@"%@----开辟子线程",[NSThread currentThread]);
NSThread *tmpThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];
self.subThread = tmpThread;
self.subThread.name = @"subThread";
[self.subThread start];
}
- (void)subThreadTodo
{
NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
@autoreleasepool{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//监听 runloop
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
//NSDefaultRunLoopMode下暂时什么都不干,只是为了让RunLoop能在该模式下运行添加了一个source1
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerTodo) userInfo:nil repeats:YES];
//给UITrackingRunLoopMode添加一个timer,为了等下切换到该模式的时候能看到效果。
//提示:子线程RunLoop如果不给UITrackingRunLoopMode添加item就没有这个Mode,可以看前面初体验疑问的截图。
//但是,NSDefaultRunLoopMode是无论如何都存在的,就算你不给他添加item,他也只是内容为空而已。
[runLoop addTimer:timer forMode:UITrackingRunLoopMode];
self.runLoopMode = NSDefaultRunLoopMode;
//子线程 runloop 中默认只有NSDefaultRunLoopMode
CFRunLoopAddCommonMode(CFRunLoopGetCurrent(), (CFStringRef)UITrackingRunLoopMode);
while (!self.isNeedRunLoopStop) {//用while来控制RunLoop的运行与否
//让RunLoop在我们希望的Mode下运行
[runLoop runMode:self.runLoopMode beforeDate:[NSDate distantFuture]];
}
}
}
- (void)changeSubThreadRunLoopMode:(NSRunLoopMode)mode{
//改变我们希望RunLoop运行的Mode的方法
//到时候用[performSelector:onThread:withObject:waitUntilDone:]来调用
//结合[runMode:beforeDate:]触发非Timer的事件源会退出RunLoop的特性
//再结合上面While的写法,就退出了之前的RunLoop并让RunLoop以我们希望的Mode重新Run。
NSLog(@"当前线程:%@ RunLoop即将将Mode改变成:%@\n", [NSThread currentThread], mode);
self.runLoopMode = mode;
}
- (void)timerTodo{
//上面的Timer执行的函数,只是为了等下切换的mode后有打印好观察。
NSLog(@"Timer启动啦,当前RunLoopMode:%@\n", [[NSRunLoop currentRunLoop] currentMode]);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (self.runLoopMode != UITrackingRunLoopMode) {
//如果有滑动事件,并且RunLoop的Mode不为UITrackingRunLoopMode
//就改变Mode并退出当前RunLoop然后让RunLoop以更改后的Mode重新Run
//加if是为了避免重复操作,切换RunLoopMode只需要一次
[self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:UITrackingRunLoopMode waitUntilDone:NO];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
//拖拽结束会调用这个方法,如果还有拖拽后的滑动动画就不做操作
if (!decelerate) {
//如果没有后续动画了就切换Mode为NSDefaultRunLoopMode
if (self.runLoopMode != NSDefaultRunLoopMode) {
//断点1
[self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO];
}
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
//拖拽后的后续滑动动画结束(如果有才会到这,没有就不会到这个函数里面)
//也就是说上面那个函数如果切换了Mode就不会走这里,否则说明需要在这里切换Mode
//切换Mode为NSDefaultRunLoopMode
if (self.runLoopMode != NSDefaultRunLoopMode) {
[self performSelector:@selector(changeSubThreadRunLoopMode:) onThread:self.subThread withObject:NSDefaultRunLoopMode waitUntilDone:NO];
}
}
@end
启动
2018-10-15 18:43:52.564292+0800 RunLoop[12154:3478050] {number = 1, name = main}----开辟子线程
2018-10-15 18:43:52.565429+0800 RunLoop[12154:3478096] {number = 3, name = subThread}----开始执行子线程任务
2018-10-15 18:43:52.566281+0800 RunLoop[12154:3478096] RunLoop进入
2018-10-15 18:43:52.566408+0800 RunLoop[12154:3478096] RunLoop要处理Timers了
2018-10-15 18:43:52.566530+0800 RunLoop[12154:3478096] RunLoop要处理Sources了
2018-10-15 18:43:52.567887+0800 RunLoop[12154:3478096] RunLoop要休息了
拖拽
2018-10-15 18:45:02.697843+0800 RunLoop[12154:3478096] RunLoop醒来了
2018-10-15 18:45:02.698191+0800 RunLoop[12154:3478096] RunLoop要处理Timers了
2018-10-15 18:45:02.698670+0800 RunLoop[12154:3478096] RunLoop要处理Sources了
2018-10-15 18:45:02.699404+0800 RunLoop[12154:3478096] 当前线程:
{number = 3, name = subThread} RunLoop即将将Mode改变成:UITrackingRunLoopMode
2018-10-15 18:45:02.700091+0800 RunLoop[12154:3478096] RunLoop退出了
2018-10-15 18:45:02.700535+0800 RunLoop[12154:3478096] Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode
2018-10-15 18:45:04.566206+0800 RunLoop[12154:3478096] Timer启动啦,当前RunLoopMode:UITrackingRunLoopMode
放手
2018-10-15 18:45:08.891239+0800 RunLoop[12154:3478096] 当前线程:
{number = 3, name = subThread} RunLoop即将将Mode改变成:kCFRunLoopDefaultMode
2018-10-15 18:45:08.893010+0800 RunLoop[12154:3478096] RunLoop进入
2018-10-15 18:45:08.893403+0800 RunLoop[12154:3478096] RunLoop要处理Timers了
2018-10-15 18:45:08.893711+0800 RunLoop[12154:3478096] RunLoop要处理Sources了
2018-10-15 18:45:08.894009+0800 RunLoop[12154:3478096] RunLoop要休息了
参考文档
- iOS RunLoop入门小结
- runLoop