RunLoop基础知识
作用
- 保持程序的持续运行
- 处理APP中的各种事件(比如触摸事件、定时器事件、Selector事件)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息 (用户态 切换到 内核态)
RunLoop与多线程的关系
- 线程与RunLoop是一一对应的关系;RunLoop保存在一个全局的NSDictionary字典里面,线程为key,RunLoop为Value
- 主线程的RunLoop在Main函数中自动开启,保证了程序的持续运行。
子线程的RunLoop需要主动创建;RunLoop在第一次获取时由系统内部创建,在线程结束时销毁。(苹果不允许直接创建 RunLoop) - 只能在一个线程的内部获取其 RunLoop(主线程除外),它是寄生于线程的
参考
RunLoop 有五种运行模式,其中常用的有1、2两种
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
- UIInitializationRunLoopMode: 启动Mode,在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes模式
一种模式组合,在主线程默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程只包NSDefaultRunLoopMode。
注意:
①在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。
②在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功。
③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合。
RunLoop对象.png每次RunLoop启动时,只能指定其中一个 Mode;如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入;这样做主要是为了分隔开不同组的 事件源,让其互不影响
source就是输入源事件,Timer即为定时源事件,Observer相当于消息循环中的一个监听器
Observer的创建
- NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
上面的 Source/Timer/Observer 被统称为 mode item,一个item可以被同时加入多个mode。
但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item 都没有(只有Observer也不行),则 RunLoop 会直接退出,不进入循环。
RunLoop正常运行的条件是:
- 有Mode。
- Mode有事件源。
- 运行在有事件源的Mode下
经过NSRunLoop封装后,只可以往mode中添加两类事件源:NSPort(对应的是source1)和NSTimer(Timer源放在后面讲)。
启动RunLoop有那些方法及区别
NSRunLoop总共包装了3个方法供我们使用
- (void) run
不建议使用。 除非希望子线程永远存在,因为这个会导致Run Loop永久性的运行在NSDefaultRunLoopMode模式,即使使用 CFRunLoopStop(runloopRef);也无法停止RunLoop的运行,那么这个子线程也就无法销毁,只能永久运行下去
- (void)runUntilDate:(NSDate *)limitDate
//举例代码
while (!Stop){
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
比上面的接口好点,有个超时时间,可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行RunLoop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef);仍然无法停止RunLoop的运行,因此最好自己设置一个合理的RunLoop运行时间。
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate
有一个超时时间限制,而且可以设置运行模式
这个接口在非Timer事件触发、显式的用CFRunLoopStop停止RunLoop或者到达limitDate后会退出返回。如果仅是Timer事件触发并不会让RunLoop退出返回,但是如果是PerfromSelector事件或者其他Input Source事件触发处理后,RunLoop会退出返回YES。同样可以像上面那样用while包起来使用
关于GCD
RunLoop 底层也会用到 GCD 的东西,但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
PerformSelecter
performSelector相关的知识.png runLoop调用run方法的内部实现.png子线程保活
子线程保活.png@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
}];
[self.thread start];
}
// 用于停止子线程的RunLoop
- (void)stopThread
{
// 设置标记为YES
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 清空线程
self.thread = nil;
}
自动释放池
Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大。
那么什么时候释放,怎么释放呢?
主线程的RunLoop默认启动,并会自动创建自动释放池。当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。
子线程需要在线程中手动添加自动释放池
NSThread和NSOperationQueue开辟子线程需要手动创建autoreleasepool。GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool
参考
RunLoop的应用
- NSTimer 用于轮播图
- TableView滚动时不显示图片
- TableView停止滚动时计算行高或者预加载
sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。 - 怎样保证子线程数据回来更新UI的时候,不打断用户的滑动操作。
- tableView在滑动,处于UITrackingRunloopMode模式下。
- 子线程请求的数据,那么在和主线程处理的时候,我们将更新的逻辑加载defaultMode下。那么defaultMode下的操作是不会执行的。
- 滑动结束了,runloop由UITrackingRunloopMode又回到defaultMode下了,那么defaultMode下的更新操作就能执行了