iOSRunloop技术

深入浅出 RunLoop

2017-12-25  本文已影响98人  ZhengYaWei

前言

文章主要分为四个部分

一、RunLoop 简介

1.1 RunLoop 基本概念

一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop 机制能让线程随时处理事件但并不退出。这里说的随时是指:程序需要运行时就保持程序的持续运行,不需要的时候就进入休眠状态。

NSRunLoop 和 CFRunLoopRef 都是和RunLoop 机制相关的类。CFRunLoopRef 基于 CoreFoundation 框架内,是纯 C 函数的 API,所有这些 API 都是线程安全的。CFRunLoopRef 的代码是开源的。NSRunLoop 是基于 CFRunLoopRef ,提供了面向对象的 API,但是这些 API 不是线程安全的。

1.2 RunLoop 和 线程的关系

关于RunLoop 和线程之间的关系要知道以下几点:

1.3 为什么 main 函数不会 return掉 ?

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

上面main函数同一般函数相比,启动程序后并不会立刻 return 掉。其中UIApplicationMain函数内部默认开启了主线程的 RunLoop ,并执行了一段无限循环的代码。UIApplicationMain函数一直没有返回,所以运行程序之后会保持持续运行状态。

//无限循环代码模式
int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
        // ......
    } while (running);

    return 0;
}

二、RunLoop 相关接口

2.1 RunLoop 的结构

和 RunLoop 相关的主要涉及五个类:

RunLoop的结构

从上图可以看出,RunLoop 对象中可以包含多个 Mode,每个 Mode 又包含多个个 Source、Timer、Observer。

2.2 RunLoop 中的 Mode

关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。

总共是有五种Mode:

有这样一个场景,假设自己封装一个无限轮播视图,很有可能会出现这样一种情况:当你滑动轮播视图时,轮播视图的定时器不再起作用,不能通过定时器调整UIScrollView的偏移值。之所以会出项上述现象,是因为主线程的 RunLoop 里有两个 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。默认情况下是defaultMode,但是当滑动UIScrollView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回。如果想在滑动的时候不让定时器失效,可以使用CommonMode来解决。

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

2.3 Mode 中的 CFRunLoopSourceRef

CFRunLoopSourceRef是事件源,主要有两种分类方式,一种是苹果官方的分类方式,另一种是按照函数调用栈栈分类方式。

2.3.1 官方分类
2.3.2 按照函数调用栈分类
函数调用栈分类举例
创建一个按钮,添加点击事件,并在按钮回调事件添加断点,当执行到断点出左侧会出现相关栈调用信息。从上图可以看出:点击事件就是在Sources0中处理的。至于 Source1 主要是用来接收、分发系统事件,然后再分发到Sources0中处理。

2.4 Mode 中的 CFRunLoopTimerRef

CFRunLoopTimerRef 是定时源,你可以简单把它理解为NSTimer。其包含一个时间点和一个回调(函数指针)。当被加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间到时,RunLoop 会执行对应时间点的回调。

2.5 Mode 中的 CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,主要用来监听RunLoop 的状态,主要有以下几种状态。

可以通过以下代码验证RunLoop的几种状态:

    // 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"监听到RunLoop发生改变---%zd",activity);
    });
    // 添加观察者到当前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    // 释放observer
    CFRelease(observer);

三、RunLoop 相关逻辑流程

RunLoop 逻辑流程

上图是笔者从网上找到的一张 RunLoop 运行的相关流程逻辑图。具体来说主要执行逻辑是这样的:

四、RunLoop 实际应用

4.1 后台常驻线程

借助RunLoop可以实现线程后台常驻的功能,关键是在于两行代码,具体请看如下代码。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runOne) object:nil];
    [self.thread start];
}
- (void) runOne{
    NSLog(@"----任务1-----");
    // 下面两句代码可以实现线程保活
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"未开启RunLoop");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(runTwo) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) runTwo{
    NSLog(@"----任务2------");
}

实现了上述代码之后,每次点击屏幕都会打印----任务2------,这说明子线程处于活跃状态。

在一些分析AFNetworking源码的文章中,也经常会出现如下这些代码。其核心也是为了实现线程后台常驻。

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [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;
}

当后台线程执行任务时,通过 performSelector:onThread:..方法将任务放在后台线程的 RunLoop 中。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

4.2 AutoreleasePool

应用程序一旦启动,主线程 RunLoop 里注册了两个 Observer。一个 Observer 监听即将进入Loop事件,回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池,并保证创建释放池发生在其他所有回调之前。另外一个 Observer 监视了两个事件(RunLoop即将进入休眠和即将退出 RunLoop 事件) ,前者会调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;后者会调用 _objc_autoreleasePoolPop() 来释放自动释放池,并保证释放自动释放池事件发生在其它回调之后。

上一篇 下一篇

猜你喜欢

热点阅读