RunLoop 是什么
RunLoop 是一个事件处理的循环,目的是有工作要做时让线程忙碌,没有工作要做时让线程进入睡眠状态。
Cocoa 和 Core Foundation 两个层面都提供了 RunLoop 的对象,即 NSRunLoop 和 CFRunLoop。
RunLoop 与线程是一对一的关系。不能手动创建 RunLoop,只能 [NSRunLoop currentRunLoop] 或 CFRunLoopGetCurrent(),调用这两个方法时当没有 RunLoop 会自动创建一个并返回。
模式
我们常用到的三种模式:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode):默认模式。
- UITrackingRunLoopMode:当拖动 UIScrollView 触发的模式。
- NSRunLoopCommonModes (kCFRunLoopCommonModes) 占位模式,相当于前面两种模式都添加进去了。
一个 Runloop 可以有多个模式, Runloop 执行其中一种模式的时候,另外的模式不会执行。
两种 Sources
Input Sources 和 Timer Sources。
Sources 的作用是使 RunLoop 从睡眠状态转化到工作状态。
Input Sources 是指 Port-Based Sources、Custom Input Sources、Cocoa Perform Selector Sources。
Port-Based Sources 的类有:
NSPort
NSMachPort
...
Custom Input Sources 通常用到 CFRunLoopSourceRef
Cocoa Perform Selector Sources 有以下的方法:
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
Timer Sources 就是指 NSTimer。
Observers
可以添加观察者去观察 RunLoop 的状态的变化,状态有以下几种
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
应用例子
子线程里创建一个定时执行的跑圈
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static int count = 0;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"执行定时器任务");
count ++;
if (count >= 5) {
[timer invalidate];
}
}];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"runLoop 开启");
[runLoop run];
NSLog(@"runLoop 退出");
});
运行结果:
runLoop 开启
执行定时器任务
执行定时器任务
执行定时器任务
执行定时器任务
执行定时器任务
runLoop 退出
注意当 Timer 不失效,那么 RunLoop 永远不会退出。
主线程中使用 [NSTimer scheduledTimerWith...] 并不需要 RunLoop 去 addTimer 是因为此方法默认在主线程的 RunLoop 添加该 Timer。
子线程里创建一个监听 port 的跑圈
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
self.port = [NSPort port];
self.port.delegate = self;
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:self.port forMode:NSDefaultRunLoopMode];
NSLog(@"开始");
[runLoop run];
NSLog(@"结束");
});
}
- (void)handlePortMessage:(NSPortMessage *)message {
NSLog(@"handlePortMessage:%@", message);
[[NSRunLoop currentRunLoop] removePort:self.port forMode:NSDefaultRunLoopMode];
}
- (IBAction)clickSendButton:(id)sender {
[self.port sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];
}
移除 Port 即可退出 RunLoop。
解决 Timer 添加到主线程时的问题
因为 Timer 默认是以 NSDefaultRunLoopMode 添加到 RunLoop 的,当拖动 UIScrollView 时,切换到了 UITrackingRunLoopMode ,所以会导致Timer 的任务被延迟执行了,解决这个问题可以把 Timer 添加到 NSRunLoopCommonModes 模式。但是可能会引起另外一个问题,如果 Timer 中的任务比较耗时,会影响拖动 UIScrollView 的效果,这时可以把 Timer 在子线程中运行。
把加载多张图片的耗时操作分散到多次循环中
之前发现一个利用 RunLoop 优化的开源代码 RunLoopWorkDistribution,运行效果自行查看开源项目 https://github.com/diwu/RunLoopWorkDistribution 。
没优化前的主要问题是:多张图片的加载任务会是需要在一次循环中执行完的,所以会比较耗时,造成界面的卡顿。解决技巧是把每一张图片分散在每一次循环中,用一个任务队列依次加载图片。为了触发多次循环,添加一个 Timer 去触发。
检测卡顿
添加一个观察者,观察 RunLoop 的 处理事件前的状态和睡眠前的状态,两者之间的时间差达到一定值,并且连续五次以上,就判定为卡顿。
参考开源代码 https://github.com/suifengqjn/PerformanceMonitor
处理一些交互画面问题
有时会发生一些奇怪的交互画面问题,原因是两个 UI 更新都在 RunLoop 的同一个循环里了,解决方法是把其中一个放到下一个循环当中。最便捷的就是调用 performSelector:withObject:afterDelay:
还有其他应用例子待添加。
网友评论