iOS面试题(二十六)RunLoop

8.RunLoop

iOS面试题(二十六)RunLoop_第1张图片

  • 什么是RunLoop?(RunLoop的实现机制?剖析RunLoop的源码)
  • 数据结构  (Mode/Source/Timer/Observer )
  • 事件循环机制(RunLoop有事做事,没事休息是由事件循环机制来维护的)
  • RunLoop与NSTimer之间的关系是怎样的?(我们在使用NSTimer的时候,应该怎样考虑和RunLoop的关系,包括RunLoop所带来的影响)
  • RunLoop与多线程之间有什么关系?(如何通过RunLoop来实现常驻线程?)

什么是RunLoop?

RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象

事件循环是什么呢?
iOS面试题(二十六)RunLoop_第2张图片

  • 没有消息处理时,进入休眠以避免资源占用
  • 有消息时,立刻被唤醒

runloop循环不是单纯的for...while循环,而是发生一个用户态到内核态切换,以及内核态到用户态的一个切换
它维护的事件循环,可以用来不断的处理消息或者事件,对他们进行管理
当没有消息进行处理时,会从用户态经过系统调用进入到内核态,由此可以用来当前进程/线程的休眠,会把控制权交给内核态,避免资源占用
当有消息需要处理时,会发生从内核态到用户态的切换,当前的用户线程会被唤醒
所以说,状态切换才是Runloop的关键
 

关于用户态和内核态

  • 用户态:
    应用程序一般都是运行在用户态上,用户进程,包括我们开发所使用的绝大多数API,都是针对用户层面的
  • 内核态:
    在内核态往往有些陷阱指令,中断,以及一些开机关机的操作
    并且内核态里面的一些内容,可以对用户态中的一些线程进行调度和管理,包括进程间的通信

何时切换:

  • 当我们发生了系统调用,需要使用到操作系统以及一些底层内核相关的指令或者API时,就会触发系统调用,有些系统调用就会发生一个状态空间的切换,

为什么区分状态:

  • 之所以通过切换空间来区分出了用户态和内核态,是对计算机的资源调度,资源管理进行一个统一操作,就可以合理安排资源调度,避免特殊的异常(比如说在内核态,有一些陷阱指令、中断、开机关机的操作)
  • 试想下,如果每一个用户进程,或者说一个APP,都可以促使当前用户手机关机或者中断,就无法想象了.....

什么是事件循环?(总结):

  • 维护的事件循环,可以用来不断的处理消息或者事件,对他们进行管理。
  • 同时当没有消息进行处理时,会从用户态发生到内核态的切换,由此可以用来当前线程的休眠,避免资源占用
    当有消息需要处理时,会发生从内核态到用户态的切换,当前的用户线程会被唤醒
  • 所以说,状态切换才是Runloop的关键

入口函数:

iOS面试题(二十六)RunLoop_第3张图片
在我们程序中,默认是从主函数进行程序启动
按说在main函数里,顺着执行体代码,进行依次执行,最后main函数就会退出,我们的程序也会随之退出
main函数为何能保持不退出?
在main函数中,会调用UIApplicationMain函数,
在内部会启动主线程的Runloop,可以不断的接收消息,比如点击屏幕事件,滑动列表以及处理网络请求的返回等
接收消息后对事件进行处理,处理完之后,就会继续等待
Runloop是对事件循环的一种维护机制,可以做到在有事做的时候做事,没有事情的时候会通过用户态发生到内核态的切换,避免资源占用,当前线程处于休眠的状态。

RunLoop的数据结构

在 OC 中实际提供了两个 RunLoop 的
一个是 NSRunLoop
一个是 CFRunLoop
NSRunLoop 是对 CFRunLoop 的封装,提供了一些面向对象的 API
NSRunLoop 是位于 Foundation 当中的,CFRunLoop 位于 CoreFoundation 当中的

RunLoop 的数据结构主要有三个

  • CFRunLoop
  • CFRunLoopMode
  • Source/Timer/Observer

CFRunLoop

iOS面试题(二十六)RunLoop_第4张图片


struct __CFRunLoop {
    CFRuntimeBase _base;
     ...
   
    /*C级别的一个线程对象 , RunLoop和线程是一一对应的关系*/
    pthread_t _pthread; 
    
    /*数据结构:NSMutableSet
      RunLoop当前所处的模式mode,其实是CFRunLoopMode的数据结构,
    */
    CFRunLoopModeRef _currentMode;
    
     /*多个mode的集合,从数据结构看出,
        RunLoop和它的mode是一对多的关系    
     */
    CFMutableSetRef _modes;

    /*也是一个集合里面都是字符串,
     有别于_modes里面的元素CFRunLoopModeRef
    */
    CFMutableSetRef _commonModes; 
    /*
     也是一个集合,包含多个Observer(观察者)、Timer、Source。
     我们可以为RunLoop添加Observer(观察者),包括Timer、Source都可以提交到某一个RunLoop对应的
     某一个_currentMode上面。
    */
    CFMutableSetRef _commonModeItems;
   
};

CFRunLoopMode具体的成员变量

iOS面试题(二十六)RunLoop_第5张图片

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // 结构 
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

CFRunLoopSource

在 CF 框架当中官方名称叫 CFRunLoopSource ,有两种 source0 和 source1
唤醒线程就是从内核态切换到用户态

CFRunLoopTimer

和平时所使用的 NSTimer 是具备免费桥转换的

CFRunLoopObserver

观测时间点

  • KCFRunLoopEntry
  • KCFRunLoopBeforeTimers
  • KCFRunLoopBeforeSources
  • KCFRunLoopBeforeWaiting
  • KCFRunLoopAfterWaiting
  • KCFRunLoopExit

可以通过注册一些 Observer 来实现对 RunLoop 的一些相关时间点的监测或者观察

问:我们可以监听 RunLoop 哪些时间点?

  • RunLoop 的入口事迹,当 RunLoop 准备启动的时候系统会给我们一个回调通知,这个通知掉 CFRunLoopEntry
  • 代表的含义:通知观察者 RunLoop 将要对 Timer 一些相关事件进行处理了
  • 代表将要处理一些 Source 事件
  • 通知对应观察者,当前 RunLoop 将要进入休眠状态,这个通知或者说观测点是非常重要的一个观测点,在 RunLoop 发送这个通知的时候,即将要发生用户态到内核态的切换
  • 这也是一个重要的观测点,这个通知发出的时机恰好是从内核态切换到用户态之后的不久之间
  • 代表 RunLoop 退出的通知

各个数据之间的关系

iOS面试题(二十六)RunLoop_第6张图片

RunLoop 的 Mode

iOS面试题(二十六)RunLoop_第7张图片

一个 RunLoop 可以对应多个 mode,每个 mode 当中又可以有多个 Sources1,Timers,Observers
当 RunLoop 运行在某一个 mode 上的时候,比如说运行在 mode1 上面,这个时候如果 mode2 当中某一个 timer 事件或者 Observe 事件回调了,这个时候是没有办法接收对应 mode2 当中所回调过来的事件,这就 RunLoop 有多个 mode 的原因,实际上起到的就是一个屏蔽的效果,当运行到 mode1 上时,只能接收处理 mode1 当中的 Sources1,Timers,Observers

思考:一个 Timer 要想同时加入到两个 mode 里面,需要怎么做?如果说这个 Timer 既想要它在 mode1 上可以正常运行,在每一个事件回调中做正常的处理,在 mode2 上也需要做相应的处理和事件的回调接收

系统提供了添加到两个 mode 的机制的,这样可以保证当 RunLoop 发生 mode 切换的时候也可以让对应的 Timer 等事件正常处理接收

这实际上就涉及到了 CommonMode(CommonMode 的特殊性):

CommonMode 本身是有它的特殊性的,CommonMode 并不是一个实际存在的模式,在 OC 当中经常会通过 NSRunLoopCommonModes 字符串常量来表达 CommonMode
CommonMode 本身和 defaultMode 是有区分的,调用一个线程运行在 CommonMode 上面和运行在具体的 defaultMode 上是有区别的

这里有个概念叫 “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 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。

总结:
什么是RunLoop?它是怎么做到有事做事,没事休息的?
1、RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
2、程序运行会调用main函数,在main函数里面调用UIApplicationMain,UIApplicationMain函数会启动主线程的runloop。
3、runloop运行后,会调用系统方法mach_msg(),会使得程序从用户态变成核心态,此时线程处于休眠状态。
4、当有外界条件变化(Source/Timer/Observer),mach_msg会使得程序从核心态变成用户态,此时线程处于活跃状态。

RunLoop与线程是怎么样的关系

1、RunLoop与线程是一一对应的关系。
2、一个线程默认是没有runloop的(主线程除外),我们需要为它手动创建。

问题13、如何实现一个常驻线程

  • 为当前线程开启一个RunLoop
    可以通过[CFRunLoop getCurrent]或者 [NSRunLoop currentRunLoop]来创建,因为获取RunLoop这个方法本身会查找,如果当前线程没有runloop,会在系统内部为我们创建
  • 向该RunLoop中添加一个port / Source等维护RunLoop的事件循环
    RunLoop如果没有事件需要处理的话,默认情况下,是不能自己维持事件循环,会直接退出,所以需要添加port / Source来维持事件循环机制
  • 启动该RunLoop
    调用run方法
  • 运行的模式和上面添加资源的模式必须是同一个,否则会因为外部使用个while循环就导致死循环。

#import "MCObject.h"

@implementation MCObject
//定义两个k静态全局变量

// 自定义线程
static NSThread *thread = nil;
// 标记当前线程是否要继续事件循环
static BOOL runAlways = YES;

+ (NSThread *)threadForDispatch{
    if (thread == nil) {
        @synchronized(self) {
            if (thread == nil) {//采用线程安全的方式去创建thread,入口方法为runRequest
                // 线程的创建
                thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
                [thread setName:@"com.imooc.thread"];
                // 启动
                [thread start];
            }
        }
    }
    return thread;
}

+ (void)runRequest
{
    // 创建一个Source
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    
    // 为thread线程创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    //while循环维持RunLoop的事件循环
    // 如果可以运行
    while (runAlways) {
        //确保每次yRunLoop运行一圈的时候能够对内存进行释放
        @autoreleasepool {
            /* 令当前RunLoop运行在DefaultMode下面,注意这个运行的mode和上面添加资源的mode必须是同一个mode
               否则把事件源添加到另一个mode上,而运行的defaultMode下,是无法维持运行的
               函数内部会调用mach_msg,发生由用户态到核心态的切换,当前线程就会休眠,就停在里面,不是死循环
               1.0e10是让循环运行到指定时间退出,这个代表很久远的时间
               true代表资源被处理后是否马上返回
             */
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }
    
    // 如果Runloop的mode中没有对应的事件源可以处理,runloop就会自动退出
    // 所以我们在某一时机 将source移除,静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出并s释放掉
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

@end

问题14、怎样保证子线程数据回来更新UI的时候,不打断用户的滑动操作?

在用户进行滑动的过程中,当前的RunLoop运行在UITrackingRunLoopMode模式下,
而我们一般对网络请求是放在子线程中,子线程返回给主线程的数据要抛给主线程用来更新UI,
可以把这部分逻辑包装起来,提交到主线程的default模式下,这样的话,当用户滑动时,default模式下的任务不会执行,
当用户手停止时,mode就切换到了default模式下,就会处理子线程的数据了,这样就不会打断用户的滑动操作了

 

1、把子线程抛给主线程进行UI更新的逻辑,可以包装起来,提交到主线程的NSDefaultRunLoopMode模式下面。
2、因为用户滑动操作是在UITrackingRunLoopMode模式下进行的。

//参考代码事件
[self.tableView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];


 

 

你可能感兴趣的:(iOS面试题(二十六)RunLoop)