iOS开发代码段iOS开发 oc中重要知识点iOS Runloop

iOS之RunLoop详解与实践

2016-05-18  本文已影响2023人  StarkShen

目录

-RunLoop的概念

-RunLoop逻辑与实现

-RunLoop在iOS中运用

-RunLoop实践

-RunLoop的概念

苹果在文档里,是这样定义RunLoop的 :

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.

RunLoop是与线程相关的基础功能,RunLoop是用于调度工作,并协调接收传入事件的事件处理循环。RunLoop的目标是让线程有任务时工作,没有任务处理时休眠。

RunLoop与线程的关系
线程在处理完自己的任务后一般会退出,为了实现线程不退出能够随时处理任务的机制被称为EventLoopnode.js 的事件处理,windows程序的消息循环,iOS、OSX的RunLoop都是这种机制。
线程和RunLoop是一一对应的,关系保存在全局的字典里。
在主线程中,程序启动时,系统默认添加了有kCFRunLoopDefaultModeUITrackingRunLoopMode两个预置ModeRunLoop,保证程序处于等待状态,如果接收到来自触摸事件等,就会执行任务,否则处于休眠中。
线程创建时并没有RunLoop,(主线程除外),RunLoop不能创建,只能主动获取才会有。RunLoop的创建是在第一次获取时,RunLoop的销毁是发生在线程结束时。只能在一个线程中获取自己和主线程的RunLoop
RunLoop的挂起与唤醒堆栈调用

FD7A79D6-A7C8-40D7-B59E-10F36213E8E4.jpg
指定用于唤醒的 mach_port 端口调用 mach_msg 监听唤醒端口,被唤醒前系统内核将这个线程挂起,停留在mach_msg_trap状态。
由另一个线程向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续工作。

一个运行环接收来自两个不同类型的源事件。输入源传递异步事件,通常消息从另一个线程或从不同的应用程序。定时源提供的同步事件,在预定的时间发生的或重复间隔。这两种类型的源的使用应用特定处理例程,当它到达处理该事件。

EE37B277-0C9E-4C31-B8E1-719F7397D0D9.jpg
上图显示了一个运行循环和各种来源的概念结构:
输入源传递异步事件到相应的处理程序,并调用runUntilDate:方法(称为线程的相关NSRunLoop对象)退出。Timer 定时源提供的事件他们的处理程序例程,但不会导致运行循环退出。

-RunLoop逻辑与实现

3026BBF4-3C5A-4501-B6A0-29413F9DF308.png

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0Source1
Source0只包含了一个回调(函数指针),并不能主动触发事件,需要手动触发,
需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

CFRunLoopTimerRef 是基于时间的触发器,可以和NSTimer 混用,包含一个时间长度和回调,加入
RunLoop时,RunLoop会注册对应的时间点,当时间点时,RunLoop会被唤醒以执行那个回调

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 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
};


// 用DefaultMode启动
void CFRunLoopRun(void) {                   
CFRunLoopRunSpecific(CFRunLoopGetCurrent(),        
kCFRunLoopDefaultMode, 1.0e10, false);
}
// 用指定的Mode启动,允许设置RunLoop超时时间int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { 
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
// RunLoop的实现int    CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) { 
// 首先根据modeName找到对应mode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); 
// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode))
return; 
// 1. 通知 Observers: RunLoop 即将进入 loop。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 内部函数,进入loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) { 
Boolean sourceHandledThisLoop = NO; int retVal = 0; do { 
// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); 
// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); 
// 执行被加入的block __CFRunLoopDoBlocks(runloop, currentMode);
// 4. RunLoop 触发 Source0 (非port) 回调。 sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); 
// 执行被加入的block __CFRunLoopDoBlocks(runloop, currentMode);
// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) { 
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) 
if (hasMsg) goto handle_msg;
} 
// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) { 
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。 
//  一个基于 port 的Source 的事件。
//  一个 Timer 到时间了 
//  RunLoop 自身的超时时间到了 
//  被其他什么调用者手动唤醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); 
// thread wait for receive msg } 
// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
// 收到消息,处理消息。 handle_msg: 
// 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); 
}
// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件 else { 
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort); 
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); 
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply); 
}
}
// 执行加入到Loop的block __CFRunLoopDoBlocks(runloop, currentMode); 
if (sourceHandledThisLoop && stopAfterHandle) { 
// 进入loop时参数说处理完事件就返回。 
retVal = kCFRunLoopRunHandledSource; 
} else if (timeout) { 
// 超出传入参数标记的超时时间了 
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) { 
// 被外部调用者强制停止了 retVal = kCFRunLoopRunStopped; 
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) { 
// source/timer/observer一个都没有了 
retVal = kCFRunLoopRunFinished; 
} 
// 如果没超时,mode里没空,loop也没被停止,那继续loop。 
}
while (retVal == 0); 
}
// 10. 通知 Observers: RunLoop 即将退出。          __CFRunLoopDoObservers(url, currentMode, kCFRunLoopExit);
}
void CFRunLoopRun(void) { 
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
0A1E5A26-9719-459F-A07E-8B1ED0D28160.jpg

-RunLoop在iOS中运用

Run Loop的作用
1.使程序一直运行并接受用户输入
2.决定程序在何时应该处理那些Event
3.调用解耦(Message Queue)
4.节省CPU时间

何时调用
1.使用端口port或自定义输入源input sources和其他线程通信
2.使用线程的定时器timer
3.Cocoa中使用任何performSelector的方法
4.线程执行周期性任务

RunLoop应用场景
AutoreleasePool

程序启动时,苹果在主线程的RunLoop中添加了两个Observer用于监视RunLoopkCFRunLoopEntry(唤起)和BeforeWating(准备休眠)

PerformSelecter
调用PerformSelecter:afterDelay方法时,系统会默认创建一个timer添加到当前线程中去,如果在线程中调用该方法,而没有获取currentRunLoop,则会失效

GCD
GCD与RunLoop的部分实现相互用到了对方,RunLooptimer是用dispatch_source_t实现的,而GCD的dispatch_async()也用到了RunLoop
当调用dispatch_async(dispatch_get_main_queue(), block)时,dispatch会向主线程RunLoop发送消息,RunLoop会被唤醒,并从消息中获取block后回调

定时器 NStimer
NSTimer实际上就是CFRunLoopTimerRefRunLoop不会非常准确的时间回调,延迟到程度跟当前线程是否繁忙有关,当线程空闲时,timer比较准时,线程繁忙时延迟会比较明显。Timer有个属性Tolerance宽容度,到达了某个时间点后容许有多少误差。
當創建一個Timer並添加到DefaultMode 時,Timer就會重復回調,此時滑動TableView時,RunLoop會將mode切換成TrackingRunLoopMode這時Timer就會被回調,並且也不會影響到滑動操作。如果需要Timer在多個mode下得到回調,有一個辦法就是將Timer分別加入這兩個Mode中,或者加入到頂層RunLoop的commonModeItems中去,commonModeItems被RunLoop自動更新到具有Common屬性的Mode里去。

网络请求
在基于CFNetworkNSURLConnection发起的NSURLConnectionLoader线程中,runloop通过基于mach portSource0接收来自底层CFSocket的通知,同时唤醒delegateRunLoop来处理这些通知。
事件响应 用到了 __IOHIDEventSystemClientQueueCallback()
手势识别 用到了_UIGestureRecognizerUpdateObserver()
界面更新 用到了_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
等等

RunLoop实践

// RunLoop - 创建生命周期跟App相同的常驻线程

#pragma mark - 常驻线程
- (void)viewWillAppear:(BOOL)animated
{
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(longRunLoop) object:nil];
    [thread start];
}
- (void)longRunLoop
{

    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

}

//RunLoop - 维护线程的生命周期,让线程不自动退出,isFinished为Yes时退出。 在启动RunLoop之前,必须添加监听的Port或输入源事件sources或者定时源事件timer,否则调用[runloop run]会直接返回,而不会进入循环让线程长驻。如果没有添加任何输入源事件或Timer事件,线程会一直在无限循环空转中,会一直占用CPU时间片,没有实现资源的合理分配。没有while循环且没有添加任何输入源或Timer的线程,线程会直接完成,被系统回收。

 NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
@autoreleasepool {
        [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    }
}

// RunLoop - 不断检测直到满足条件

#pragma mark - 不断检测直到满足条件
- (NSString *)userAgentString
{
NSString *string;

while (string == nil)
{
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

    return string;
}

// tableView延迟加载图片的新思路

#pragma mark - 图片延迟加载
- (void)imageViewLoad{

    //ImageView的显示 滑动时不加载 只在NSDefaultRunLoopMode模式下显示图片
    UIImageView *img;
    
    [img performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

}

// RunLoop - 在一段时间内每隔一会执行某种任务的线程

//Run Loop 有 [acceptInputForMode:beforeDate:] 和[runMode:beforeDate:]方法来指定在一时间之内运行模式。如果不指定时间,Run Loop默认会运行在Default模式下(不断重复调用runMode:NSDefaultRunLoopMode beforeDate:...) 例如需要在应用启动之后,在一定时间内持续更新某项数据。
-(void)loopEvent{
@autoreleasepool {
    //在30分钟内,每隔30s执行 run 方法
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30
                                                    target:self
                                                  selector:@selector(run)                                                  userInfo:nil
                                                   repeats:YES];
    [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]];
}
}

// RunLoop - NSTimer 在不同模式下工作

    NSTimer *timer = [NSTimer timerWithTimeInterval:3.0
                                             target:self
                                           selector:@selector(run)
                                           userInfo:nil
                                            repeats:YES];
    // timerWithTimeInterval: 方式创建的如果不加入RunLoop,则不会执行
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

/或者/

    NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                   target:self
                                                 selector:@selector(doSomeThing)
                                                 userInfo:nil
                                                  repeats:YES];
                                                  
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

     //或 [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

    [timer fire];

// RunLoop - NSTimer 在线程中创建一个定时器

- (void)viewDidLoad {

    [super viewDidLoad];
    
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(createTimer) object:nil];
    
    [thread start];
}

- (void)createTimer{

    [NSTimer scheduledTimerWithTimeInterval:1.0
                                 target:self
                               selector:@selector(run)
                               userInfo:nil
                                repeats:YES];

    [[NSRunLoop currentRunLoop] run];
}

// RunLoop - ReactNative 框架中RCTWebSocket中用到的RunLoop

#pragma mark - ReactNative 框架中RCTWebSocket中用到的RunLoop
NSRunLoop *_runLoop;
dispatch_group_t _waitGroup;
- (void)main
{

_waitGroup = dispatch_group_create();

dispatch_group_enter(_waitGroup);

@autoreleasepool {
    _runLoop = [NSRunLoop currentRunLoop];
    dispatch_group_leave(_waitGroup);
    
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:self selector:@selector(step) userInfo:nil repeats:NO];
    [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
    
    while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { }
    assert(NO);
}
}
- (void)step
{
// Does nothing
}
- (NSRunLoop *)runLoop;
{
    dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER);
    return _runLoop;
}

// RunLoop AFNetWorking之 AFURLRequestSerilization

#pragma mark - AFNetWorking之 AFURLRequestSerilization
//额外提供的一个方法,把request里的bodyStream写到文件
//即可以把multipart发送的内容先生成好保存到文件

- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
                             writingStreamContentsToFile:(NSURL *)fileURL
                                       completionHandler:(void (^)(NSError *error))handler
{
    NSParameterAssert(request.HTTPBodyStream);
    NSParameterAssert([fileURL isFileURL]);
    
    NSInputStream *inputStream = request.HTTPBodyStream;
    NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO];
    __block NSError *error = nil;
    
    //
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //新开一条线程做这个事,stream的read和write是阻塞的
    
    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    
    /*
     *NSInputStream子类
     NSURLRequest的setHTTPBodyStream接受的是一个NSInputStream*参数,那我们要自定义inputStream的话,创建一个NSInputStream的子类传给它是不是就可以了?实际上不行,这样做后用NSURLRequest发出请求会导致crash,提示[xx _scheduleInCFRunLoop:forMode:]: unrecognized selector。
     这是因为NSURLRequest实际上接受的不是NSInputStream对象,而是CoreFoundation的CFReadStreamRef对象,因为CFReadStreamRef和NSInputStream是toll-free bridged,可以自由转换,但CFReadStreamRef会用到CFStreamScheduleWithRunLoop这个方法,当它调用到这个方法时,object-c的toll-free bridging机制会调用object-c对象NSInputStream的相应函数,这里就调用到了_scheduleInCFRunLoop:forMode:,若不实现这个方法就会crash。详见这篇文章。
     */
    
    [inputStream open];
    [outputStream open];
    
    while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) {
        uint8_t buffer[1024];
        
        NSInteger bytesRead = [inputStream read:buffer maxLength:1024];
        if (inputStream.streamError || bytesRead < 0) {
            error = inputStream.streamError;
            break;
        }
        
        NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead];
        if (outputStream.streamError || bytesWritten < 0) {
            error = outputStream.streamError;
            break;
        }
        
        if (bytesRead == 0 && bytesWritten == 0) {
            break;
        }
    }
    //NSURLRequest怎么知道数据读完了?
    //应该是这个函数返回0时外部就知道stream已经读完,调用它的close方法。
    
    [outputStream close];
    [inputStream close];
    
    if (handler) {
        dispatch_async(dispatch_get_main_queue(), ^{
            handler(error);
        });
    }
});

//

NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest.HTTPBodyStream = nil;

return mutableRequest;
}

//AFNetworking之前的版本中AFURLConnectionOperationRunLoop用到了常驻线程

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
   [[NSThread currentThread] setName:@"AFNetworking"];
      // 为了使接收delegate回调能够在后台线程中执行,且该线程不会提前被回收
   NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    // 在这里通过监听MachPort使线程不会回收,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;
}

//

1432798974517485.png

相关资料:
官方文档 NSRunLoop Class Reference , CFRunLoop Reference.
CFRunLoopRef 的代码是开源的,你可以在这里http://opensource.apple.com/tarballs/CF/ 下载到整个 CoreFoundation 的源码来查看。

Swift 版跨平台的 CoreFoundation :https://github.com/apple/swift-corelibs-foundation/,这个版本的源码可能和现有 iOS 系统中的实现略不一样,但更容易编译,而且已经适配了 Linux/Windows。

上一篇下一篇

猜你喜欢

热点阅读