【iOS】NSRunLoop

目录

概念

RunLoop与线程的关系

Runloop对外的接口

CFRunLoopSourceRef

Source0

Source1

CFRunLoopTimer

CFRunLoopObserver

RunLoop的Mode

应用场景

Runloop的内部逻辑

Runloop应用

tableView延迟加载图片,保证流畅

Timer不被ScrollView的滑动影响

AFNetworking

​编辑

PerformSelecter


概念

一般来讲线程一次只能执行一个任务,执行完后就会退出,我们现在想实现一个功能:线程一直在处理事件并且不会退出,这就是我们Runloop的作用。其实很像runloop的名字所表示的,绕着一个圈圈一直跑。

要实现runloop这种模型一个关键点就是怎么样去管理事件/消息,让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒

runloop就是一个可以管理需要处理的消息与事件的一个对象,CFRunLoopRef 可以理解为在 CoreFoundation 框架内的NSRunLoop,它提供了纯 C 函数的 API,但是它的API都是线程安全的,而NSRunLoop却不是线程安全的

RunLoop与线程的关系

基本上所有的线程操作的底层都是对pthread_t的封装

而关于RunLoop和线程,苹果不允许直接创建Runloop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()

CFRunLoopGetMain的实现如下:

这当中用到了函数_CFRunLoopGet0,其实现如下:

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {//如果t不存在,则标记为主线程(即默认情况,默认是主线程)if (pthread_equal(t, kNilPthreadT)) {t = pthread_main_thread_np();}__CFLock(&loopsLock);if (!__CFRunLoops) {__CFUnlock(&loopsLock);//创建全局字典,标记为kCFAllocatorSystemDefaultCFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//通过主线程 创建主运行循环CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//利用dict,进行key-value绑定操作,即可以说明,线程和runloop是一一对应的// dict : key valueCFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {CFRelease(dict);}CFRelease(mainLoop);__CFLock(&loopsLock);}//通过其他线程获取runloopCFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));__CFUnlock(&loopsLock);if (!loop) {//如果没有获取到,则新建一个运行循环CFRunLoopRef newLoop = __CFRunLoopCreate(t);__CFLock(&loopsLock);loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));if (!loop) {//将新建的runloop 与 线程进行key-value绑定CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);loop = newLoop;}// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it__CFUnlock(&loopsLock);CFRelease(newLoop);}if (pthread_equal(t, pthread_self())) {_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);}}return loop;
}

从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里 线程刚创建时并没有runloop,我们需要主动获取,系统才会自动帮我们创建runloop并加到字典中

Runloop对外的接口

通过前两个函数我们可以分别获得当前线程的CFRunLoop对象和主线程的CFRunLoop对象,但是只是获得了,如果想要让Runloop运行起来,就还需要一些别的操作。首先先看一下runloop的一些具体结构

在CoreFoundation里面关于RunLoop有5个类:

  • CFRunLoopRef

  • CFRunLoopModeRef

  • CFRunLoopSourceRef

  • CFRunLoopTimerRef

  • CFRunLoopObserverRef

一个Runloop可以包含多个Mode,CFRunLoopModeRef类没有对外暴露,只是通过CFRunLoopRef 的接口进行了封装,他们的关系如下:

可以看到每个model中包含了Source/Timer/Observer的集合,每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode,如果要切换Mode,必须退出Loop,再重新指定一个Mode进入,这样做可以分隔开不同组的Source/TImer/Observer,避免互相影响

接下来我们分别来看看Source/Timer/Observer这三种结构,首先是Source

CFRunLoopSourceRef

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。两个的区别主要是RunLoop事件源的不同:

  • Source0:处理非基于端口的事件(如程序内部自定义的事件)

  • Source1:处理基于端口的事件(如来自内核的Mach端口信息)

Source0 需要手动标记(CFRunLoopSourceSignal)并唤醒 RunLoop 才能触发,而 Source1 会自动唤醒 RunLoop。

需要明确一个概念,RunLoop主要用来处理异步事件,如用户输入、定时器触发、网络响应等这些事件通常被封装成事件源,然后由RunLoop在适当的时机调度和处理

Source0

Source0 只包含了一个回调(函数指针),它并不能主动触发事件

使用时,先调用CFRunLoopSourceSignal(source0)将 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件

// 假设有一个方法,用于处理按钮点击
- (void)buttonClicked {// 手动触发RunLoop的Source0CFRunLoopSourceSignal(source0);CFRunLoopWakeUp(CFRunLoopGetCurrent()); // 唤醒RunLoop来处理事件
}
​
// 配置Source0
- (void)setupSource0 {CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &callout};source0 = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);CFRunLoopAddSource(CFRunLoopGetCurrent(), source0, kCFRunLoopDefaultMode);
}
​
// Source0的回调函数
void callout(void *info) {NSLog(@"Source0 event triggered.");
}

Source1

Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

// 配置Source1
- (void)setupSource1 {CFRunLoopSourceContext1 context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, &perform, NULL};CFMessagePortRef localPort = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.example.app.port"), &callback, &context, false);CFRunLoopSourceRef source1 = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, localPort, 0);CFRunLoopAddSource(CFRunLoopGetCurrent(), source1, kCFRunLoopCommonModes);
}
​
// Source1的回调函数
CFDataRef callback(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void *info) {NSLog(@"Received message: %d", msgid);return NULL;
}
​
// Source1的事件执行
void perform(void *info) {NSLog(@"Performing work in response to external event.");
}

使用场景: • 处理来自其他进程的数据或信号。 • 监听系统级事件或网络事件。

CFRunLoopTimer

这是一个基于时间的触发器,包含一个时间长度和一个回调,当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调

CFRunLoopObserver

这是用来监视和相应RunLoop的特定活动的一种对象,通过 CFRunLoopObserver,开发者可以在 RunLoop 的不同阶段插入自定义的代码来执行特定的任务

可以观测的时间点有以下几个:

我们在上面讲的 Source/Timer/Observer 被统称为 mode item,一个item被重复添加到同一个mode时不会多次执行,但是如果一个mode中一个item都没有,runloop会自动退出,不会进出循环

RunLoop的Mode

刚才在上文讲了mode item,modeitem是被加到mode中的,我们现在讲一下Runloop的Mode

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

应用场景

比如说之前写项目时经常遇到的那个滑动时计数器停止计时的问题,用这个点就可以回答:

主线程的Runloop中有两个预置的Mode:

kCFRunLoopDefaultModeUITrackingRunLoopMode

DefalutMode是App平时所处的状态,TrackingMode是当滑动时所处的状态,当我们创建NSTimer添加到DefalutMode中,Timer会得到重复回调,但是当我们滚动我们的TableView时,Runloop会切换Mode,由DefalutMode切换为TrackingMode,此时Timer会停止同时不会进行回调,也不会影响到滑动的操作

这时想让滑动时NSTimer可以继续运作的话,有两个方法:

  • 一种方法就是将Timer分别加入到两个Mode

  • 另一种方法就是将NSTimer加到最顶层的RunLoop 的 commonModeItems,加入后的ModeItems类型会被Runloop加到具有common属性的Mode中去,也就是直接将Timer同时加到defaultMode与TrackMode中去

iOS中有5种Mode:

苹果公开的三种有:

  • NSDefaultRunLoopMode(kCFRunloopDefaultMode):默认状态,app通常在这个mode下运行

  • UITrackingRunLoopMode:界面跟踪mode(例如滑动scrollview时不被其他mode影响)

  • NSRunLoopCommonModes(kCFRunLoopCommonModes):是 前两个mode的集合,可以把自定义mode用CFRunLoopAddCommonMode函数加入到集合中

还有两种只需了解:

  • GSEventReceiveRunLoopMode:接收系统内部mode,通常用不到

  • UIInitializationRunLoopMode:私有,只在app启动时使用,使用完就不在集合中了

Runloop的内部逻辑

Runloop的逻辑有一张非常经典的图:

Runloop应用

tableView延迟加载图片,保证流畅

在快速滑动tableView时,滑动过的图片会一直加载,但滑动过的图片都不是我们想要呈现的图片,如果加载就浪费CPU资源,用RunLoop就可以避免滑动时加载图片

给ImgaeView的加载图片的方法指定只有在DefalutMode下才能加载,滑动时不加载图片

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName.png"] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

Timer不被ScrollView的滑动影响

上面其实已经讲过这个问题了,除了刚才提到的两个方法,还有一种方法:

用GCD创建定时器,它不会受到RunLoop影响

AFNetworking

在多线程中,线程执行完任务就会退出,那么如果需要反复执行任务的话,就会频繁地创建与销毁线程,这样不仅效率低下,还增加了系统的开销,因此如果有一个常驻线程来处理这些任务就可以避免这种情况。

一个RunLoop中如果没有Observer/Timer/Source等items,Runloop会自动退出,因此我们创建一个空的port发送消息给Runloop,以至于Runloop不会退出而是一直常驻

PerformSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,其内部会自动创建一个Timer加到Runloop中,当时间到了执行回调,如果当前线程没有Runloop,此方法也会失效

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/94043.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/94043.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTTP接口鉴权方式

几种主流且可行的HTTP接口鉴权方式,从简单到复杂,各有其适用场景。我将它们分为两大类:传统方式和现代方式。一、传统方式这类方式简单易用,但通常安全性较低或扩展性较差,适用于内部系统或简单API。1. HTTP Basic Aut…

DIC技术极端环境案例分享:系泊链在海水环境下氢脆化性能测试

实验结果的具体视频可详见以下链接:研索仪器DIC技术在极端条件下的应用 01 海水环境: DIC技术在海水环境下的应用核心挑战在于恶劣的光学条件(如散射、衰减、畸变)、严酷的化学/生物环境(腐蚀、生物污损)…

DL00291-联邦学习以去中心化锂离子电池健康预测模型完整实现

联邦学习在锂离子电池健康预测中的应用:去中心化训练与客户选择策略在锂离子电池健康预测领域,随着电池使用环境的多样化以及电池状态监测需求的不断增长,传统的集中式数据训练方法逐渐显现出局限性。为了解决数据隐私保护和大规模数据集中处…

TCP协议大全

什么是TCP?基本定义与属性TCP(传输控制协议)是传输层的重要协议,具有面向连接(传输前需先建立连接,是发送方和接收方的点对点一对一连接)、基于字节流(以字节流形式传输数据&#xf…

当硅基生命遇见碳基萌宠:Deepoc具身智能如何重新定义“宠物监护者”

在东京某高级公寓里,一只布偶猫正优雅地踱步到智能喂食器前。令人惊讶的是,这个通体雪白的喂食器突然"活"了过来——它微微倾斜身体,用柔和的机械音发出问候,同时伸出仿生机械臂轻轻抚过猫咪的背部。这不是科幻电影场景…

线上日志排查问题

1、查异常堆栈 显示该行及其后面的50行内容,然后通过 less 命令进行分页查看 grep -A 50 "NullPointerException" a.log | less参数解释: grep: 文本搜索命令-A 50: After 的意思,显示匹配行后面的50行“NullPointerException”: 要…

LabVIEW与CAN开发燃料电池监控

​基于 LabVIEW 与 CAN 总线技术,构建了一套多组质子交换膜燃料电池(PEMFC)堆监控系统。系统采用优质硬件设备,通过 LabVIEW 的图形化编程能力实现数据采集、实时监控与多堆切换控制,稳定可靠,为燃料电池性…

CVPR焦点 | 神经网络新范式:轻量化与精度并行,重塑视觉任务性能天花板

关注gongzhonghao【CVPR顶会精选】神经网络卷积想找新亮点?不妨考虑:动态结构设计。作为深度学习架构搜索与高效建模两大热点的结合,动态神经网络凭借自适应推理与高效特征利用的优势,在视觉识别、视频理解等任务中脱颖而出&#…

机器学习之集成算法学习

一、集成学习概述集成学习(ensemble learning)通过构建并结合多个个体学习器来完成学习任务,核心思想是 “集众家之长”—— 就像多个专家共同判断往往比单个专家更可靠。其关键在于如何生成多样化的个体学习器并设计有效的结合策略。结合策略…

Unreal Engine UE_LOG

Unreal🎮 Unreal Engine - UE_LOG📝 定义🏛 类/宏关联⚡ 关键特性🛠️ 常见配置📚 使用方法🔧 基础语法🔍 示例🪂 典型应用场景🔗 与其他组件对比⚠️ 常见问题与注意事项…

Halcon那些事:什么是动态阈值,如何用dyn_threshold分割图片

Halcon那些事:什么是动态阈值,如何用dyn_threshold分割图片 一、什么是动态阈值?为什么需要它? 1. 传统全局阈值的局限性 2. 动态阈值的核心思想 二、Halcon 中的核心算子:`dyn_threshold` 1. 算子原型 2. 参数详解 三、工作原理(数学模型) 四、详细使用步骤与实例 五、关…

Go初级二

Go初级入门(二):变量、常量与数据类型 大家好,欢迎来到《Go初级入门》系列的第二篇!在上一篇文章中,我们介绍了如何安装Go环境并运行第一个“Hello, World”程序。今天,我们将深入Go语言的基础语…

《战神:诸神黄昏》v1.0.668中文版,索尼大作,PC平台体验诸神黄昏

[游戏名称]: 《战神:诸神黄昏》v1.0.668中文版 [软件大小]: 175 GB [软件大小]: 夸克网盘 游戏介绍 《战神:诸神黄昏》是由索尼制作并发行的动作冒险游戏,作为《战神4》的正统续作,它继续了奎托斯与阿特柔斯的神话之旅。在诸神…

AI赋能环保精准治理:AI水质监测溯源快、空气质量预测施策准,守护生态新效能

传统环境保护工作长期受限于 “污染监测滞后”“溯源难度大”“治理方案针对性弱” 的问题,而 AI 技术的深度应用,正让环保工作从 “被动应对” 转向 “主动预判”,既能实时捕捉污染踪迹,还能精准制定治理方案,让生态保…

yolo训练实例(一)

yolo官网 https://github.com/ultralytics/ultralytics?tabreadme-ov-file 下载python和解除限制 https://www.python.org/downloads/windows/ Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled"…

STM32-BKP备份寄存器与RTC实时时钟

引言本文主要从BKP备份寄存器和RTC实时时钟的原理,特性及应用三个方面展开讨论,解析它们在STM32中的独特价值,助力开发者更好的掌握和运用它们。BKP备份寄存器的定义STM32的BKP备份寄存器是一种特殊的存储单元,它位于备份区域&…

Linux网络服务(五)——FTP服务详解与实践操作手册

文章目录前言一、FTP服务概述1.1 FTP基本定义1.2 VSFTP模式分类1.3 FTP端口作用二、FTP作用与工作原理(重点)2.1 FTP的作用、模式及通信方式2.1.1 FTP核心作用2.1.2 VSFTP模式与通信协议2.2 FTP工作原理与流程2.2.1 主动模式工作原理2.2.2 被动模式工作原…

5.3 包管理工具 npm yarn pnpm 对比

基本介绍 工具发布时间开发者定位npm2010 年npm Inc / OpenJS FoundationNode.js 官方包管理器Yarn2016 年Facebook(现 Meta)更快、更可靠的替代方案pnpm2016 年Zoltan Kochan高性能、节省磁盘空间 一、核心机制与设计差异 1. npm(Node Pa…

爬虫基础学习-授权认证,cookie认证,异常处理

验证: HTTPBasicAuthHandler(用户基本的身份验证处理) HTTPPasswordMgrWithDefaultRealm(经常和authhandler一起出现)#创建一个密码管理器 password_mgr urllib.request.HTTPPasswordMgrWithDefaultRealm() #添加进目…

开发避坑指南(34):mysql深度分页查询优化方案

问题语句 SELECT* FROMt_order_log l WHERE1 1 AND l.create_time > 2024-08-28 AND l.create_time < 2024-09-04 23:59:59 LIMIT 10000,10上述查询sql&#xff0c;即使create_time字段已建立索引&#xff0c;但偏移量达到几十万时候&#xff0c;查询耗时将近1分钟&…