美文网首页
iOS RunLoop详解

iOS RunLoop详解

作者: 一片姜汁 | 来源:发表于2018-05-24 14:44 被阅读11次

    一.RunLoop介绍

    • 1.概念

    RunLoop是一个运行循环,正是因为RunLoop,IOS才可以保持程序的持续运行,处理App中的各种事件,并且可以节省CPU资源,提高性能(因为RunLoop可以做到工作休息两不误)。因为一般来讲,一个线程在处理完一个任务以后就会退出。

    线程与RunLoop

    • 每条线程都有唯一的一个与之对应的RunLoop对象
    • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要通过系统提供的方法进行获取
    • 获取RunLoop以后,如果没有事件源和Timer事件或者没有设置RunLoop运行模式,RunLoop会在获取以后立即销毁;如果超时,RunLoop也会被销毁。
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    UIApplicationMain内部就开启了一个RunLoop,这个函数是没有返回值的,因为RunLoop是一个运行循环。如果此处改为:
    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return 0;
        }
    }
    App在启动以后,就会结束运行。
    
    • 2.如何访问RunLoop对象

    • Foundation
      NSRunLoop
        [NSRunLoop currentRunLoop];//获取当前线程RunLoop,如果当前线程RunLoop未创建,则创建。
        [NSRunLoop mainRunLoop];//获取主线程RunLoop
    
    • Core Foundation
      CFRunLoopRef
        CFRunLoopGetCurrent();//获取当前线程RunLoop,如果当前线程RunLoop未创建,则创建。
        CFRunLoopGetMain();//获取主线程RunLoop
    

    NSRunLoop是CFRunLoopRef的OC封装

    • 3.RunLoop相关类介绍

    • CFRunLoopRef【RunLoop对象】
    • CFRunLoopModeRef【RunLoop的运行模式】
    • CFRunLoopSourceRef【RunLoop要处理的事件源】
    • CFRunLoopTimerRef【Timer事件】
    • CFRunLoopObserverRef【RunLoop观察者】
    RunLoop.jpg

    一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

    • CFRunLoopModeRef

    CFRunLoopModeRef代表RunLoop的运行模式(下面列举5种)

    NSDefaultRunLoopMode(Cocoa)/kCFRunLoopDefaultMode(Core Foundation):App的默认Mode,通常主线程是在这个Mode下运行
    UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    NSRunLoopCommonModes(Cocoa)/kCFRunLoopCommonModes(Core Foundation): 这是一个占位用的Mode,不是一种真正的Mode
    
    • 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
    • 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
    • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
      这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
    • kCFRunLoopCommonModes是一种模式组合,IOS系统中默认包含了kCFRunLoopDefaultMode和UITrackingRunLoopMode,系统会分别注册这两种模式,还可以通过CFRunLoopAddCommonMode()将自定义Mode放到kCFRunLoopCommonModes中。
    • CFRunLoopSourceRef
    • 按照官方文档的分类
      Port-Based Sources (基于端口,跟其他线程交互,通过内核发布的消息)
      Custom Input Sources (自定义)
      Cocoa Perform Selector Sources (performSelector…方法)
    • 按照函数调用栈的分类
      Source0:非基于Port的
      Source1:基于Port的

    Source0: event事件,只含有回调,需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop。
    Source1: 包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒 RunLoop 的线程。

    • CFRunLoopTimerRef

    CFRunLoopTimerRef是基于时间的触发器
    基本上说的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响
    GCD的定时器不受RunLoop的Mode影响

    • CFRunLoopObserverRef

    CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

    可以监听的RunLoop状态
    /* Run Loop Observer Activities */
    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
        kCFRunLoopAllActivities = 0x0FFFFFFFU//以上所有状态
    };
    

    监听RunLoop状态示例:

        //新建子线程
    - (void)viewDidLoad {
        [super viewDidLoad];
        //新建子线程
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadTask) object:nil];
        [self.thread start];
    }
    
    - (void)subThreadTask {
        //创建观察者
        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(@"即将处理Source");
                }
                    break;
                case kCFRunLoopBeforeWaiting:
                {
                    NSLog(@"即将进入休眠");
                }
                    break;
                case kCFRunLoopAfterWaiting:
                {
                    NSLog(@"即将从休眠中唤醒");
                }
                    break;
                case kCFRunLoopExit:
                {
                    NSLog(@"即将退出RunLoop");
                }
                    break;
                default:
                    break;
            }
        });
        //添加观察者
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        
        //释放资源
        CFRelease(observer);
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        self.timer = [NSTimer timerWithTimeInterval:2.0f target:self selector:@selector(timerActon) userInfo:nil repeats:YES];
        [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
        [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:4.0f]];
    }
    
    - (void)timerActon {
        NSLog(@"定时器工作了");
    }
    
    运行程序控制台输出:
    2018-05-24 09:32:04.495445+0800 Test[1059:57057] 即将进入RunLoop
    2018-05-24 09:32:04.499081+0800 Test[1059:57057] 即将处理Timer
    2018-05-24 09:32:04.502540+0800 Test[1059:57057] 即将处理Source
    2018-05-24 09:32:04.503239+0800 Test[1059:57057] 即将进入休眠
    2018-05-24 09:32:06.497401+0800 Test[1059:57057] 即将从休眠中唤醒
    2018-05-24 09:32:06.497973+0800 Test[1059:57057] 定时器工作了
    2018-05-24 09:32:06.498469+0800 Test[1059:57057] 即将处理Timer
    2018-05-24 09:32:06.498736+0800 Test[1059:57057] 即将处理Source
    2018-05-24 09:32:06.499872+0800 Test[1059:57057] 即将进入休眠
    2018-05-24 09:32:08.500044+0800 Test[1059:57057] 即将从休眠中唤醒
    2018-05-24 09:32:08.500392+0800 Test[1059:57057] 定时器工作了
    2018-05-24 09:32:08.500790+0800 Test[1059:57057] 即将退出RunLoop
    注:因为定时器是每两秒钟调用一次,子线程的runLoop在4秒钟以后会销毁,所以定时器会输出两次。4秒钟以后,runLoop会销毁,在销毁以前观察者会收到"即将推出RunLoop"的状态通知。
    

    二.RunLoop流程

    • 1.官方文档
      RunLoop官方流程图
      来自Apple官方文档翻译
    • 2.网友对官方文档的整理
      RunLoop流程

    三.RunLoop应用

    • 1.NSTimer
      创建一个tableView和一个定时器,定时器用于显示当前时间。当tableView静止的时候,定时器正常工作。当拖拽tableView的时候,定时器停止了工作。
      定时器的创建代码:
        //创建定时器 并指定RunLoop运行模式为NSDefaultRunLoopMode
        self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
    NSDefaultRunLoopMode下的定时器.gif

    如果需要在列表滑动时定时器继续工作,则需要指定RunLoop运行模式为NSRunLoopCommonModes

        //创建定时器 并指定RunLoop运行模式为NSRunLoopCommonModes
        self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    
    NSRunLoopCommonModes下的定时器.gif
    • 2.PerformSelector
      PerformSelector 可以指定在何种RunLoopMode下进行工作
        //testAction仅在当前RunLoop处于UITrackingRunLoopMode下工作,即ScrollView滚动的时候
        [self performSelector:@selector(testAction) withObject:nil afterDelay:2.0f inModes:@[UITrackingRunLoopMode]];
    
    • 3.常驻线程
      有时候我们需要让一个线程保活,即一直处于可以执行任务的状态。这时候我们就需要用到RunLoop。
      一般情况,一个线程在任务执行完毕以后,是不可以去执行其他工作的:
    - (IBAction)openSubThreadAndexecutingMethodA:(id)sender {
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(methodA) object:nil];
        [self.thread start];
    }
    
    - (IBAction)executingMethodB:(id)sender {
        [self performSelector:@selector(methodAB) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
    
    - (void)methodA {
        NSLog(@"方法A被执行");
    }
    
    - (void)methodAB {
        NSLog(@"方法B被执行");
    }
    
    点击按钮执行上述代码中的方法.png

    当我们点击"开启子线程并执行MethodA"进行子线程创建,并且让MethodA在子线程中执行。当MethodA执行完毕以后,我们如果点击
    “让子线程去执行MethodB”按钮,让MethodB在之前创建的子线程中去执行的话,程序就会崩溃。因为当MethodA被执行完毕以后,子线程已经处于finished状态,系统会将其释放。
    解决上述问题,让子线程进行常驻,随时可以执行任务:

    将上述methodA方法改为:
    - (void)methodA {
        //在当前子线程中开启一个RunLoop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
        NSLog(@"方法A被执行");
    }
    

    此时我们点击"开启子线程并执行MethodA"创建子线程,并且执行methodA。methodA方法是不会输出"方法A被执行"的。因为我们创建了一个持续运行的RunLoop。只有RunLoop结束的时候,此语句才会被输出。我们点击“让子线程去执行MethodB”按钮去执行methodB,程序正常运行。并且我们如果获取此子线程的状态,它处于正在运行的状态,这就达到了保活的目的。在开启RunLoop的时候,我们需要添加事件源或者Timer,否则RunLoop在获取以后,会立马运行结束。

    • 4.自动释放池

    第一次创建的时机:即将进入runloop的时候【kCFRunLoopEntry】
    释放的时机:runloop进入休眠状态【kCFRunLoopBeforeWaiting】,或者退出runLoop时【kCFRunLoopExit】。

    • 注: 当runloop即将休眠的时候会把之前的自动释放池释放,然后重新创建一个新的释放池。

    四.RunLoop参考资料

    相关文章

      网友评论

          本文标题:iOS RunLoop详解

          本文链接:https://www.haomeiwen.com/subject/aakqjftx.html