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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容