阅读 695

iOS探索:RunLoop本质、数据结构以及常驻线程实现

RunLoop的本质

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

  • 没有消息需要处理时,休眠以避免资源占用,状态切换是从用户态通过系统调用切换到内核态

  • 有消息处理时,立刻被唤醒,状态切换是从内核态通过系统调用切换到用户态

这里有一个问题,我们应用程序中的main函数为什么可以保持无退出呢

实际上呢,在我们的main函数中会调用UIApplicationMain函数,在这个函数中会启动一个运行循环(也就是我们所说的RunLoop),在这个运行循环中可以处理很多事件,例如屏幕的点击,滑动列表,或者网络请求的返回等等,在处理完事件之后,会进入等待,在这个循环中,并不是一个单纯的for循环或者while循环,而是从用户态到内核态的切换,以及再从内核态到用户态的切换,这里面的等待也不等于死循环,这里面最重要的是状态的切换

RunLoop的数据结构

在OC中,系统为我们提供了两个RunLoop,一个是CFRunLoop,另一个是NSRunLoop,而NSRunLoop是对CFRunLoop的一个封装,提供了面向对象的API,并且它们也分别属于不同的框架,NSRunLoop是属于Foundation框架,而CFRunLoop是属于Core Foundation框架

关于RunLoop的数据结构主要有三种:

  • CFRunLoop

  • CFRunLoopMode

  • Source/Timer/Observer

WX20181221-145251@2x.png

  • pthread:代表的是线程,RunLoop与线程的关系是一一对应的

  • currentMode:是一个CFRunLoopMode这样一个数据结构

  • modes:是一个包含CFRunLoopMode类型的集合(NSMutableSet<CFRunLoopMode*>)

  • commonModes:是一个包含NSString类型的集合(NSMutableSet<NSString*>)

  • commonModeItems:也是一个集合,在这个集合中包含多个元素,其中包括多个Observer,多个Timer,多个Source

WX20181221-150257@2x.png

  • name:名称,例如NSDefaultRunLoopMode,所以说是通过这样一个名称来切换对应的模式,例如在上面的commonModes里面都是名称字符串,也就是说通过这些名称来支持多种模式

  • source0:集合类型的数据结构

  • source1:集合类型的数据结构

  • obsevers:数组类型的数据结构

  • timers:数组类型的数据结构

CFRunLoopSource

  • source0:需要手动唤醒线程

  • source1:具备唤醒线程的能力

CFRunLoopTimer

和NSTimer是toll-free bridge的(免费桥转换)

CFRunLoopObserver

我们可以通过注册一些Observer来实现对RunLoop相关时间点的观测

可以观测的时间点包括:

  • kCFRunLoopEntry:RunLoop的入口时机,RunLoop将要启动的时候的回调通知

  • kCFRunLoopBeforeTimers:RunLoop将要处理Timer事件的时候

  • kCFRunLoopBeforeSources:RunLoop将要处理Source事件的时候

  • kCFRunLoopBeforeWaiting:RunLoop将要进入休眠的时候,将要进行用户态到内核态的切换

  • kCFRunLoopAfterWaiting:RunLoop将要进入唤醒的时候,内核态到用户态的切换后不久

  • kCFRunLoopExit:RunLoop退出的时候

RunLoop的mode

WX20181221-153513@2x.png

在RunLoop中,假如在mode1中运行,那么在mode2中事件的回调就会接收不到,RunLoop只接受在当前mode中的回调,那么这里有一个经典问题,当我们在滑动列表时,为什么会出现cell上的定时器停止的情况以及如何解决

因为在列表滑动的时候当前RunLoop的mode从Default切换到了Tracking,所以导致原来mode中的事件回调接收不到,想要解决便可将其加入commonModes中,下面我们来看一下commonMode

CommonMode的特殊性

  • CommonMode并不是一个实际存在的模式

  • 是同步Source/Timer/Observer到多个Mode中的一中技术方案

事件循环的实现机制

WX20181221-161307@2x.png

  • 在RunLoop启动之后会发送一个通知,来告知观察者

  • 将要处理Timer/Source0事件这样一个通知的发送

  • 处理Source0事件

  • 如果有Source1要处理,这时会通过一个go to语句的实现来进行代码逻辑的跳转,处理唤醒是收到的消息

  • 如果没有Source1要处理,线程就将要休眠,同时发送一个通知,告诉观察者

  • 然后线程进入一个用户态到内核态的切换,休眠,然后等待唤醒,唤醒的条件大约包括三种: 1、Source1
    2、Timer事件
    3、外部手动唤醒

  • 线程刚被唤醒之后也要发送一个通知告诉观察者,然后处理唤醒时收到的消息

  • 回到将要处理Timer/Source0事件这样一个通知的发送

  • 然后再次进行上面步骤,这就是一个RunLoop的事件循环机制

这里有一个这样的问题:当我们点击一个app,从我们点击到程序启动、程序运行再到程序杀死这个过程,系统都发生了什么呢

实际上当我们调用了main函数之后,会调用UIApplicationMain函数,在这个函数内部会启动主线程的RunLoop,然后经过一系列的处理,最终主线程的RunLoop会处于一个休眠状态,然后我们此时如果点击一下屏幕,会转化成一个Source1来讲我们的主线程唤醒,然后当我们杀死程序时,会调用RunLoop的退出,同时发送通知告诉观察者

RunLoop与多线程

  • 线程与RunLoop是一一对应的

  • 自己创建的线程默认没有RunLoop

实现一个常驻线程

  • 为当前线程开启一个RunLoop

  • 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环

  • 启动该RunLoop

请看下面的一个代码逻辑

#import "WXObject.h"

static NSThread *thread = nil;
/** 是否继续事件循环*/
static BOOL runAlways = YES;

@implementation WXObject

+ (NSThread *)threadForDispatch {
    
    if (thread == nil) {
        @synchronized (self) {
            if (thread == nil) {
                thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
                [thread setName:@"alwaysThread"];
                //启动线程
                [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);
    
    //创建RunLoop,同时向RunLoop的defaultMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    //如果可以运行
    while (runAlways) {
        @autoreleasepool {
            //令当前RunLoop运行在defaultMode下
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }
    
    //某一时机,静态变量runAlways变为NO时,保证跳出RunLoop,线程推出
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

@end
复制代码
  • 首先我们在这里定义两个全局静态变量,一个是我们自定义的线程thread,还有一个是用来控制是否事件循环

  • 然后我们创建线程,用@synchronized来保证线程安全,创建的时候添加入口方法,然后启动线程,当线程调用start方法时,会调用下面入口方法

  • 在这个方法中首先创建source,传入一个上下文,然后创建RunLoop,同时向RunLoop的defaultMode下面添加Source,CFRunLoopGetCurrent()这个方法如果获取不到就会创建一个RunLoop,然后添加到defaultMode中

  • 通过我们前面定义的静态变量来进行判断,如果可以运行,就令当前RunLoop运行在defaultMode下,这里用了一个自动释放池,减小内存峰值消耗,这里需要注意的是,如果我们上面添加到的是defaultMode,这里也需要运行在defaultMode中,否则会出现死循环

  • 某一时机,静态变量runAlways变为NO时,保证跳出RunLoop,线程推出,释放source

以上就是实现一个常驻线程的代码逻辑

GitHub

Demo

关注下面的标签,发现更多相似文章
评论