运行循环是与线程相关的基本基础结构的一部分。运行循环是事件处理循环,用于安排工作并协调传入事件的接收。运行循环的目的是在有工作要做时让线程保持忙碌,在没有工作要做时让线程休眠。
运行循环管理不是完全自动的。您仍然必须设计线程代码以在适当的时间启动运行循环并响应传入事件。Cocoa和Core Foundation都提供了运行循环对象来帮助您配置和管理线程的运行循环。您的应用程序不需要显式地创建这些对象;每个线程,包括应用程序的主线程,都有一个相关的运行循环对象。但是,只有子要线程需要显式地运行它们的run循环。作为应用程序启动过程的一部分,应用程序框架会自动在主线程上设置并运行run循环。
以下部分提供了有关运行循环以及如何为应用程序配置它们的更多信息。有关运行循环对象的更多信息,请参见NSRunLoop类参考和CFRunLoop参考。
运行循环在线程进入并用于运行事件处理程序以响应传入事件的循环。您的代码提供了用于实现运行循环的实际循环部分的控制语句——换句话说,您的代码提供了驱动运行循环的while或for循环。在循环中,使用运行循环对象“运行”接收事件并调用已安装处理程序的事件处理代码。
运行循环从两种不同类型的源接收事件。输入源提供异步事件,通常是来自另一个线程或不同应用程序的消息。计时器源提供同步事件,在预定时间或重复间隔发生。这两种类型的源都使用特定于应用程序的处理程序例程来处理事件。
图3-1展示了运行循环和各种源的概念结构。输入源将异步事件传递给相应的处理程序,并因runUntilDate:方法(在线程的相关NSRunLoop对象上调用)退出。计时器源将事件传递给它们的处理程序例程,但不会导致运行循环退出。
除了处理输入源外,运行循环还生成关于运行循环行为的通知。已注册的运行循环观察者可以接收这些通知,并使用它们在线程上执行额外的处理。您可以使用Core Foundation在线程上安装运行循环观察器。
以下部分提供了有关运行循环组件及其操作模式的更多信息。它们还描述了在处理事件期间在不同时间生成的通知。
运行循环模式是要监视的输入源和计时器的集合,以及要通知的运行循环观察器的集合。每次运行运行循环时,都指定(显式或隐式)运行的特定“模式”。在运行循环的传递过程中,只有与该模式关联的源被监视并允许传递它们的事件。(类似地,只有与该模式相关的观察者才会被通知运行循环的进度。)与其他模式关联的源保留任何新事件,直到后续的事件以适当的模式通过循环。
在代码中,通过名称标识模式。Cocoa和Core Foundation都定义了一个默认模式和几个常用模式,以及用于在代码中指定这些模式的字符串。您可以通过简单地为模式名指定一个自定义字符串来定义自定义模式。尽管分配给自定义模式的名称是任意的,但这些模式的内容不是。您必须确保将一个或多个输入源、计时器或运行循环观察器添加到您为它们创建的任何模式中,以便它们发挥作用。
在运行循环的特定传递过程中,使用模式从不需要的源中过滤事件。大多数情况下,您希望以系统定义的“默认”模式运行运行循环。然而,模态面板可能以“模态”模式运行。在这种模式下,只有与模态面板相关的源才会将事件传递给线程。对于辅助线程,可以使用自定义模式来防止低优先级源在时间关键型操作期间传递事件。
表3-1列出了Cocoa和Core Foundation定义的标准模式以及使用该模式的说明。name列列出了用于在代码中指定模式的实际常量。
Mode | Name | Description |
Default |
|
默认模式是用于大多数操作的模式。大多数情况下,您应该使用此模式启动运行循环并配置输入源。 |
Connection | NSConnectionReplyMode (Cocoa) |
Cocoa将此模式与NSConnection对象一起使用来监视响应,一般很少需要使用这种模式。 |
Modal |
|
Cocoa使用此模式来标识用于模式面板的事件。 |
Event tracking |
|
Cocoa使用此模式在鼠标拖动循环和其他类型的用户界面跟踪循环期间限制传入事件。 |
Common modes |
|
这是一组可配置的常用模式。将输入源与此模式相关联也会将其与组中的每个模式相关联。对于Cocoa应用程序,这个集合默认包括默认模式、模式和事件跟踪模式。Core Foundation最初只包含默认模式。您可以使用CFRunLoopAddCommonMode函数向集合中添加自定义模式。 |
输入源将事件异步地传递给线程。事件的来源取决于输入源的类型,通常是两类之一。基于端口的输入源监视应用程序的Mach端口。自定义输入源监视自定义事件源。就运行循环而言,输入源是基于端口的还是自定义的并不重要。系统通常实现两种类型的输入源,您可以按原样使用它们。这两个源之间唯一的区别是它们的信号方式。基于端口的源由内核自动发出信号,而自定义源必须从另一个线程手动发出信号。
创建输入源时,将其分配给运行循环的一个或多个模式。模式影响在任何给定时刻监视哪些输入源。大多数情况下,以默认模式运行运行循环,但也可以指定自定义模式。如果输入源不在当前监视模式中,则它生成的任何事件都将保持,直到运行循环以正确的模式运行为止。
下面几节描述了一些输入源。
Cocoa和Core Foundation为使用端口相关对象和函数创建基于端口的输入源提供了内置支持。例如,在Cocoa中,你根本不需要直接创建一个输入源。您只需创建一个端口对象,并使用NSPort的方法将该端口添加到运行循环。端口对象为您处理所需输入源的创建和配置。
在Core Foundation中,必须手动创建端口及其运行循环源。在这两种情况下,您都使用与端口不透明类型(CFMachPortRef、CFMessagePortRef或CFSocketRef)关联的函数来创建适当的对象。
eg:
NSPort* myPort = [NSMachPort port];
if (myPort)
{
// This class handles incoming port messages.
[myPort setDelegate:self];
// Install the port as an input source on the current run loop.
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// Detach the thread. Let the worker release the port.
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}
}
要创建自定义输入源,必须使用Core Foundation中与CFRunLoopSourceRef不透明类型关联的函数。您可以使用几个回调函数配置自定义输入源。Core Foundation在不同的位置调用这些函数来配置源、处理任何传入事件,并在源从运行循环中删除时将其删除。
除了定义事件到达时自定义源的行为外,还必须定义事件交付机制。源的这一部分运行在单独的线程上,负责向输入源提供其数据,并在数据准备好进行处理时向输入源发出信号。事件交付机制由您决定,但不必过于复杂。
除了基于端口的源,Cocoa还定义了一个自定义输入源,允许您在任何线程上执行选择器。与基于端口的源一样,执行选择器请求在目标线程上序列化,减轻了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同,执行选择器源在执行它的选择器后将自己从运行循环中移除。
当在另一个线程上执行选择器时,目标线程必须有一个活动的运行循环。对于您创建的线程,这意味着要等到代码显式地启动运行循环。但是,因为主线程启动了自己的运行循环,所以只要应用程序调用应用程序委托的applicationDidFinishLaunching:方法,您就可以开始对该线程发出调用。运行循环在每次循环中处理所有排队的执行选择器调用,而不是在每次循环迭代中处理一次。
下列出了NSObject中定义的方法,这些方法可用于在其他线程上执行选择器。因为这些方法是在NSObject上声明的,所以你可以在任何可以访问Objective-C对象的线程中使用它们,包括POSIX线程。这些方法实际上并不创建一个新线程来执行选择器。
Methods | Description |
---|---|
|
在应用程序主线程的下一个运行循环周期中,在该主线程上执行指定的选择器。这些方法提供了阻塞当前线程的选项,直到执行选择器为止。 |
|
在你有NSThread对象的任何线程上执行指定的选择器。这些方法提供了阻塞当前线程的选项,直到执行选择器为止。 |
|
在下一个运行循环周期期间和可选延迟期之后,对当前线程执行指定的选择器。因为它要等到下一个运行循环周期才能执行选择器,所以这些方法从当前执行的代码中自动提供了一个最小的延迟。多个队列选择器按照它们排队的顺序依次执行。 |
|
让你取消一个消息发送到当前线程使用performSelector:withObject:afterDelay:或performSelector:withObject:afterDelay:inModes:方法。 |
每次运行时,线程的运行循环都会处理挂起的事件,并为任何附加的观察者生成通知。它这样做的顺序非常具体,如下所示:
1.一个事件到达基于端口的输入源。
2.计时器触发。
3.为运行循环设置的超时值过期。
4.运行循环被显式唤醒。
由于计时器和输入源的观察者通知是在这些事件实际发生之前交付的,因此通知的时间和实际事件的时间之间可能存在间隙。如果这些事件之间的时间间隔很重要,则可以使用sleep和从睡眠中醒来的通知来帮助关联实际事件之间的时间间隔。
只有在为应用程序创建辅助线程时,才需要显式地运行运行循环。应用程序主线程的运行循环是基础结构的关键部分。因此,应用程序框架提供了运行主应用程序循环的代码,并自动启动该循环。iOS中的UIApplication(或OS X中的NSApplication)的run方法作为正常启动序列的一部分启动应用程序的主循环。
对于子线程,您需要决定是否需要运行循环,如果需要,则自己配置并启动它。在所有情况下都不需要启动线程的run循环。例如,如果使用线程执行一些长时间运行的预定任务,则可能可以避免启动运行循环。运行循环适用于需要与线程进行更多交互的情况。例如,如果您计划执行以下任何操作,则需要启动一个运行循环:
运行循环对象提供了向运行循环添加输入源、计时器和运行循环观察器并运行它的主接口。每个线程都有一个与之关联的运行循环对象。在Cocoa中,这个对象是NSRunLoop类的一个实例。在低级应用程序中,它是指向CFRunLoopRef不透明类型的指针。
要获得当前线程的运行循环,您可以使用以下方法之一:
NSRunLoop *theLoop = [NSRunLoop currentRunLoop];
[theLoop acceptInputForMode:NSDefaultRunLoopMode beforeDate:[theLoop
limitDateForMode:NSDefaultRunLoopMode]];
虽然它们不是免费的桥接类型,但当需要时,你可以从NSRunLoop对象获得CFRunLoopRef不透明类型。NSRunLoop类定义了一个getCFRunLoop方法,它返回一个CFRunLoopRef类型,你可以把它传递给Core Foundation例程。因为两个对象都引用同一个运行循环,你可以根据需要混合调用NSRunLoop对象和CFRunLoopRef不透明类型
在次要线程上运行运行循环之前,必须至少向其添加一个输入源或计时器。如果运行循环没有任何要监视的源,则在尝试运行它时立即退出。有关如何向运行循环添加源的示例,请参见配置运行循环源。
除了安装源之外,还可以安装运行循环观察器,并使用它们来检测运行循环的不同执行阶段。要安装运行循环观察器,您可以创建CFRunLoopObserverRef不透明类型,并使用CFRunLoopAddObserver函数将其添加到运行循环中。运行循环观察器必须使用Core Foundation创建,即使对于Cocoa应用程序也是如此。
清单3-1显示了将运行循环观察者附加到其运行循环的线程的主例程。该示例的目的是向您展示如何创建运行循环观察器,因此代码只是设置了一个运行循环观察器来监视所有运行循环活动。基本处理程序例程(未显示)只是在处理计时器请求时记录运行循环活动。
listing3-1Createa run loop observer
在为长期存在的线程配置运行循环时,最好至少添加一个输入源来接收消息。虽然您可以只带一个附加的计时器进入运行循环,但一旦计时器触发,它通常会失效,这将导致运行循环退出。附加一个重复计时器可以使运行循环在更长的时间内运行,但这将涉及定期触发计时器以唤醒线程,这实际上是另一种轮询形式。相比之下,输入源等待事件发生,让线程处于睡眠状态,直到事件发生。
只有应用程序中的子线程才需要启动运行循环。运行循环必须至少有一个要监视的输入源或计时器。如果没有附加,则运行循环立即退出。
有几种方法可以启动运行循环,包括以下方法:
//[[NSRunLoop currentRunLoop] run];
//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
无条件地进入运行循环是最简单的选择,但也是最不可取的。无条件地运行运行循环将线程置于永久循环中,这使您对运行循环本身几乎没有控制。您可以添加和删除输入源和计时器,但是停止运行循环的唯一方法是终止它。也没有办法在自定义模式下运行运行循环。
与其无条件地运行运行循环,不如使用超时值运行运行循环。使用超时值时,运行循环将运行到事件到达或分配的时间到期为止。如果事件到达,则将该事件分派给处理程序进行处理,然后运行循环退出。然后,代码可以重新启动运行循环以处理下一个事件。如果分配的时间过期了,您可以简单地重新启动运行循环,或者使用这段时间做任何必要的管理工作。
除了超时值,还可以使用特定模式运行运行循环。模式和超时值并不互斥,在开始运行循环时都可以使用。模式限制向运行循环传递事件的源的类型,在运行循环模式中有更详细的描述。
清单3-2显示了线程主入口例程的框架版本。本例的关键部分展示了运行循环的基本结构。实际上,您将输入源和计时器添加到运行循环中,然后重复调用其中一个例程来启动运行循环。每次run循环例程返回时,您都要检查是否出现了任何可能导致退出线程的条件。该示例使用Core Foundation运行循环例程,以便它可以检查返回结果并确定运行循环退出的原因。如果你正在使用Cocoa并且不需要检查返回值,你也可以使用NSRunLoop类的方法以类似的方式运行运行循环。
Listing 3-2 Running a run loop
递归地运行运行循环是可能的。换句话说,你可以调用CFRunLoopRun, CFRunLoopRunInMode,或任何NSRunLoop方法,从输入源或定时器的处理程序例程中启动运行循环。这样做时,您可以使用想要运行嵌套运行循环的任何模式,包括外部运行循环使用的模式。
有两种方法可以让运行循环在处理事件之前退出:
如果可以管理,使用超时值当然是首选。指定超时值可以让运行循环在退出之前完成所有正常处理,包括向运行循环观察者发送通知。
使用CFRunLoopStop函数显式地停止运行循环会产生类似于超时的结果。运行循环发出所有剩余的运行循环通知,然后退出。不同之处在于,您可以在无条件启动的运行循环上使用此技术。
尽管删除运行循环的输入源和计时器也可能导致运行循环退出,但这不是停止运行循环的可靠方法。一些系统例程将输入源添加到运行循环中以处理所需的事件。因为您的代码可能不知道这些输入源,所以它将无法删除它们,这将阻止运行循环退出。
线程安全性取决于您使用哪个API来操作运行循环。Core Foundation中的函数通常是线程安全的,可以从任何线程调用。但是,如果您正在执行更改运行循环配置的操作,那么尽可能从拥有运行循环的线程执行操作仍然是一种良好的实践。
Cocoa NSRunLoop类并不像Core Foundation类那样具有内在的线程安全性。如果你正在使用NSRunLoop类来修改你的运行循环,你应该只在拥有该运行循环的同一个线程中这样做。向属于不同线程的运行循环添加输入源或计时器可能导致代码崩溃或以意想不到的方式运行。
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];
FPS :Frames Per Second 的简称缩写,意思是每秒传输帧数,FPS值越低就越卡顿,所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。iOS系统中正常的屏幕刷新率为60Hz(60次每秒)。根据CADisplayLink 来查看屏幕刷新频率保持一致 一般情况保持50—60就认为还是可以的。
#import "FPSMonitor.h"
#import "UIKit/UIKit.h"
@interface FPSMonitor()
@property(nonatomic,strong) CADisplayLink *link;
@property(nonatomic,assign) NSInteger count;
@property(nonatomic,assign) NSTimeInterval lastTime;
@end
@implementation FPSMonitor
-(void)beginMonitor{
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsInfoCaculate:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
-(void)fpsInfoCaculate:(CADisplayLink *)sender{
if (_lastTime==0) {
_lastTime = sender.timestamp;
}
_count ++;
double deltaTime = sender.timestamp - _lastTime;
if (deltaTime >= 1) {
NSInteger FPS = _count/deltaTime;
_lastTime = sender.timestamp;
_count = 0;
NSLog(@"FPS:%li",(NSInteger)ceil(FPS+0.5));
}
}
通过RunLoop知道主线程上都调用了哪些方法,通过监听 nsrunloop 的状态,知道调用方法是否执行时间过长,从而判断出是否卡顿。根据RunLoop从任务开始beforeSoure 到任务任务结束 beforeWating 如果时间过长 ,基本认为卡顿了。
//
// RunloopMonitor.m
// DemoTest2022
//
// Created by wy on 2022/6/22.
//
#import "RunloopMonitor.h"
@interface RunloopMonitor()
@property(nonatomic,strong) dispatch_semaphore_t dispatchSemaphore;
@property(nonatomic,assign) CFRunLoopObserverRef runLoopBeginObserver;
@property(nonatomic,assign) CFRunLoopObserverRef runLoopEndObserver;
@property(nonatomic,assign) CFRunLoopActivity runLoopBeginActivity;
@property(nonatomic,assign) CFRunLoopActivity runLoopEndActivity;
@property(nonatomic,assign) float timeoutCount;
@end
@implementation RunloopMonitor
-(void)beginMonitor{
self.dispatchSemaphore = dispatch_semaphore_create(0);
//第一个监控,监控是否处于运行状态
CFRunLoopObserverContext context ={0,(__bridge void*) self,NULL,NULL,NULL};
self.runLoopBeginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context);
// 第二个监控,监控是否处于 睡眠状态
self.runLoopEndObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopBeginObserver, kCFRunLoopCommonModes);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopEndObserver, kCFRunLoopCommonModes);
//创建子线程监控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程开启一个loop来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 17 * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!self.runLoopBeginObserver || !self.runLoopEndObserver) {
self.timeoutCount = 0;
self.dispatchSemaphore = 0;
self.runLoopBeginActivity = 0;
self.runLoopEndActivity = 0;
}
//两个runloop的状态beforsource和afterwaiting这两个状态区间时间能够检测到是否卡顿
if ((self.runLoopBeginActivity == kCFRunLoopBeforeSources || self.runLoopBeginActivity == kCFRunLoopAfterWaiting) ||(self.runLoopEndActivity == kCFRunLoopBeforeSources || self.runLoopEndActivity==kCFRunLoopAfterWaiting)) {
//出现三次出结果
if (++self.timeoutCount<2) {
continue;
}
NSLog(@"调试,检测到卡顿");
}
}//end samapgore wait
self.timeoutCount = 0;
}
});
}
// 第一个监控,监控是否处于 运行状态
void myRunLoopBeginCallback(CFRunLoopObserverRef observer,CFRunLoopActivity activity,void *info){
RunloopMonitor *lagMonitor =(__bridge RunloopMonitor *)info;
lagMonitor.runLoopBeginActivity = activity;
dispatch_semaphore_t semaphare = lagMonitor.dispatchSemaphore;
dispatch_semaphore_signal(semaphare);
}
// 第二个监控,监控是否处于 睡眠状态
void myRunLoopEndCallback(CFRunLoopObserverRef observer,CFRunLoopActivity activity,void *info){
RunloopMonitor *lagMonitor =(__bridge RunloopMonitor *)info;
lagMonitor.runLoopEndActivity = activity;
dispatch_semaphore_t semaphare = lagMonitor.dispatchSemaphore;
dispatch_semaphore_signal(semaphare);
}
@end
__weak typeof(self) weakSelf = self;
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"新线程");
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"即将进入RunLoop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即将处理Timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即将处理Sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即将进入休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"从休眠中唤醒");
break;
case kCFRunLoopExit:
NSLog(@"即将退出RunLoop");
break;
default:
break;
}
});
// 监听RunLoop的状态变化
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
while (!weakSelf.stopped) {
// [[NSRunLoop currentRunLoop] run];
// [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"-----------while---");
}
NSLog(@"end");
}];
self.thread.name=@"mytestthread";
[self.thread start];
利用Runloop在UIScrollView滑动时和App默认运行时的Model不同来实现
利用PerformSelector设置当前线程的Runloop的运行模式,
NSDefaultRunLoopMode:App的默认运行模式,通常主线程是在这个运行默认下运行的,
UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)然后 我们滑动UITableView时候 RunLoop的运行模式就会变为UITrackingRunLoopMode所以我们把给ImageView加载图片的方法用PerformSelector设置当前线程的RunLoop的运行模式kCFRunLoopDefaultMode 这样滑动时候就不会执行加载图片的方法了
[cell performSelector:@selector(setImage:) withObject:nil afterDelay:0.1 inModes:@[NSDefaultRunLoopMode]];