iOS面试题:runloop 的 mode 作用是啥?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS面试题:runloop 的 mode 作用是啥?相关的知识,希望对你有一定的参考价值。

参考技术A

在 CoreFoundation 里面关于 RunLoop 有 5 个类,分别对应不同的概念:

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

这些概念的包含关系如下图所示:

线程的运行的过程中需要去处理不同情境的不同事件,mode 则是这个情景的标识,告诉当前应该响应哪些事件。一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopMode 和 CFRunLoop 的结构大致如下:

这里有个概念叫 CommonModes:一个 Mode 可以将自己标记为 Common 属性(通过将其 ModeName 添加到 RunLoop 的 commonModes 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 Common 标记的所有 Mode 里。

应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为 Common 属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个 TableView 时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作,因为这个 Timer 作为一个 mode item 并没有被添加到 commonModeItems 里,所以它不会被同步到其他 Common Mode 里。

有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 commonModeItems 中。commonModeItems 被 RunLoop 自动更新到所有具有 Common 属性的 Mode 里去。

CFRunLoop 对外暴露的管理 Mode 接口只有下面 2 个:

Mode 暴露的管理 mode item 的接口有下面几个:

你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

苹果公开提供的 Mode 有两个,你可以用这两个 Mode Name 来操作其对应的 Mode:

同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 Common。使用时注意区分这个字符串和其他 mode name。

更多: iOS面试题合集

iOS开发RunLoop学习:四:RunLoop的应用和RunLoop的面试题

一:RunLoop的应用

 

#import "ViewController.h"

@interface ViewController ()
/** 注释 */
@property (nonatomic, strong) NSThread *thread;
@end

@implementation ViewController

/**
 * 1:用NSThread创建线程的时候,不要忘记调用start方法来开启线程,在一条线程中的任务执行的顺序是同步的,串行执行,并且当线程中的任务执行完毕后,此条线程就会被销毁,若是强引用该线程,则也不会保住该线程的命。若是想线程执行完任务后不被销毁,可以开启常驻线程,创建RunLoop,创建RunLoop后其运行模式mode为默认的运行模式,也可以重新设置RunLoop的运行模式,设置RunLoop的timer或是source(否则RunLoop会立即退出),最后开启RunLoop,开启RunLoop的时候,可以可RunLoop设置销毁的时间。
 
 2:保证runloop不退出:1:创建NSTimer:NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
     [runloop addTimer:timer forMode:NSDefaultRunLoopMode];
                    2: [runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];开启source0事件
 3:开启runloop:runloop默认是关闭的:[runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
 
 4:Runloop中自动释放池的创建和释放:
 
 第一次创建:启动runloop
 最后一次销毁:runloop退出的时候
 其他时候的创建和销毁:当runloop即将睡眠的时候销毁之前的释放池,重新创建一个新的
 *
 */
- (IBAction)createBtnClick:(id)sender {
    
    //1.创建线程
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(task1) object:nil];
    
    [self.thread start];
    
}
- (IBAction)otherBtnClick:(id)sender {
    
    //[self.thread start];
    
    [self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];
}

-(void)task1
{
    NSLog(@"task1---%@",[NSThread currentThread]);
//    while (1) {
//       NSLog(@"task1---%@",[NSThread currentThread]);
//    }
    //解决方法:开runloop
    //1.获得子线程对应的runloop
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    
    //保证runloop不退出
    //NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //[runloop addTimer:timer forMode:NSDefaultRunLoopMode];
    [runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    //2.默认是没有开启
    [runloop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
    
    NSLog(@"---end----");
}

-(void)task2
{
    NSLog(@"task2---%@",[NSThread currentThread]);
}

-(void)run
{
    NSLog(@"%s",__func__);
}

//Runloop中自动释放池的创建和释放
//第一次创建:启动runloop
//最后一次销毁:runloop退出的时候
//其他时候的创建和销毁:当runloop即将睡眠的时候销毁之前的释放池,重新创建一个新的
@end

 

二:面试题:

 

 

####1.Runloop基础知识

- 1.1 字面意思

 

a 运行循环

b 跑圈

 

- 1.2 基本作用(作用重大)

 

a 保持程序的持续运行(ios程序为什么能一直活着不会死)

b 处理app中的各种事件(比如触摸事件、定时器事件【NSTimer】、selector事件【选择器·performSelector···】)

c 节省CPU资源,提高程序性能,有事情就做事情,没事情就休息

 

- 1.3 重要说明

 

        (1)如果没有Runloop,那么程序一启动就会退出,什么事情都做不了。

        (2)如果有了Runloop,那么相当于在内部有一个死循环,能够保证程序的持续运行

        (2)main函数中的Runloop

        a 在UIApplication函数内部就启动了一个Runloop

        该函数返回一个int类型的值

        b 这个默认启动的Runloop是跟主线程相关联的

 

- 1.4 Runloop对象

 

        (1)在iOS开发中有两套api来访问Runloop

            a.foundation框架【NSRunloop】

            b.core foundation框架【CFRunloopRef】

        (2)NSRunLoop和CFRunLoopRef都代表着RunLoop对象,它们是等价的,可以互相转换

        (3)NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)

 

 

- 1.5 Runloop参考资料

 

```objc

(1)苹果官方文档

https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

 

(2)CFRunLoopRef开源代码下载地址:

http://opensource.apple.com/source/CF/CF-1151.16/

 

```

 

- 1.6 Runloop与线程

 

1.Runloop和线程的关系:一个Runloop对应着一条唯一的线程

    问题:如何让子线程不死

    回答:给这条子线程开启一个Runloop

2.Runloop的创建:主线程Runloop已经创建好了,子线程的runloop需要手动创建

3.Runloop的生命周期:在第一次获取时创建,在线程结束时销毁

 

- 1.7 获得Runloop对象

 

```objc

1.获得当前Runloop对象

    //01 NSRunloop

     NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];

    //02 CFRunLoopRef

    CFRunLoopRef runloop2 =   CFRunLoopGetCurrent();

 

2.拿到当前应用程序的主Runloop(主线程对应的Runloop)

    //01 NSRunloop

     NSRunLoop * runloop1 = [NSRunLoop mainRunLoop];

    //02 CFRunLoopRef

     CFRunLoopRef runloop2 =   CFRunLoopGetMain();

 

3.注意点:开一个子线程创建runloop,不是通过alloc init方法创建,而是直接通过调用currentRunLoop方法来创建,它本身是一个懒加载的。

4.在子线程中,如果不主动获取Runloop的话,那么子线程内部是不会创建Runloop的。可以下载CFRunloopRef的源码,搜索_CFRunloopGet0,查看代码。

5.Runloop对象是利用字典来进行存储,而且key是对应的线程Value为该线程对应的Runloop。

 

```

- 1.8 Runloop相关类

 

(1)Runloop运行原理图

 

![PNG](2.png)

 

(2)五个相关的类

 

a.CFRunloopRef

b.CFRunloopModeRef【Runloop的运行模式】

c.CFRunloopSourceRef【Runloop要处理的事件源】

d.CFRunloopTimerRef【Timer事件】

e.CFRunloopObserverRef【Runloop的观察者(监听者)】

 

(3)Runloop和相关类之间的关系图

 

 ![PNG](1.png)

 

(4)Runloop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\\observer\\timer,至少要有其中的一个。

 

- CFRunloopModeRef

 

    1.CFRunloopModeRef代表着Runloop的运行模式

    2.一个Runloop中可以有多个mode,一个mode里面又可以有多个source\\observer\\timer等等

    3.每次runloop启动的时候,只能指定一个mode,这个mode被称为该Runloop的当前mode

    4.如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入

    5.这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响

    6.系统默认注册了5个mode

        a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行

            b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

            c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用

            d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

            e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

 

 

- CFRunloopTimerRef

 

(1)NSTimer相关代码

```objc

/*

说明:

(1)runloop一启动就会选中一种模式,当选中了一种模式之后其它的模式就都不鸟。一个mode里面可以添加多个NSTimer,也就是说以后当创建NSTimer的时候,可以指定它是在什么模式下运行的。

(2)它是基于时间的触发器,说直白点那就是时间到了我就触发一个事件,触发一个操作。基本上说的就是NSTimer

(3)相关代码

*/

- (void)timer2

{

    //NSTimer 调用了scheduledTimer方法,那么会自动添加到当前的runloop里面去,而且runloop的运行模式kCFRunLoopDefaultMode

 

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

 

    //更改模式

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

 

}

 

- (void)timer1

{

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

 

    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

 

    //定时器添加到UITrackingRunLoopMode模式,一旦runloop切换模式,那么定时器就不工作

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

 

    //定时器添加到NSDefaultRunLoopMode模式,一旦runloop切换模式,那么定时器就不工作

    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

 

    //占位模式:common modes标记

    //被标记为common modes的模式 kCFRunLoopDefaultMode  UITrackingRunLoopMode

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

 

    //    NSLog(@"%@",[NSRunLoop currentRunLoop]);

}

 

- (void)run

{

    NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode);

}

 

- (IBAction)btnClick {

 

    NSLog(@"---btnClick---");

}

 

```

 

(2)GCD中的定时器

```objc

//0.创建一个队列

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

 

    //1.创建一个GCD的定时器

    /*

     第一个参数:说明这是一个定时器

     第四个参数:GCD的回调任务添加到那个队列中执行,如果是主队列则在主线程执行

     */

    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

 

    //2.设置定时器的开始时间,间隔时间以及精准度

 

    //设置开始时间,三秒钟之后调用

    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);

    //设置定时器工作的间隔时间

    uint64_t intevel = 1.0 * NSEC_PER_SEC;

 

    /*

     第一个参数:要给哪个定时器设置

     第二个参数:定时器的开始时间DISPATCH_TIME_NOW表示从当前开始

     第三个参数:定时器调用方法的间隔时间

     第四个参数:定时器的精准度,如果传0则表示采用最精准的方式计算,如果传大于0的数值,则表示该定时切换i可以接收该值范围内的误差,通常传0

     该参数的意义:可以适当的提高程序的性能

     注意点:GCD定时器中的时间以纳秒为单位(面试)

     */

 

    dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);

 

    //3.设置定时器开启后回调的方法

    /*

     第一个参数:要给哪个定时器设置

     第二个参数:回调block

     */

    dispatch_source_set_event_handler(timer, ^{

        NSLog(@"------%@",[NSThread currentThread]);

    });

 

    //4.执行定时器

    dispatch_resume(timer);

 

    //注意:dispatch_source_t本质上是OC类,在这里是个局部变量,需要强引用

    self.timer = timer;

 

GCD定时器补充

/*

 DISPATCH_SOURCE_TYPE_TIMER         定时响应(定时器事件)

 DISPATCH_SOURCE_TYPE_SIGNAL        接收到UNIX信号时响应

 

 DISPATCH_SOURCE_TYPE_READ          IO操作,如对文件的操作、socket操作的读响应

 DISPATCH_SOURCE_TYPE_WRITE         IO操作,如对文件的操作、socket操作的写响应

 DISPATCH_SOURCE_TYPE_VNODE         文件状态监听,文件被删除、移动、重命名

 DISPATCH_SOURCE_TYPE_PROC          进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号

 

 下面两个都属于Mach相关事件响应

    DISPATCH_SOURCE_TYPE_MACH_SEND

    DISPATCH_SOURCE_TYPE_MACH_RECV

 下面两个都属于自定义的事件,并且也是有自己来触发

    DISPATCH_SOURCE_TYPE_DATA_ADD

    DISPATCH_SOURCE_TYPE_DATA_OR

 */

```

 

- CFRunloopSourceRef

 

    1.是事件源也就是输入源,有两种分类模式;

      一种是按照苹果官方文档进行划分的

      另一种是基于函数的调用栈来进行划分的(source0和source1)。

        2.具体的分类情况

            (1)以前的分法

                Port-Based Sources

                Custom Input Sources

                Cocoa Perform Selector Sources

 

            (2)现在的分法

                Source0:非基于Port的

                Source1:基于Port的

        3.可以通过打断点的方式查看一个方法的函数调用栈

 

- CFRunLoopObserverRef

 

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

 

(2)如何监听

```objc

 //创建一个runloop监听者

    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

 

        NSLog(@"监听runloop状态改变---%zd",activity);

    });

 

    //为runloop添加一个监听者

    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

 

    CFRelease(observer);

```

(3)监听的状态

```objc

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

    kCFRunLoopEntry = (1UL << 0),   //即将进入Runloop

    kCFRunLoopBeforeTimers = (1UL << 1),    //即将处理NSTimer

    kCFRunLoopBeforeSources = (1UL << 2),   //即将处理Sources

    kCFRunLoopBeforeWaiting = (1UL << 5),   //即将进入休眠

    kCFRunLoopAfterWaiting = (1UL << 6),    //刚从休眠中唤醒

    kCFRunLoopExit = (1UL << 7),            //即将退出runloop

    kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有状态改变

};

```

 

- 1.9 Runloop运行逻辑

-

![PNG](3.png)

--------------------

![PNG](4.png)

 

---

 

####2.Runloop应用

 

    1)NSTimer

    2)ImageView显示:控制方法在特定的模式下可用

    3)PerformSelector

    4)常驻线程:在子线程中开启一个runloop

    5)自动释放池

        第一次创建:进入runloop的时候

        最后一次释放:runloop退出的时候

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

 

---

 

以上是关于iOS面试题:runloop 的 mode 作用是啥?的主要内容,如果未能解决你的问题,请参考以下文章

精选面试题教你应对高级iOS开发面试官(提供底层进阶规划蓝图)

iOS面试题之runloop

2021年,大厂常问iOS面试题--Runloop篇

iOS开发RunLoop学习:四:RunLoop的应用和RunLoop的面试题

iOS面试题: Runloop的基本使用

iOS中runloop总结(二)