iOS_RunLoop介绍+使用示例
一丶RunLoop 介绍
官方文档:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
看任何文档,都不如看官方文档
简介:
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
大概意思:
Run loops 是与线程相关的基础架构的一部分,是一个事件处理的循环,用于工作调度并处理接收事件,Run loop 的可以使线程有工作需要做时可以忙碌起来,而当没有事可做时,又可以使得线程睡眠。
其他:
1.Run Loop 管理不是完全自动的;,设计线程的时候,适当的时候启动和响应传入事件;
2.每个线程都有一个关联的Run Loop 除了主线程默认开启,分线程需要手动运行;采用懒加载的方式进行加载;
1.1 Run Loop 结构:
Run Loop 接收2种不同类型的sources事件
1.1.1.输入源:
异步传递事件,通常是来自不同的线程或不同的应用的消息。输入源异步传递事件到对应的处理程序和在线程关联的NSRunLoop对象调起runUntilDate:方法来退出事件处理。
1.1.2.timer源:
同步地传递事件,发生在每个定时器调用或周期性地调用。Timer源传递事件到他们的处理程序,但是不会调用run loop来退出处理。
运行循环及其源的结构图
Paste_Image.png
1.2 Run Loop Modes
Run Loop模式是一个监视输入源和定时器的集合和注册成为run loop的观察者的集合。每次要运行run loop,都需要显示或隐式地指定某种运行的mode。只有与这种指定的mode关联的源才会被监视和允许传递他们的事件,同样地,只有与这种模式关联的观察者都会收到run loop行为变化的通知。与其它模式想关联的源,直到随后在合适的模式通过循环后,都会接收到新的事件
我们通过名称来唯一标识mode。在Cocoa和Core Foundation中都定义了default模式和几个常用的模式,都是通过字符串名称来指定。我们也可以自定义模式,但是我们需要手动添加至少一个input source/timers/observers。
通过使用mode来过滤掉我们不希望接收到来自不想要的通过run loop的源。大部分情况下,我们都是使用系统定义的default模式。对于子线程,我们可以使用自定义模式在关键性操作时阻止低优先级的源传递事件。
Paste_Image.png二丶什么时候使用Run Loop
2.1什么时候应该使用run loop呢?
只有当我们需要创建子线程的时候,才会需要到显示地运行run loop。应用程序的主线程的run loop是应用启动的基础任务,在启动时就会自动启动run loop。所以我们不需要手动启动主线程的run loop。
对于子线程,我们需要确定线程是否需要run loop,如果需要,则配置它并启动它。我们并不问题需要启动run loop的。比如说,如果我们开一个子线程去执行一些长时间的和预先决定的任务,我们可能不需要启动run loop。Run loop是用于那么需要在线程中有更多地交互的场景。比如说,我们会在下面的任何一种场景中需要开启run loop:
1.使用端口源或者自定义输入源与其它线程通信
2.在线程中使用定时器
3.使用Cocoa中的任何performSelector…方法
4.保持线程来执行周期性的任务
2.2 使用Run Loop 对象
2.2.1
Run Loop对象给添加输入源、定时器和观察者到run loop提供了主接口。每个线程都有一个单独的run loop与之关联(对于子线程,若没有调用过任何获取run loop的方法是不会有run loop的,只有调用过,才会创建或者直接使用)。
采用懒加载的方式获取方式:
2.2.2通过以下两种方式来获取run loop对象:
1.在Cocoa中,使用[NSRunLoop currentRunLoop]获取
2.使用CFRunLoopGetCurrent()函数获取
2.2.3配置RunLoop
在子线程运行run loop之前,你必须至少添加一种输入源或者定时器。如果run loop没有任何的源需要监视,它就会立刻退出。
除了添加sources之外,你还可以添加观察者来检测runloop不同的执行状态。要添加观察者,可以使用CFRunLoopObserverRef指针类型和使用CFRunLoopAddObserver函数来添加到run loop中。我们只能通过Core Foundation来创建run loop观察者,即使是Cocoa应用。
下面这段代码展示主线程如何添加观察者到run loop以及如何创建run loop观察者:
- (void)threadMain {
// The application uses garbage collection, so no autorelease pool is needed.
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// Create a run loop observer and attach it to the run loop.
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer) {
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// Create and schedule the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do {
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
} while (loopCount);
}
2.2.4 启动Run Loop
只有子线程才有可能需要启动run loop。Run loop必须至少有一种输入源或者timer源来监视。如果没有任何源,则run loop会退出。
下面的几种方式可以启动run loop:
- 无条件地:无条件进入run loop是最简单的方式,但也是最不希望这么做的,因为这样会导致run loop会进入永久地循环。可以添加、删除输入源和timer源,但是只能通过kill掉run loop才能停止。而且还不能使用自定义mode。
- 限时:与无条件运行run loop不同,最好是给run loop添加一个超时时间。
- 在特定的mode:除了添加超时时间,还可以指定mode。
下面是运行run loop的一段代码:
- (void)skeletonThreadMain {
// Set up an autorelease pool here if not using garbage collection.
BOOL done = NO;
// Add your sources or timers to the run loop and do any other setup.
do {
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;
// Check for any other exit conditions here and set the
// done variable as needed.
} while (!done);
// Clean up code here. Be sure to release any allocated autorelease pools.
}
2.2.5 退出Run Loop
有两种方法使run loop在处理事件之前,退出run loop:
1.给run loop设定超时时间
2.告诉run loop要stop
设定超时时间是比较推荐的。我们可以通过CFRunLoopStop函数来停止run loop。
2.3 线程安全
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
三丶使用示例:
3.1 检测卡顿监控
通过CFRunLoop源代码
https://opensource.apple.com/source/CF/CF-1151.16/CFRunLoop.c
NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿.
技术要点:
1.利用CFRunLoopObserverRef 实时获得这些状态值的变化
2.知道RunLoop的Activity
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
};
- dispatch_semaphore_t 利用保证计算精确
参考:https://github.com/suifengqjn/PerformanceMonitor
#import "PerformanceMonitor.h"
#import <CrashReporter/CrashReporter.h>
@interface PerformanceMonitor ()
{
int timeoutCount;
//实时获得这些状态值的变化
CFRunLoopObserverRef observer;
@public
dispatch_semaphore_t semaphore;
CFRunLoopActivity activity;
}
@end
@implementation PerformanceMonitor
+ (instancetype)sharedInstance
{
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
PerformanceMonitor *moniotr = (__bridge PerformanceMonitor*)info;
moniotr->activity = activity;
dispatch_semaphore_t semaphore = moniotr->semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)stop
{
if (!observer)return;
CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
observer = NULL;
}
- (void)start
{
if (observer)return;
// 信号
semaphore = dispatch_semaphore_create(0);
// 注册RunLoop状态观察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
NSLog(@"---卡顿计时器:%ld",(long)timeoutCount);
if (++timeoutCount < 5)
continue;
NSLog(@"...我被卡住了");
}
}
timeoutCount = 0;
}
});
}
@end
Paste_Image.png
3.2 让线程常在
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添加MachPort源,保证线程不销毁
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
3.3 NStimer使用
一丶主线程情况:
NSTimer *timer =[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES];
//滑动Scroll,或者tabview等,由于主线程只能执行一个程序,正在运行的timer会被停止,所以,加入以下字段,能提升计时器的优先级别
[[NSRunLoop currentRunLoop] addTimer:_myTimer forMode:UITrackingRunLoopMode];
二丶分线程情况:
//创建分线程
dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQ, ^{
NSTimer *timer =[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES];
//通过[NSRunLoop currentRunLoop]创建RunLoop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//分线程必须运行
[[NSRunLoop currentRunLoop] run];
});