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

news/2024/7/24 18:53:42 标签: 数据结构与算法

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

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

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

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

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

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

  • 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

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

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

CommonMode的特殊性

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

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

事件循环的实现机制

  • 在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


http://www.niftyadmin.cn/n/1269924.html

相关文章

2019年React学习路线图

之前我们已经介绍了2019年Vue学习路线图&#xff0c;而React作为当前应用最广泛的前端框架&#xff0c;在Facebook的支持下&#xff0c;近年来实现了飞越式的发展&#xff0c;所以&#xff0c;我们将在下文中介绍2019年React学习路线图&#xff0c;希望给想学React的开发者一些…

[译] 用 React 和 Node.js 实现受保护的路由和权限验证

原文地址&#xff1a;Protected routes and Authentication with React and Node.js原文作者&#xff1a;Strapi译文出自&#xff1a;掘金翻译计划本文永久链接&#xff1a;github.com/xitu/gold-m…译者&#xff1a;ElizurHz校对者&#xff1a;LeviDing上周末我想挖掘一些没有…

【C语言】函数指针与指针函数

【C语言】详解函数指针与指针函数 最近在阅读cJSON的源代码&#xff0c;在看见如下代码是产生了&#xff0c;深深地疑惑&#xff0c;这个是什么声明&#xff1f;用来干嘛的&#xff1f; void *(*malloc_fn)(size_t sz); void (*free_fn)(void *ptr);在读懂这两句代码之前我们…

媒体专访 | BoCloud博云CTO:将发布微服务框架开源项目

12月5日&#xff0c;ITE 2018大会活动在北京金隅喜来登酒店圆满落幕。BoCloud博云CTO李亚琼博士在大会上宣布了BoCloud博云研究院正式成立。 关于博云研究院和博云的技术发展之路&#xff0c;北极熊专访了李博士&#xff0c;和李博士做了深入交流&#xff0c;以下为精心整理的专…

【C语言】一文全了解常用格式化函数

格式化输入与输出 本文通过函数原型–参数–返回值–实例的顺序一一解析C语言中常用输入输出函数。首先对在格式化输入输出中经常出现的控制格式以表格形式列出。 格式控制字符 以printf为例&#xff1a; printf&#xff08;“%[falgs][width][.prec ][hIL]type”&#xff0…

回溯算法理解

一、算法含义 回溯算法也叫试探法&#xff0c;它是一种系统地搜索问题的解的方法。回溯算法的基本思路是&#xff1a;暴力算法的改进&#xff0c;在通过遍历所有路径基础上&#xff0c;通过回溯&#xff08;往回找&#xff09;筛除不可能的路径&#xff0c;提高效率。 二、解题…

Rust开发操作系统系列:全新Hello World系统

如果想看更好的排版&#xff0c;建议这个链接&#xff1a;http://www.iouyi.top/index.php/archives/235.html注意&#xff1a;文章篇幅较长&#xff0c;如果想提前拿到代码&#xff0c;可以直接下载附件&#xff0c;大小仅为57k正如标题&#xff0c;这篇文章是关于如何用Rust开…

撩课-Web大前端每天5道面试题-Day23

1、为什么用Nodejs,它有哪些优缺点&#xff1f; 优点&#xff1a; 事件驱动&#xff0c;通过闭包很容易实现客户端的生命活期。 不用担心多线程&#xff0c;锁&#xff0c;并行计算的问题 V8引擎速度非常快 对于游戏来说&#xff0c;写一遍游戏逻辑代码&#xff0c;前端后端通用…