美文网首页iOS开发
iOS RunLoop,面试装13的神器

iOS RunLoop,面试装13的神器

作者: shenXs | 来源:发表于2019-01-02 11:11 被阅读7次
    图层0@2x.png

    今天我们来学习下iOS中一个较为重要的核心--RunLoop。其实我们对RunLoop既熟悉又陌生。熟悉是因为我们在开发中时不时的都会用到它,陌生是因为它较为底层,我们对它的了解不是多。今天我们就一起来揭开RunLoop神秘的面纱,对他进行一个较为简单的介绍。通过我的检点介绍,如果大家能对它有一定的了解和认识,那无论是在平时的工作中,还是在以后的面试中,它都是我们能拿上台面的一个利器。好,废话不多说,我们进入正题吧。

    1.RunLoop的简单介绍

    1.1 RunLoop的基本概念

    那么到底是什么RunLoop呢,从字面意思上可以得知,他就是“跑循环”,翻译的雅一点就是“运行的循环”。那么在iOS SDK中,RunLoop实际上也是一个对象,这个对象在循环的处理程序在运行过程中发出各种事件,例如,用户点击了屏幕(TouchEvent),UI界面的刷新事件,定时器事件(Timer),Selector事件等等。当我们将我们的程序退出到后台,注意只是退出到后台,并没有terminate,这时RunLoop不会处理任何事件,此时为了节省CPU的资源,RunLoop会自动进入休眠模式。

    1.2 RunLoop和线程的关系

    我们一谈论到RunLoop,其实我们就会提到与之息息相关的东西,线程。我们知道,线程的作用是用来执行一个或者多个特定任务的,一般情况下,某个线程的任务执行完毕,他就return掉。那么了,我们为了让线程能够不断执行我们指派的,或者系统分配任务,让其任务执行完不能退出,这时候我们就用到了RunLoop。
    一个线程对应唯一一个RunLoop对象,但是往往,RunLoop并不能保证我们线程安全,因为我们只能在当前线程中操作与之对应的RunLoop对象,而不能在当前线程中去操作其他线程的RunLoop对象。子线程的RunLoop对象,需要我们手动去创建和维护,当线程结束时,它也就销毁了。这里我们就会想到,那么主线程mainThread的RunLoop,是谁创建的呢?其实这个问题有点白痴:),是系统自动创建的。

    1.3 主线程的RunLoop

    当我们创建一个新工程的时候,我们找他的main.m,我们可以看到他的代码很简单,就一个main方法,代码如下:

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

    这个方法就是我们程序的入口,在这个方法中,首先标注了一个自动释放池autoreleasepool,然后再返回一个系统函数UIApplicationMain执行结果,其实这个UIApplicationMain函数就是为我们的程序创建了一个主线程的RunLoop,只要我们的程序不crash掉,那他就会一直运行循环,直到我们将程序退出到后台,或者terminate掉,它才会被挂起或是被销毁掉。
    我们再来看看,官方文档上是如何图解RunLoop的,如下图所示:

    640.jpeg
    从上图中我们可以看出,RunLoop实际就是一个循环,它会一直监听输入源InputSources和定时源TimerSources,一旦接收到事件,它便会对其进行处理,如果长时间没有检测到事件,那么它会自动进入休眠状态。

    2.RunLoop相关类介绍

    我们要想对RunLoop有跟进一步的理解,我则需要对CoreFoundation框架下的与RunLoop相关的5个类,他们分别是:

    • CFRunLoopRef:RunLoop对象
    • CFRunLoopMode:RunLoop的运行模式
    • CFRunLoopSourceRef:RunLoop监听的输入源(事件源)
    • CFRunLoopTimerRef:RunLoop监听的定时源(Timer源)
    • CFRunLoopObserverRef:观察者,监听RunLoop状态的变化
      接下来我们就来详细的说下上面的这5个类的具体内容和他们之间的关系。

    首先我们来看下5个类的关系,如下图所示,

    640.jpeg
    我们来简单解释下他们之间的关系,一个CFRunLoopRef对象包含若干个CFRunLoopMode运行模式,每个运行模式又包含若干个CFRunLoopSourceRef输入源、CFRunLoopTimerRef定时源、CFRunLoopObserverRef观察者。这里需要注意的是,虽然一个RunLoopRef对象可以包含若干个CFRunLoopMode运行模式,但是该RunLoopRef对象当前运行的模式只能是指定的它包含的若干个CFRunLoopMode运行模式中的一个,那么这个被指定的运行模式就叫做“当前运行模式”CurrentMode。但是CurrentMode是可以进行切换的,如果需要切换运行模式,则需要退出当前的Loop,然后重新指定CFRunLoopMode。这样做的目的其实也很容明白,因为每一个CFRunLoopMode包含若干个CFRunLoopSourceRefCFRunLoopTimerRefCFRunLoopObserverRef,为了能有效的隔离不同CFRunLoopModeCFRunLoopSourceRefCFRunLoopTimerRefCFRunLoopObserverRef,使其不同CFRunLoopSourceRef互不影响,所以一个CFRunLoopRef只能指定一个CurrentMode
    接下来,我们将逐一详细的来介绍与RunLoop密切相关的这5个类。
    2.1 CFRunLoopRef

    CFRunLoopRef是CoreFoundation内库中RunLoop对象类。我们可以以下方式来获取CFRunLoopRef对象。

    //  获取当前线程的CFRunLoopRef
    CFRunLoopRef runLoopRef = CFRunLoopGetCurrent();
    //  获取主线程的CFRunLoopRef
    CFRunLoopRef mainRunLoopRef = CFRunLoopGetMain();
    

    NSRunLoop是Foundation内库中RunLoop的对象。我们同样可以使用以下方法来回去NSRunLoop对象

    //  获取当前RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //  获取主线程的RunLoop
    NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
    
    2.2 CFRunLoopMode

    iOS系统第一了很多种运行模式,下面我们一一简单的介绍:

    • kCFRunLoopDefaultMode:App默认运行模式,通常我们的主线程的当前模式就是这个模式。
    • UITrackingRunLoopMode:跟踪用户交互事件(用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode的影响)。
    • UIInitializationRunLoopMode:当App刚启动的时候,系统将进入这个模式,启动完成后将切换到另外的运行模式。
    • GSEventReceiveRunLoopMode:接受系统内部事件,通常我们再开发的时候很少用这个运行模式。
    • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,我们在后面的内容中会介绍到这个运行模式。
      以上提及到的5种运行模式中,我们在开发过中常用到的是kCFRunLoopDefaultModeUITrackingRunLoopModekCFRunLoopCommonModes,那么下面就来讲讲这三种运行模式的具体用法。
    2.3 CFRunLoopTimerRef (Timer事件源)

    CFRunLoopTimerRef是我们上文提及到的定时源,他是RunLoop监听的事件源之一,在RunLoop相关类的关系图中我们提过它。我们可以简单的将其理解为基于时间的触发器。我们也可以将其理解我们开发时常用的NSTimer。
    接下来我们举一个简单的例子来演示一下CFRunLoopModeCFRunLoopTimerRef相结合的简单用法。
    1.首先我们再UIViewController.view上添加一个scrollView, 在scrollView上我们再添加一个Label, 然后我们再懒加载一个定时器, 然后我们启动定时器,定时器将每隔1秒修改Label.text,代码如下:

    - (void)viewDidLoad {
      [self.view addSubview:self.scrollView];
      [self.scrollView addSubview: self.label];
      [self.timer fire];
    }
    
    - (void)on_timer {
        _num += 1;
        self.label.text = [NSString stringWithFormat:@"%zd", _num];
    }
    
    //  懒加载一个定时器
    - (NSTimer *)timer {
        if (!_timer) {
            _timer = ({
                NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(on_timer) userInfo:nil repeats:YES];
                timer;
            });
        }
        return _timer;
    }
    
    

    我们运行我们的程序,我们不做任何操作,我们可以看到我们的label.text会每隔一秒钟就变化一次。如果此时我们滑动我们的scrollView,我们就会发现label.text将不会再发生改变,当我们停止滑动,手指离开屏幕的时候,label.text又开始继续发生改变。那么到底是什么原因到导致这样的结果呢?
    当我们再初始化Timer的时候,没有手动将Timer注入到某种运行模式下,此时系统会默认将其注入到NSDefaultRunLoopMode,所以我们运行程序,且不做任何操作的时候,此时RunLoop是当currentMode是NSDefaultRunLoopMode,Timer正常工作。当我们滑动scrollView的时候,此时系统会将currentMode切换到UITrackingRunLoopMode,我们的Timer并没有注入到这个模式下,所以Timer失效。当我们的滑动结束,手指离开屏幕的时候,currentMode又切换到NSDefaultRunLoopMode,此时Timer又正常工作。那么我么在初始化Timer的时候,可以将其注入到指定的运行模式下吗?是可以的,代码如下:

    //  手动的将Timer注入到UITrackingRunLoopMode模式下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    

    此时我们手动将Timer注入到UITrackingRunLoopMode,然后我们运行程序,其实显而易见,此时我们不做任何操作,Timer是不会正常工作的,当我们滑动scrollView的时候,Timer开始正常工作,原因与上同理。这里我来问个S13问题:), 难倒我们就不能在这两种模式下让Timer都能正常工作吗?(这个问题真的S13)。
    答案是当然可以的,其实我是为了引出伪模式(kCFRunLoopCommonModes)。kCFRunLoopCommonModes其实都不能算是一种运行模式,它只是一种标记,它可以对其他运行模型进行标记。说道这里,大家可能都明白了系统的NSDefaultRunLoopModeUITrackingRunLoopMode都是被标记上Common modes。所以我们只需要将我们的timer手动注入到kCFRunLoopCommonModes下,那么此时无论系统的currentMode是default还是UITracking,timer将都能正常工作。修改代码如下:

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

    这里我们说道了Timer,那么我就顺便就说说这个timer
    NSTimer有两个便利构造函数入下:

    //  构造函数1
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    //  构造函数2
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    

    构造函数1,返回的timer对象会自动注入到NSDefaultRunLoopMode,其实相当于构造函数1,再手动注入到运行模式中,例如:

    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(...) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    2.4 CFRunLoopSourceRef (Input事件源)

    所有的事件源有两种分类方法,我们来具体看看他哪两种分类:

    1. 按照官方文档来分类(和前面我贴出的官方RunLoop模型图里一样):
      Port-Based Sources :基于端口事件
      Custom Input Sources:用户自定义事件

    2. 按照函数调用栈来分类:
      Source0:非基于端口事件
      Source1:基于端口,通过内核和其他线程通信、接收、分发系统事件

    其实根本一点讲,这两种分类是没有区别的,只不过呢,第一种是根据官方给出的理论来进行分类的。第二是我们在实际应用中通过函数的调用来进行分类的。

    2.5 CFRunLoopObserverRef (I观察者类)

    RunLoop的状态会根据用户的操作以及程序状态的变化而发生改变,通常我们会去监听RunLoop的实时变化,此时我们就会用到一个观察者类CFRunLoopObserverRef。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
        kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
    };
    

    下面我们用个小小的例子来观察下,RunLoop的状态到底是怎么变化的,以及我们如何使用CFRunLoopObserverRef来监听RunLoop的状态变化。
    首先我们在viewDidLoad中添加如下代码:

    CFRunLoopObserverRef observerRef =
        CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),
                                           kCFRunLoopAllActivities,
                                           YES,
                                           0,
                                           ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            NSLog(@"监听到RunLoop发生改变---%zd",activity);
        });
        
        // 2.append Observer to RunLoop
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observerRef, kCFRunLoopDefaultMode);
        
        // 3.release observer
        CFRelease(observerRef);
    

    然后我们运行我们的程序,然后控制台会打印出一大串信息,这里我只贴出最后部分打印信息:

    2018-12-25 10:36:03.293175+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---2
    2018-12-25 10:36:03.293363+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---4
    2018-12-25 10:36:03.293517+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---32
    2018-12-25 10:37:00.032407+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---64
    2018-12-25 10:37:00.036248+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---2
    2018-12-25 10:37:00.036335+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---4
    2018-12-25 10:37:00.036392+0800 TestApp[12836:1611962] 监听到RunLoop发生改变---32
    

    我们可以看到,RunLoop的状态是在不断的变化的,他最后的状态是32,也就是我们上面的枚举中的kCFRunLoopBeforeWaiting,RunLoop即将进入休眠。

    3.RunLoop的原理介绍

    OK,我们上面讲解了RunLoop的基本概念,以及与RunLoop相关的几个类的介绍,接下来我们将具体的来说一说RunLoop的原理。它到底是怎么运作,希望通过下面讲解,大家都可以了解RunLoop的内部运行逻辑。

    这里我贴一张我自己画的图,简单的描述了一下RunLoop的运行逻辑,如下图所示:


    屏幕快照 2018-12-25 上午11.38.44.png

    上图是我自己按照自己的理解画的RunLoop运行逻辑图,可能对大家在理解RunLoop的运行逻辑的时候有一点帮助,下面我们来看看官方文档对RunLoop的运行逻辑是如何阐述的。

    当我们运行我们的程序的时候,所有线程的RunLoop会同时被唤醒,并且开始自动处理之前未处理完的事件,通知也发出通知,通知其相应的Observer。详细的顺序如下:

    1. 通知观察者RunLoop已经启动
    2. 通知观察者即将要开始的定时器
    3. 通知观察者任何即将启动的非基于端口的源
    4. 启动任何准备好的非基于端口的源
    5. 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
    6. 通知观察者线程进入休眠状态
    7. 将线程置于休眠知道任一下面的事件发生:
      某一事件到达基于端口的源
      定时器启动
      RunLoop设置的时间已经超时
      RunLoop被显示唤醒
    8. 通知观察者线程将被唤醒
    9. 处理未处理的事件
      如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
      如果输入源启动,传递相应的消息
      如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
    10. 通知观察者RunLoop结束。
      以上就是官方对RunLoop的运行逻辑给出的说明,我是直接用有道翻译出来的,没有去做校验,大家可以对照我上面贴出的图,大致看下RunLoop的运行逻辑。

    4.RunLoop开发应用

    上面说一大堆,很多都是概念性东西,要想真正的掌握RunLoop我们还是得到实际运用中。通过在实际开发过程中的实际应用,我们对RunLoop的认识才能更加深刻。

    4.1 NSTimer的应用

    NSTimer,我们上面的2.3CFRunLoopTimerRef (Timer事件源)中已经提到过有关NSTimer和RunLoop的关系,这里呢,我们就不再重复了。

    4.2 开启常驻线程

    在我们的实际开发过程中,有时候可能会遇到在后台频繁操作的的一些需求,例如,文件现在,音乐播放,实时定位等等,这些操作经常会在子线程做一些耗时的操作,此时,我们就可以应用RunLoop将这些耗时的子线程放到我们的常驻线程中去。

    具体的操作室,代码如下:
    1.首先我们开启一条子线程:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 初始化线程,执行任务run1
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
        [self.thread start];    
    }
    
    - (void) run1
    {
        NSLog(@"----run1-----");
    
        // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    
        // 如果开启RunLoop,则不会执行下面这句打印,因为RunLoop开启了循环。
        NSLog(@"未开启RunLoop");
    }
    

    我们运行我们的程序,控制并没有打印 "未开启RunLoop", 说明我们开启了一条常驻线程,此时我们再往线程中添加一些子任务代码如下:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {   
        // 在self.thread的线程中执行任务run2
        [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    - (void) run2
    {
        NSLog(@"----run2------");
    }
    

    此时我们运行我们的程序,每当我们点击我们的屏幕的时候,控制台都会打印“----run2------”,这样我们就实现了常驻线程的需求。当然这里我只是简单的给大家演示了一下常驻线程的例子,那么大家可以根据自己不同是实际开发需求,来进行编码,并将RunLoop特性发挥起来。

    以上就是对RunLoop的基本概念,相关类,运行逻辑,以及实际应用的简单介绍,当然我个人是水平也是比较次,有不对的,或者不准确的地方,希望大家留言指正。

    相关文章

      网友评论

        本文标题:iOS RunLoop,面试装13的神器

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