iOS - RunLoop 深入理解

作者:Mitchell 

Run loop 剖析:
Runloop 接收的输入事件来自两种不同的源:输入源(intput source)和定时源(timer source)。输入源传递异步事件。通常消息来自于其他线程或程序。定时源则传递同步事件,发生在特定时间或者重复的时间间隔。两种源都使用程序的某一特定的处理历程来处理到达的时间。


一、什么是RunLoop

  • 基本作用
    • 保持程序的持续运行(一个死循环,使app不断运行)
    • 处理App中的各种事件(触摸、定时器、Selector)
    • 节省CPU资源、提高程序性能:该做事的时候做事,该休息的时候休息。
  • 如果没有RunLoop
int main(int argc,char * argv[]){
    NSLog(@"execute main function");---->程序开始
    return 0; ------------------------->程序结束
}
  • 有 RunLoop
    • 由于 main 函数里面启动了个 RunLoop,所以程序并不会马上退出,保持持续运行状态
int main(int argc,char * argv[]){
    BOOL running = YES; -------->程序开始
    do {------------------------------
         // 执行各种任务,处理各种事件------持续运行
    }while(running);---------------------
    return 0;
}

二、main 函数中的 RunLoop

  • UIApplicationMain函数内部就启动了一个RunLoop
  • 所以UIApplicationMain 函数一直没有返回,保持了程序的持续运行
  • 这个默认启动的 RunLoop 是跟主线程相关联的
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

三、RunLoop 的输入源

  • 输入源异步的发送消息给你的线程,时间的来源取决于输入源的种类:基于端口的输入源自定义输入源。基于端口的输入源监听程序相应的端口。自定义输入源则监听自定义的事件源。runloop,不关心输入源是基于端口的还是自定义的。系统会实现两种输入源供你使用。两类输入源的区别在于如何显示:基于端口的输入源由内核自动发送,而自定义的则需要人工从其他线程发送。
  • 当你创建输入源的时候,需要将其分配给 runloop 中的一个或多个模式。模式只会在特定事件影响监听的源。大多数情况下,runloop 运行在默认模式下,但是你也可以使其运行在自定义模式中。若某一源在当前模式下不被监听,那么任何生成的消息只在 runloop 运行在所关联的模式下才会被传递。
  • 基于端口的输入源
    • Cocoa 和 CoreFoundation 内置支持使用端口相关的对象和函数来创建基于端口的源。在 Cocoa 里面你从来不需要直接创建输入源。只要简单的创建对象,并使用 NSPort 的方法将该端口添加到 ruhnloop 中。端口对象会自己处理创建和配置的输入源。
    • 配置基于端口的输入源
      配置 NSMachPort 对象
      为了和 NSMachPort 对象建立稳定的本地连接,你需要创建端口对象并将之加入相应的线程的 run loop。当运行辅助线程的时候,你传递端口对象到线程的主体入口点。辅助线程可以使用相同的端口对象将消息返回给原线程。
      • a) 实现主线程的代码
  - (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
//这个类持有即将到来的端口消息
[myPort setDelegate:self];
//将端口作为输入源安装到当前的 runLoop
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
//当前线程去调起工作线程
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}

为了在线程间建立双向的通信,你需要让工作线程在签到的消息中发送自己的本地端口到主线程。主线程接收到签到消息后就可以知道辅助线程运行正常,并且􏰀供了发送消息给辅助线程的方法。
以下代码显示了主线程的 handlePortMessage:方法。当由数据到达线程的本地端口时,该方法被调用。当签到消息到达时,此方法可以直接从辅助线程里面检索端口并保存下来以备后续使用。

#define kCheckinMessage 100
//处理从工作线程返回的响应
 - (void)handlePortMessage:(NSPortMessage *)portMessage
{
//消息的 id
unsigned int message = [portMessage msgid];
//创建远的端口
NSPort* distantPort = nil;
if (message == kCheckinMessage)
{
//获取工作线程关联的端口,并设置给远程端口
distantPort = [portMessage sendPort];
//为了以后的使用保存工作端口
[self storeDistantPort:distantPort];
}
else
{
//处理其他的消息
}
    - ***b) 辅助线程的实现代码***

对于辅助工作线程,你必须配置线程使用特定的端口以发送消息返回给主要线程。
以下显示了如何设置工作线程的代码。创建了线程的自动释放池后,紧接着创建工作对象驱动线程运行。工作对象的*** sendCheckinMessage: ***方法创建了工作线程的本地端口并发送签到消息回主线程。

//根据端口信息启动线程
 +(void)LaunchThreadWithPort:(id)inData
{
//设置当前线程和主线程的通信端口
NSPort* distantPort = (NSPort*)inData;
//获取当前的工作类
MyWorkerClass* workerObj = [[self alloc] init];
//发送签到消息
[workerObj sendCheckinMessage:distantPort];
//释放
[distantPort release];
//让 runloop 处理事务
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);
[workerObj release];
[pool release];

当使用 NSMachPort 的时候,本地和远程线程可以使用相同的端口对象在线程间进行单边通信。换句话说,一个线程创建的本地端口对象成为另一个线程的远程端口对象。
以下代码辅助线程的签到例程,该方法为之后的通信设置自己的本地端口,然后发送签到消息给主线程。它使用 LaunchThreadWithPort:方法中收到的端口对象做为目标消息。

//工作线程签到方法
 - (void)sendCheckinMessage:(NSPort*)outPort
{
//保留远程端口,以便将来使用
[self setRemotePort:outPort];
//创建并且传递工作线程的端口
NSPort* myPort = [NSMachPort port];
[myPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
//创建签到消息
NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
receivePort:myPort components:nil];
if (messageObj)
{
//完成配置消息并立即将其发送
[messageObj setMsgId:setMsgid:kCheckinMessage];
[messageObj sendBeforeDate:[NSDate date]];
}
- ***配置 NSMessagePort 对象***

为了和 NSMeaasgePort 的建立稳定的本地连接,你不能简单的在线程间传递端口
对象。远程消息端口必须通过名字来获得。在 Cocoa 中这需要你给本地端口指定一个名字,并将名字传递到远程线程以便远程线程可以获得合适的端口对象用于通信。以下代码显示端口创建,注册到你想要使用消息端口的进程。

//创建本地消息端口
NSPort* localPort = [[NSMessagePort alloc] init];
//配置对象并且将其添加到当前 runloop 中
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
//使用一个特殊的名字注册端口,名字必须是唯一的
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];
- ***在 Core Foundation 中配置基于端口的源***

这部分介绍了在 Core Foundation 中如何在程序主线程和工作线程间建立双通道通信。
以下代码显示了程序主线程加载工作线程的代码。第一步是设置
CFMessagePortRef 不透明类型来监听工作线程的消息。工作线程需要端口的名称来建立连接,以便使字符串传递给工作线程的主入口函数。在当前的用户上下文中端口名必须是唯一的,否则可能在运行时造成冲突。

#define kThreadStackSize (8 *4096)
OSStatus MySpawnThread()
{
//创建一个本地的端口来接收响应
CFStringRef myPortName;
CFMessagePortRef myPort;
CFRunLoopSourceRef rlSource;
CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
Boolean shouldFreeInfo;
//创建端口的名称
myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
//创建端口
myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&MainThreadResponseHandler,
&context,
&shouldFreeInfo);
if (myPort != NULL)
{
//端口被成功的创建了,现在为它创建一个 runloop 源
rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
 if (rlSource) {
//将源添加到 runloop 中去
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
//一旦被安装,这些指针可以被释放了。
CFRelease(myPort); 
CFRelease(rlSource);
}}
//创建线程并且持续的运行
MPTaskID taskID;
return(MPCreateTask(&ServerThreadEntryPoint,
(void*)myPortName,kThreadStackSize,NULL,NULL,
NULL,0,&taskID));
}

端口建立而且线程启动后,主线程在等待线程签到时可以继续执行。当签到消息到达后,主线程使用 MainThreadResponseHandler 来分发消息,如下面代码 所示。这个函数􏰀取工作线程的端口名,并创建用于未来通信的管道。

#define kCheckinMessage 100
//获取主线程端口消息持有者
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
 SInt32 msgid, CFDataRef data, void* info)
{
//如果消息是签到的消息
 if (msgid == kCheckinMessage)
 {
    //消息端口
     CFMessagePortRef messagePort;
    //线程名称
     CFStringRef threadPortName;
    //数据长度
     CFIndex bufferLength = CFDataGetLength(data); 
    //读取数据流
    UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
     CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
    //设置线程端口名称
    threadPortName = CFStringCreateWithBytes (NULL, buffer,bufferLength,kCFStringEncodingASCII, FALSE);
    //你必须获得一个远程消息端口的名称
    messagePort = CFMessagePortCreateRemote(NULL,(CFStringRef)threadPortName);
    //如果有消息端口
     if (messagePort) {
    //保留线程公共的端口为了将来的引用
    AddPortToListOfActiveThreads(messagePort);
    //因为端口已经被之前的方法保留,所以这里将指针释放       
     CFRelease(messagePort);
}
//清空,释放线程端口名称
CFAllocatorDeallocate(NULL, buffer);
}
else
{
//处理其他的信息
}
return NULL;
}

主线程配置好后,剩下的唯一事情是让新创建的工作线程创建自己的端口然后签到。以下代码 显示了工作线程的入口函数。函数获取了主线程的端口名并使用它来创建和主线程的远程连接。然后这个函数创建自己的本地端口号,安装到线程的 runloop,最后连同本地端口名称一起发回主线程签到。

#工作线程的入口函数#
OSStatus ServerThreadEntryPoint(void* param)
{
//创建对主线程的远程端口
CFMessagePortRef mainThreadPort;
//获取主线程的名称
CFStringRef portName = (CFStringRef)param;
mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
//释放作为参数传递过去的字符串的指针
CFRelease(portName);
//创建工作线程的端口
CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL,CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
//在线程的 context 信息中存储端口的信息以备以后的使用
CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
Boolean shouldFreeInfo;
Boolean shouldAbort = TRUE;
CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL, myPortName,
 &ProcessClientRequest, &context, &shouldFreeInfo);
if (shouldFreeInfo)
{
//不能创建本地的端口,那么就杀死线程
MPExit(0);
}
CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (!rlSource){
//如果没有创建本地的端口,那么就杀死线程
MPExit(0);
}
//将源添加到当前的 runloop 中去
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
//一旦被安装完毕,那么就可以被释放了
CFRelease(myPort);
CFRelease(rlSource);
//将端口名称和签到信息打包,并且写入流
CFDataRef returnData = nil;
CFDataRef outData;
CFIndex stringLength = CFStringGetLength(myPortName);
UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
CFStringGetBytes(myPortName,
CFRangeMake(0,stringLength),
kCFStringEncodingASCII,
0,
FALSE,
buffer,
stringLength,
NULL);
outData = CFDataCreate(NULL, buffer, stringLength);
CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL,NULL);
//清空线程数据
CFRelease(outData);
CFAllocatorDeallocate(NULL, buffer);
//进入 runLoop
CFRunLoopRun();
}

一旦线程进入了它的runloop,所有发送到线程端口的时间都会由 ProcessClientRequest 函数来处理。

  • 自定义输入源
    • 为了自定义输入源,必须使用 Core Foundation里面的 CGRunLoopSourceRef类型相关的函数来创建。你可以使用回调函数来配置自定义输入源。Corefondation 会在配置源的不同地方调用回调函数,处理输入时间,在源从 runloop 移除的时候清理它。
    • 除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并源并通知它处理数据。消息传递机制的定义取决于你,但是最好不要过于复杂。
    • 创建自定义的输入源包括定义以下内容:
      • 输入源要处理的信息。
      • 使感兴趣的客户端知道如何和输入源交互的调度例程。
      • 处理其他任何客户端发送请求的例程。
      • 使输入源失效的取消例程。
    • 由于创建输入源来处理自定义消息,实际配置选是灵活配置的。调度例程,处理例程和取消例程都是创建自定义输入源是最关键的例程。二输入源其他的大部分行为都发生在这些例程的外部。比如,由于你决定数据传输到输入源的机制,还有输入源和其他线程的通信机制也是由你决定。
    • 图 3-2 中,程序的主线程维护了一个输入源的引用,输入源所需的自定义命令缓冲区和输入源所在的 runloop。当主线程有任务需要分发给工作线程时候,主线程会给命令缓冲区发送命令和必须的信息来通知工作线程开始执行任务。(因为主线程和输入源所在工作线程都可以访问命令缓冲区,因此这些访问必须是同步的)一旦命令传送出去,主线程会通知输入源并且唤醒工作线程的 runloop。而一收到唤醒命令,runloop 会调用输入源的处理程序,由它来执行命令缓冲区中响应的命令。
      3-2.png
    • 图3-2 中的输入源使用了 Objective-C 的对象辅助 runloop来管理命令缓冲区。下面代码给出了改对象的定义。RunLoopSource 对象管理着命令缓冲区并以此来接收其他线程的消息。例子同样给出了 RunLoopContext 对象的定义,它是一个用于传递 RunLoopSource 对象和 runloop 引用给程序主线程的一个容器。
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}
 - (id)init;
 - (void)addToCurrentRunLoop;
 - (void)invalidate;
// Handler method
 - (void)sourceFired;
// Client interface for registering commands to process
 - (void)addCommand:(NSInteger)command withData:(id)data;
 - (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
@end
//这些是 CFRunLoopSourceRef 的回调函数
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
 - (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;

尽管使用 Objective-C 代码来管理输入源的自定义数据,但是将输入源附加到 runloop 却需要使用基于 C 的回调函数(RunLoopSourceScheduleRoutine)。因为这个输入源只有一个客户端(主线程),它使用调度函数发送注册信息给应用程序的委托(delegate)。当委托需要和输入源通信的时候,它会使用 RunLoopContext 对象来完成。

void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
//获取输入源
RunLoopSource* obj = (RunLoopSource*)info;
//获取应用程序的委托
AppDelegate* del = [AppDelegate sharedAppDelegate];
//根据 runloop输入源 和 runloop 获取 RunLoopContext,并将这个 RunLoopContext 注册到主线程,用于委托和输入源之间的通信。
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
//在主线程执行注册源
[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext waitUntilDone:NO];

当输入源被告知的时候回处理自定义数据的那个例程,以下代码展示了这个和 RunLoopSource 对象相关回调的例程。这里只是简单的让 RunLoopSource 执行 sourceFired 方法,然后继续处理在命令缓存区出现的命令。

void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}

使用 CFRunLoopSourceInvalidate 函数将输入源从 runloop 中移除,系统会调用输入源的取消例程。可以使用该例程来通知其他客户端该输入源已经失效,客户端应该释放输入源的引用。

void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
//获取源
RunLoopSource* obj = (RunLoopSource*)info;
//获取系统代理
AppDelegate* del = [AppDelegate sharedAppDelegate];
//根据 源和 runloop 获取 RunLoopContext
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
//发送移除源的命令
[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext waitUntilDone:YES];
}
- ***安装输入源到 RunLoop***

以下代码显示了 RunLoopSource 的 init 和 addToCurrentRunloop 的方法。Init 方法创建 CGFunLoopSourceRef 类型,该类型必须被附加到 runloop 里。它将 RunLoopSource 对象作为上下文引用参数,以便回调例程持有该对象的一个引用指针。输入源的安装只在工作线程调用 addToCurrentRunLoop 方法才发生,此时 RunLoopSourceScheduledRoutine 被调用。一旦输入源被添加到 runloop,线程就运行 runloop 并等待事件。

 - (id)init
{
CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,
//CFRunLoopSource 输入源
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};
//命令数组
commands = [[NSMutableArray alloc] init];
return self;
}
 - (void)addToCurrentRunLoop
{
    //获取当前 runloop
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    //将源添加到 runloop
    CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
- ***协调输入源的客户端***

为了让添加的输入源有用,需要维护它并从其他线程给它发送信号。输入源的主要工作就是将于输入源相关的线程置于休眠状态知道有事件发生。这就意味着程序中的要有其他线程知道该输入源信息并且有办法与之通信。
通知客户端关于输入源信息的方法之一就是当你的输入源开始安装到你的 runloop 上面后阿松注册请求。将输入源注册到任意数量的客户端,或者通过代理将输入源注册到感兴趣的客户端那。以下代码显示了应用委托定义的注册方法以及它在 RunLoopSource 对象的调度函数被调用时如何运行。该方法接收 RunloopSource 提供的 RunLoopContext 对象,然后将其添加到他自己的源列表里面。

//注册源
 - (void)registerSource:(RunLoopContext*)sourceInfo;
{
[sourcesToPing addObject:sourceInfo];
}
//移除源
 - (void)removeSource:(RunLoopContext*)sourceInfo
{
id objToRemove = nil;
for (RunLoopContext* context in sourcesToPing)
{
if ([context isEqual:sourceInfo])
{
objToRemove = context;
break;
}
}
if (objToRemove)
[sourcesToPing removeObject:objToRemove];
}
- ***通知输入源***

当客户端发送数据到输入源之后,它必须发送信号通知源并且唤醒它的 runloop。发送信号给源可以让 runloop 知道该源已经做好处理消息的准备。而且因为信号发送时线程可能处于休眠,所以必须总是显示的唤醒 runllop。如果不这样做的话会导致延迟处理输入源。
当客户端准备好处理加入缓冲区的命令后会调用此方法。

 - (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
//给 runLoop 发送信号
CFRunLoopSourceSignal(runLoopSource);
//显示的启动 runLoop
CFRunLoopWakeUp(runloop);

注意:你不应该试图通过自定义输入源处理一个 SIGHUP 或其他进程级别类型的信号。CoreFoundation 唤醒 run loop 的函数不是信号安全的,不能在你的应用信号处理例程(signalhandler routines)里面使用。关于更多信号处理例程,参阅 sigaction 主页。

  • Cocoa 执行 Selector 的源
    • 除了基于端口的源,Cocoa 定义了自定义的输入源,允许你在任何线程中执行 seletor。和基于端口的源一样,执行 selector 请求会在目标线程上序列化,减缓许多咋线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个 selector 执行完后会自动从 runloop 里面移除。
    • 当在其他线程上面执行 selector 时候,目标线程需有一个活动的 runloop,对于你创建的线程,这意味着线程在你显示的启动 runloop 之前处于等待状态。由于主线程自己启动它的 runloop,那么在程序通过委托调用 applicationDidFinishlaunching:的时候你会遇到线程调用的问题。因为 RunLoop 通过每次循环来处理所有队列的 selector 的调用,而不是通过 loop 的迭代来处理 selector。
  • 定时源
    • 定时源在预设的时间点同步方式传递消息。定时器是线程通知自己做某事的一种方法。
    • 尽管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和 runloop 的特定模式相关。如果定时器所在的模式当前未被 runloop 监视,那么定时器将不会开始知道 runloop 运行在响应的模式下。类似的。如果定时器在 runloop 处理某一事件期间开始,定时器会一直等待直到下次 runloop 开始响应的处理程序。如果 runloop 不运行了,那么定时器也永远不启动。
    • 配置定时源
      Cocoa 中可以使用以下 NSTimer 类方法来创建并调配一个定时器:􏰂
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:􏰂 
scheduledTimerWithTimeInterval:invocation:repeats:

上述方法创建了定时器并以默认模式把它们添加到当前线程的 run loop。
Core Foundation 创建定时器

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);

四、RunLoop 对象

  • iOS 中有2套 API 来访问和使用 RunLoop
    • Foundation
      • NSRunLoop 的 currentRunLoop 类方法类检索一个 NSRunLoop 对象。
    • CoreFoundation
      • CFRunLoopGetCurrent 函数
  • NSRunLoop 和 CGRunLoopRef 都代表着 RunLoop 对象
  • NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装, 所以要了解 RunLoop内部结构,需要研究 CFRunLoopRef 层面的 API (Core Foundation层面)
//获取当前 runloop(NSRunLoop*)
 + (NSRunLoop *)currentRunLoop;
[NSRunLoop currentRunLoop];
//CFRunLoopRef
 - (CFRunLoopRef)getCFRunLoop CF_RETURNS_NOT_RETAINED;
[[NSRunLoop currentRunLoop]getCFRunLoop];
  • 在辅助线程运行 run loop 之前,必须至少添加已输入源或定时器给它。如果 runloop 没有任何源需要监视的话,它会在你启动的时候马上退出。
 - (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);
}

五、RunLoop 与线程

  • 每条线程都有唯一的一个与之对应的 RunLoop 对象
  • 主线程的 RunLoop 自动创建好了,子线程的 RunLoop 需要主动创建
  • RunLoop 在第一次获取时创建,在线程结束时销毁

六、获取RunLoop 对象

  • Foundation
//获得当前线程的 RunLoop 对象
[NSRunLoop currentRunLoop];

//获得主线程的 RunLoop 对象
[NSRunLoop mainRunLoop];

  • Core Foundation
//当前RunLoop
CFRunLoopGetCurrent();
//主线程 RunLoop
CFRunLoopGetMain();

七、NSRunLoop 相关类

  • CoreFoundation 中关于 RunLoop 的5个类
    • CFRunLoopRef(运行循环对象)
    • CFRunLoopModeRef(1个runLoop可以有很多个Mode,1个Mode可以有很多个Source Observer Timer,但是在同一时刻只能同时执行一种Mode关于更多种类的Mode
    • CFRunLoopSourceRef(处理事件)
    • CFRunLoopTimerRef(处理定时器相关)
    • CFRunLoopObserverRef(观察者,观察是否有事件)


      RunLoop.png
  • CFRunLoopModeRef 代表 RunLoop 的运行模式
    • 一个 RunLoop 包含若干个 Mode,每个Mode 又包含若干个 Source/Timer、Observer
    • 每次 RunLoop 启动时,只能制定其中一个 Mode,这个 Mode 被称作 CurrentMode
    • 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入
    • 这样做主要是为了分割开不同组的 Source/Timer/Observer,让其互不影响
  • 系统默认注册了 5个Mode:
    • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个 Mode 下运行的
    • UITrackingRunLoopMode:界面跟踪 Mode,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响
    • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个Mode,启动完成之后就不再使用。
    • GSEventReceiveRunLoopMode:接收系统时间的内部 Mode,通常用不到。
    • kCFRunLoopCommonModes(比较特殊):这时一个占位用的 Mode,不是一种真正的 Mode。
  • CFRunLoopSourceRef 是事件源(输入源)
    • Source0:非基于Port的
      • Custom Input Sources
      • Cocoa Perform Selector Sources
    • Source1:基于Port的
      • Port- Based Sources
    • 举例:输出点击事件的调用栈,我们可以清楚的看到runloop中做的是__CFRunLoopDoSource0
      Source0调用栈.png
  • CFRunLoopTimerRef 处理定时器
    • NSTimer 定时器调用栈:__CFRunLoopDoTimer


      timer.png
    • 注意:使用不同种类的 Mode 会对定时器的效果有不同的展现
      • NSDefaultRunLoopMode:将NSTimer添加到主线程NSRunLoop的默认模式下,只有主线程是默认模式下才能执行NSTimer(滚动scrollView,RunLoop默认进入Tracking模式,所以NSTimer不会有效果)。
      • UITrackingRunLoopMode:将NSTimer添加到主线程NSRunLoop的追踪模式下,只有主线程是追踪模式下才能执行NSTimer。(例如滚动scrollView的时候就会监听到计时器)
      • NSRunLoopCommonModes:Common是一个表示,它是将NSDefaultRunLoopMode 和 UITrackingRunLoopMode标记为了Common
        所以,只要将 timer 添加到 Common 占位模式下,timer就可以在Default和UITrackingRunLoopMode模式下都能运行
  • 如果用GCD创建计时器:
    • GCD 创建的好处,不受 RunLoopMode 的影响。
    //1、创建timer
    //dispatchQueue:定时器将来回调的方法在哪个线程中执行
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    self.timer = timer;
    //2.设置timer
    /*
     第一个参数:需要设置哪个timer
     第二个参数:指定定时器开始的时间
     第三个参数:指定间隔时间
     第四个参数:定时器的精准度,如果传0代表要求非常精准(系统会让计时器执行时间变得更加准确,性能消耗也会提高),如果传入一个大于0的值,代表我们允许的误差
     //例如传入60,就代表允许误差有60秒
     */
    //设置第一次执行的时间
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
    //DISPATCH_TIME_NOW
    dispatch_source_set_timer(timer,start , 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    //3、设置timer的回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"%@",[NSRunLoop currentRunLoop]);
    });
    dispatch_resume(timer);
- 在 RunLoop 底层默认会调用这里
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
} 
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} 
  • CFRunLoopObserverRef观察者,能够监听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
}
- 监听的代码:
  - (void)viewDidLoad{
    [super viewDidLoad];
    //1、创建监听对象
    /*
     第一个参数:告诉系统如何给Observer对象分配存储空间
     第二个参数:需要监听的类型
     第三个参数:是否需要重复监听
     第四个参数:优先级
     第五个参数:监听到对应的状态之后的回调
     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
     };
     */
    CFRunLoopObserverRef oberver= CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"进入RunLoop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理Timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入睡眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"即将醒来");
                break;
            case kCFRunLoopExit:
                NSLog(@"退出");
                break;
            default:
                break;
        }
    });
    //2、给主线程的RunLoop添加监听
    /*
     第一个参数:需要监听的 RunLoop 对象
     第二个参数:给指定的 RunLoop 对象添加的监听对象
     第三个参数:在哪种模式下监听
     */
    CFRunLoopAddObserver(CFRunLoopGetMain(), oberver, kCFRunLoopCommonModes);
NSTimer *timer = [NSTimer   scheduledTimerWithTimeInterval:2 target:self   selector:@selector(demo) userInfo:nil repeats:YES];
}
  - (void)demo{
    NSLog(@"%s",__func__);
}

我们会看到这几行打印会重复执行

2015-09-06 17:02:04.848 RunLoop观察者[35817:418636] 即将醒来
2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] -[ViewController demo]
2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将处理Timer
2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将处理source
2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将进入睡眠
2015-09-06 17:02:06.848 RunLoop观察者[35817:418636] 即将醒来
2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] -[ViewController demo]
2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将处理Timer
2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将处理source
2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将进入睡眠

八、关于 RunLoop 的理解

  • 理解图(引用了一张网上很好的图片):
    RunLoop理解图.png
  • 一条线程对应一个 RunLoop,主线程的 RunLoop 只要程序已启动就会默认创建并与主线程绑定好,RunLoop 底层的实现是通过字典的形式来将 线程 和 RunLoop 来绑定的,RunLoop 可以理解为懒加载,子线程的 RunLoop 可以调用 currentRunLoop,先从字典里面根据子线程取,如果没有就会去创建并与子线程绑定,保存到字典当中。每个 RunLoop 里面有很多的 Mode,每个 Mode 里面又有很多的source、timer、observer。RunLoop 在同一时刻只能执行一种 Mode,当执行这种 Mode 的时候,只有这种 Mode 中的source、timer、observer 有效,别的 Mode 无效,这样做是为了避免逻辑的混乱。
  • 执行流程:先进入 RunLoop,处理系统默认事件,触发事件的时候,RunLoop 醒来处理 timer、source0、source1,处理完再睡觉。
  • RunLoop 死掉的情况:
    • RunLoop 有个默认的超时时间.
  seconds = 9999999999.0
- 线程挂了。

九、RunLoop 应用场景

  • NSTimer
    • 就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
    • 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
    • CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。
  • ImageView显示
  • PerformSelector:
    • 当调用 NSObject 的 performSelector:afterDelay:后,世纪上期内部会创建一个 Timer 并添加到当前线程的 RunLoop 中,所以如果当前线程没有 RunLoop,则这个方法会失效。
    • 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
  • 常驻线程
    • 创建一个线程来处理耗时且频繁的操作,例如即时聊天音频的压缩,或者经常下载,避免频繁开启线程以便提高性能, AFNetWorking就是如此。
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
  • 自动释放池
    • 系统在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
    • 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
    • 第二个 Observer 监视了两个事件BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
    • 打印 currentRunLoop 来获取autoreleasePool 的状态
    NSLog(@"%@",[NSRunLoop currentRunLoop]);
- 只有两种状态
     _wrapRunLoopWithAutoreleasePoolHandler:activities = 0x1  = 1
     _wrapRunLoopWithAutoreleasePoolHandler:activities = 0xa0 = 160
- 对比 RunLoop 的活动状态:
对比runLoop
     typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity){
     kCFRunLoopEntry            = (1UL << 0), // 即将进入LOOP    =1
     kCFRunLoopBeforeTimers     = (1UL << 1), // 即将处理Timer = 2
     kCFRunLoopBeforeSources    = (1UL << 2), // 即将进入处理Source = 4
     kCFRunLoopBeforeWaiting    = (1UL << 5), // 即将进入休眠 = 32
     kCFRunLoopAfterWaiting     = (1UL << 6), // 刚才休眠中唤醒 = 64
     kCFRunLoopExit             = (1UL << 7), // 即将退出Loop = 128
     }
- 得出结论:
     + _wrapRunLoopWithAutoreleasePoolHandler:activities = 0x1 = 1 = 即将进入RunLoop创建一个自动释放池
     + _wrapRunLoopWithAutoreleasePoolHandler:activities = 0xa0 = 160 = 128+32
     + 32: 即将进入休眠 1、销毁一个自动释放池 2、创建一个新的自动释放池
     + 128:即将退出RunLoop 销毁一个自动释放池

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容