RunLoop总结

2017-03-11  本文已影响58人  Resoluted

RunLoop介绍

RunLoop和线程有什么关系?

总的来说,RunLoop正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,runloop和线程是紧密相连的,可以这样说runloop是为了线程而生,没有线程,它就没有存在的必要。Runloops是线程的基础框架部分,Cocoa和CoreFoundation都提供了runloop对象方便配置和管理线程的runloop(以下都以Cocoa为例)。每个线程,包括程序的主线程(main thread)都有与之对应的runloop对象。
*RunLoop和线程之间的关系
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让他干活的时候又能立马响应。

获取对应的CFRunLoopRef类,来达到线程安全的目的。

*RunLoop的相关知识点

* 输入源(input source)

    传递异步事件,通常消息来自其他线程和程序。输入源传递异步消息给相应的处理例程,并调用runUntileDate:方法来退出(在线程里面相关的NSRunLoop对象调用)。
    * 基于端口的输入源
    
        基于端口上午输入源由内核自动发送。Cocoa和CoreFoundation内置支持使用端口相关的对象和函数来创建的基于端口的源。例如,在Cocoa里面你从来不需要直接创建输入源。你只要简单的创建端口对象,并使用NSPort的方法把该端口添加到runloop.端口对象会自己处理创建和配置输入源。
        在CoreFoundtaion,你必须人工创建端口和她的runloop源。我们可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建合适的对象。下面的例子展示了如何创建一个基于端口的输入源,将其添加到runloop并启动:


             void creatPortSource() {
            CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault, "come.someport", myCallBackFunc, nil, nil);
            CFRunLoopSourceRef source = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
            while (pageStillLoading) {
            NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
            CFRunLoopRun();
            [pool release];
            }
            CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            CFRelease(source);    
            }
            

    * 自定义输入源
        自定义的输入源需要人工从其他线程发送。为了创建自定义输入源,必须使用CoreFoundation里面的CFRunLoopSourceRef类型相关的函数来创建。你可以使用回调函数来配置自定义输入源。CoreFundation会在配置源的不同地方调用回调函数,处理输入事件,在源从runloop移除的时候清理它。
        
        除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并通知它处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。创建并启动自定义输入源的示例如下:
        
            void createCustomSource()
            {
            CFRunLoopSourceContext context = {0,NULL, NULL,NULL, NULL,NULL, NULL,NULL, NULL,NULL};
            CFRunLoopSourceRef source =CFRunLoopSourceCreate(kCFAllocatorDefault,0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
            while (pageStillLoading) {
            NSAutoreleasePool *pool = [[NSAutoreleasePoolalloc] init];
            CFRunLoopRun();
            [pool release];
            }
            CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
            CFRelease(source);
            }
            
            
    * Cocoa上的Selector源
        
        除了基于端口的源,Cocoa定义了自定义输入源,允许你在任何线程执行selector()方法。和基于端口的源一样,执行selector请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个selector执行完成后会自动从runloop里面移除。
        
        当在其他线程上面执行selector时,目标线程必须有一个活动的runloop。对于你创建的线程,这就意味着线程在你显式的启动runloop之前是不会执行selector方法的,而是一直处于休眠状态。
        
        NSObject类提供了类似如下的selector方法:
        
            - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array  

因为定时器和输入源的观察者是在相应的事件发生之前传递消息,所以通知的时间和实际事件发生的时间之间可能存在误差。如果需要精确时间控制,你可以使用休眠和唤醒通知来帮助你校对实际发生事件的时间。

因为当你运行runloop时定时器和其它周期性事件经常需要被转换,撤销runloop也会终止消息传递。典型的例子就是鼠标路径追踪。因为你的代码直接获取到消息而不是经由程序传递,因此活跃的定时器不会开始直到鼠标追踪结束并将控制权交给程序。

runloop可以有runloop对象显式唤醒。其他消息也可以唤醒runloop.例如,添加新的非基于端口的源会唤醒runloop从而可以立即处理输入源而不需要等待其他事件发生后再处理。

从这个事件队列中可以看出:

*RunLoop的mode作用是什么?

mode主要是用来指定事件在运行循环中的优先级的,分为:

苹果公开提供的Mode有两个:

1.NSDefaultRunLoopMode (kCFRunLoopDefaultMode)
2.NSRunLoopCommomodes (kCFRunLoopCommonModes)

以+ scheduledTimerWithTimeInterval...的方式触发的timer,在滑动页面上的列表时,timer会暂停回调,为什么?如何解决?

RunLoop只能运行在一种mode下,如果要换mode,当前的loop也需要停下重启成新的。基于这个机制,ScrollView在滚动过程中NSDefaultRunLoopMode (kCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动:只能在NSDefaultRunLoopMode的模式下处理的事件会影响ScrollView的滑动。

如果我们把一个NSTimer对象以NSDefaultRunLoopMode (kCFRunLoopDefaultMode)添加到主运行循环中的时候,ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。

同时因为mode还是可定制的。所以:
Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommomModes (kCFRunLoopCommonModes)来解决。 代码如下:

// 将timer添加到NSDefaultRunLoopMode
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
// 再将timer添加到NSRunLoopCommonModes
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
猜想RunLoop内部是如何实现的?

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机退出,让线程能随时处理但并不退出,通常的代码逻辑是这样的:

function loop() {
initialize();
do {
    var message = get_next_message();
    process_message(message);
} while (message != quit);
}

或用伪代码来展示下:

int main (int argc, char *srgv[]) {
    // 程序一直运行状态
    while (AppIsRunning) {
        // 睡眠状态,等待唤醒时间
        id whoWakesMe = SleepForWakingUp();
        // 得到唤醒事件
        id event = GetEvent(whoWakesMe);
        // 开始处理事件
        HandleEvent(event);
    }
    return 0;
}
什么时候使用RunLoop?

仅当在为你的程序创建辅助线程的时候,你才需要显式进行一个runloop。RunLoop是程序主线程基础设施的关键部分。所以,Cocoa和Carbon程序提供了代码运行主程序的循环并自动启动runloop.iOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作为程序启动步骤的一部分,它在程序正常启动的时候就会启动程序的主循环。类似的,RunApplicationEventLoop函数为Carbon程序启动主循环。如果你使用Xcode提供的模板创建你的程序,那你永远不需要自己去显式的调用这些例程。

对于辅助线程,你需要判断一个runloop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要再任何情况下都去启动一个线程的runloop。比如,你使用线程来处理一个月线定义的长时间运行的任务时,你应该避免启动runloop。runloop在你要和线程有更多的交互时才需要,比如以下情况:

1.使用端口或自定义输入源来和其它线程通信
2.使用线程的定时器
3.Cocoa中使用任何performSelector...方法
4.使线程周期性工作
如果你决定在程序中使用runloop,那么他的配置和启动都很简单。和所有线程编程一样,你需要好在辅助线程退出线程的情形。让线程自然退出往往比强制关闭它更好。

上一篇 下一篇

猜你喜欢

热点阅读