Runloop的再学习之浅析(一)
一,认识RunLoop
我的理解:
1. 在编程的世界里,万物皆对象。所以RunLoop 实际上也是一个对象,这个对象管理了其需要 处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就 会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
2. Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环。所以Android有RunLoop,iOS有RunLoop,Winphone也有RunLoop只不过叫法不同。类似于中国人日常是吃饭,睡觉,工作。。。。。 其它国家的也一样。不只是中国人独有的习惯
二,OSX/IOS系统中的RunLoop
OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef :基于 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop: 基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
注释:其实Foudation框架基本都是基于CoreFoundation框架的封装,因为CoreFoudation框架基于纯C函数,不易于理解面向对象;Foundation主面向对象,提供了成员变量和方法等API接口,有益于我们基于面向对象思想编程。
注释:
CFRunLoop.h没有给我们太多信息,可下载源码查看实现过程。这里不做过多分析
CFRunLoop源码链接: http://opensource.apple.com/tarballs/CF/
三,OSX/IOS系统中的RunLoop的循环处理的流程
大概的意思是:
所有的“消息”都被添加到了NSRunLoop的事件列表中去,而在这里这些消息并分为“input source”和“Timer source” 二种类型并在循环中检查是不是有事件需要发生,如果需要那么就调用相应的函数处理。由此形成了运行->检测->休眠 ->运行 的循环状态。
第一种类型:inputScoure消息
Port(端口,系统监听处理,内核发送消息)
网上的解释:监听程序的Mach ports,Mach ports是一个比较底层的东西,可以简单的理解为: 内核通过port这种方式将信息发送,而mach则监听内核发来的port信息,然后将其整理,打包发给runloop。
大概的作用:当用户进行UI的交互时,监听程序的Mach ports会将事件消息放到一个事件队列中提交给runloop,然后由loop交给主线程处理。系统自动给我们发送。比如我滑动一个scrollView,系统会将我们触摸事件发送给runloop,让runloop处理这些事件的响应。还有输入框中我们输入数据等等。
Custom(定制的,可理解为开发人员自定义输入源端口)
第一步: 定义输入源(数据结构)
第二步: 将输入源添加到runloop,那么这样就有了接受者,即为R1
第三步: 协调输入源的客户端(单独线程),专门监听消息,然后将消息打包成runloop能够处理的样式,即第一步定义的输入源。它类似Mach的功能
第四步:谁来发送消息的问题?上面的machport是由内核发送的。
自定义的当然要我们自己发送了。。。首先必须是另一个线程来发送(当然如果只是测试的话可以和第三步在同一个线程),先发送消息给输入源,然后唤醒R1,因为R1一般处于休眠状态,然后R1根据输入源来做相应的处理。
Selector Sources
1)NSObject类提供了很多方法供我们使用,这些方法是添加到runloop的,所以如果没有开启runloop的话,不会运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #import "ViewController.h" @interface ViewController () @property ( nonatomic ,strong) NSThread * myThread; @end @implementation ViewController - ( void )viewDidLoad { [ super viewDidLoad]; [ self alwaysLiveBackGroundThread]; // } #pragma mark —基于Input-NSPort,输入源 异步线程交互。 -( void )alwaysLiveBackGroundThread{ //第一步创建线程 NSThread * thread =[[ NSThread alloc]initWithTarget: self selector: @selector (RunMyThread) object:@ "lxlx1798" ]; thread .name =@ "myThread" ; self .myThread = thread ; //开启线程 [ thread start]; } -( void )RunMyThread{ NSLog (@ "我的线程开始运行" ); NSLog (@ "当前线程名字:%@" ,[ NSThread currentThread].name); /** * 1. 开启自己的线程中的RunLoop(如果不开启runloop 线程在执行完方法后就会销毁) 每个线程创建支持都会有一个runLoop相对应。 * 2. //非主线程的其它线程的runLoop是默认关闭的。 * 3. 我们不用Port的端口唤醒[NSRunLoop currentRunLoop];用 [[NSRunLoop currentRunLoop]performInModes:@[NSRunLoopCommonModes] block:^{ }]; 也能成功执行doBackGroundThreadWork。 * * * */ [[ NSRunLoop currentRunLoop] addPort:[[ NSPort alloc]init] forMode: NSDefaultRunLoopMode ]; NSLog (@ "给我的线程开启RunLoop" ); } -( void )touchesBegan:( NSSet <UITouch *> *)touches withEvent:(UIEvent *)event{ [ super touchesBegan:touches withEvent:event]; NSLog (@ "touchesBegan:[NSThread currentThread]%@" ,[ NSThread currentThread]); NSLog (@ "touchesBegan:self.myThread%@" , self .myThread); [ self performSelector: @selector (doBackGroundThreadWork) onThread: self .myThread withObject: nil waitUntilDone: nil ]; } -( void )doBackGroundThreadWork{ NSLog (@ "do some work %s" ,__FUNCTION__); NSLog (@ "%@" ,[ NSThread currentThread]); } - ( void )didReceiveMemoryWarning { [ super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } @end |
打印结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 2018-08-03 16:20:35.060149+0800 Person[10778:1688286] 1 2018-08-03 16:20:35.255462+0800 Person[10778:1688433] 我的线程开始运行 2018-08-03 16:20:35.255707+0800 Person[10778:1688433] 当前线程名字:myThread /** * 首次点击屏幕 */ 2018-08-03 16:20:42.148753+0800 Person[10778:1688286] touchesBegan:[ NSThread currentThread]< NSThread : 0x60400007ecc0>{number = 1, name = main} 2018-08-03 16:20:42.149725+0800 Person[10778:1688286] touchesBegan: self .myThread< NSThread : 0x604000477480>{number = 3, name = myThread} 2018-08-03 16:20:42.150491+0800 Person[10778:1688433] do some work -[ViewController doBackGroundThreadWork] 2018-08-03 16:20:42.151077+0800 Person[10778:1688433] < NSThread : 0x604000477480>{number = 3, name = myThread} /** * 再次点击屏幕 */ 2018-08-03 16:20:44.329850+0800 Person[10778:1688286] touchesBegan:[ NSThread currentThread]< NSThread : 0x60400007ecc0>{number = 1, name = main} 2018-08-03 16:20:44.330142+0800 Person[10778:1688286] touchesBegan: self .myThread< NSThread : 0x604000477480>{number = 3, name = myThread} //能成功走doBackGroundThreadWork方法 【 2018-08-03 16:20:44.330508+0800 Person[10778:1688433] do some work -[ViewController doBackGroundThreadWork] 2018-08-03 16:20:44.331205+0800 Person[10778:1688433] < NSThread : 0x604000477480>{number = 3, name = myThread} 】 |
假如我不开启我的runloop
打印结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 2018-08-03 16:20:35.060149+0800 Person[10778:1688286] 1 2018-08-03 16:20:35.255462+0800 Person[10778:1688433] 我的线程开始运行 2018-08-03 16:20:35.255707+0800 Person[10778:1688433] 当前线程名字:myThread /** * 首次点击屏幕 */ 2018-08-03 16:20:42.148753+0800 Person[10778:1688286] touchesBegan:[ NSThread currentThread]< NSThread : 0x60400007ecc0>{number = 1, name = main} 2018-08-03 16:20:42.149725+0800 Person[10778:1688286] touchesBegan: self .myThread< NSThread : 0x604000477480>{number = 3, name = myThread} /** * 再次点击屏幕 */ 2018-08-03 16:20:44.329850+0800 Person[10778:1688286] touchesBegan:[ NSThread currentThread]< NSThread : 0x60400007ecc0>{number = 1, name = main} 2018-08-03 16:20:44.330142+0800 Person[10778:1688286] touchesBegan: self .myThread< NSThread : 0x604000477480>{number = 3, name = myThread}
/注意:/
不能成功走doBackGroundThreadWork方法,因为 没有输入源去唤醒runloop,让其执行
doBackGroundThreadWork |
2)虽然说NSObject类提供了很多方法供我们使用,这些方法是添加到runloop的,所以如果没有开启runloop的话,但是有些时候不需要开启runloop也能运行。
1 2 3 4 5 6 7 8 9 10 11 12 | //系统提供的方法
// 主线程 performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes:
// 指定线程 performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes:
// 针对当前线程 performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes:
// 取消,在当前线程,和上面两个方法对应 cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object: |
下面提供的方法是在指定的线程运行aSelector,一般情况下aSelector会添加到指定线程的runloop。但是,如果调用线程和指定线程为同一线程,且wait参数设为YES,那么aSelector会直接在指定线程运行,不再添加到runloop。
1 2 3 4 5 | //在主线程中运行
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: //在指定线程运行 performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: |
其实这也很好理解,假设这种情况也添加到指定线程的runloop,我们可以这样反向理解:
1,当前线程runloop还没有开启,那么aSelector就不会被执行,然而你却一直在等待,造成线程卡死。
2,当前线程runloop已经开启,那么调用performSelector这个方法的位置肯定是处于runloop的callout方法里面,在这里等待runloop再callout从而调用aSelector方法完成,显然也是死等待,线程卡死。。。
还有一些performSelector方法,是不会添加到runloop的,而是直接执行,可以按照上面的特殊情况进行理解。方法列举如下:
1 2 3 4 | //针对当前线程 - ( id )performSelector:( SEL )aSelector; - ( id )performSelector:( SEL )aSelector withObject:( id )object; - ( id )performSelector:( SEL )aSelector withObject:( id )object1 withObject:( id )object2; |
总结:
看到这里,可能感觉有些乱,但是“只要记住没有延迟或者等待的都不会添加到runloop,有延迟或者等待的还要排除上面提到的特殊情况。”
“只要记住没有延迟或者等待的都不会添加到runloop,有延迟或者等待的还有排除上面提到的特殊情况。”
第二种类型:TimerSoucres消息
一个线程如果在没有唤醒runloop的情况下,只会执行一次就会销毁,那么我们在此线程中无论如何也不会出现定时器这种重复执行的事件。所以Timer的实现就是基于runloop。
二者的关系:
* NSTimer创建后需要被添加到runloop,否则不会运行,当然添加的runloop不存在也不会运行;
* 还要指定添加到的runloop的哪个模式,而且还可以指定添加到runloop的多个模式,模式不对也是不会运行的。
* runloop会对timer有强引用,这是因为要想Timer能够持续运行,必须要保证在每次NSRunloop执行循环操作NSTimer对象都需要存在。timer对目标对象进行强引用,这里要注意循环引用的问题。
timer的执行时间并不准确,系统繁忙的话,还会被跳过去。
第三种情况:特别说明
关于Observers(CFRunloopObservorRef 观察者监听runloop状态改变)
1>关于Observers的说明
首先它并不属于事件源(不会影响runloop的生命周期,即runloop是否退出与observe没有关系,observe只是监听runloop本身的状态而已,相当于runloop的一双眼睛),它比较特殊,主要是通过CFRunLoopActivity()函数,观察runloop自身的一些状态的,有以下几种:
节点一:进入runloop
节点二:runloop即将执行定时器
节点三:runloop即将执行输入源(Port,Customer,Selector Sources)
节点四:runloop即将休眠
节点五:runloop被唤醒,在处理完唤醒它的事件之前
节点六:退出
对应的是Activity的六种状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), //即将进入runloop kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer消息 kCFRunLoopBeforeSources = (1UL << 2), //即将处理input Sources消息 kCFRunLoopBeforeWaiting = (1UL << 5), //即将休眠 kCFRunLoopAfterWaiting = (1UL << 6), //从休眠中唤醒,处理完唤醒源之前 kCFRunLoopExit = (1UL << 7), //即将退出 kCFRunLoopAllActivities = 0x0FFFFFFFU }; |
2>添加观察者的步骤
1 2 3 4 5 6 7 8 | 第一步:创建observer CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES , 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog (@ "----监听到RunLoop状态发生改变---%zd" , activity); }); 第二步:添加观察者:监听RunLoop的状态 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); |
举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
+ ( void )observerTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ /** * 第一步:创建observer * param1: 给observer分配存储空间 param2: 需要监听的状态类型:kCFRunLoopAllActivities监听所有状态 param3: 是否每次都需要监听,如果NO则一次之后就被销毁,不再监听,类似定时器的是否重复 param4: 监听的优先级,一般传0 param5: 监听到的状态改变之后的回调 return: 观察对象 */ CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES , 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog (@ "即将进入runloop" ); break ; case kCFRunLoopBeforeTimers: NSLog (@ "即将处理timer" ); break ; case kCFRunLoopBeforeSources: NSLog (@ "即将处理input Sources" ); break ; case kCFRunLoopBeforeWaiting: NSLog (@ "即将睡眠" ); break ; case kCFRunLoopAfterWaiting: NSLog (@ "从睡眠中唤醒,处理完唤醒源之前" ); break ; case kCFRunLoopExit: NSLog (@ "退出" ); break ; default : break ; } }); // 没有任何事件源则不会进入runloop [ NSTimer scheduledTimerWithTimeInterval:3 target: self selector: @selector (doFireTimer) userInfo: nil repeats: NO ];
/** * 添加观察者:监听RunLoop的状态 */ CFRunLoopAddObserver([[ NSRunLoop currentRunLoop] getCFRunLoop], observer, kCFRunLoopDefaultMode); [[ NSRunLoop currentRunLoop] run]; }); } + ( void )doFireTimer { NSLog (@ "---fire---" ); } |
三,OSX/IOS系统中的RunLoopModes模式
什么是Model?
1 | 网上一个通俗的例子:Run Loop Mode就是流水线上支持生产的产品类型,流水线在一个时刻只能在一种模式下运行,生产某一类型的产品。消息事件就是订单。 |
Cocoa定义了五种Model
1 2 3 | NSDefaultRunLoopMode : App的默认 Mode,通常主线程是在这个 Mode 下运行的。 UITrackingRunLoopMode: 拖动事件,界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。 UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。<br> NSRunLoopCommonModes : 包含了多种模式:defaultModal, 和Tracking modes。<br> 将input sources与该模式关联则同时也将input sources与该组中的其它模式进行了关联。对于Cocoa应用,该模式缺省的包含了 default modal以及event tracking模式。 |
一个常见的问题就是,主线程中一个NSTimer添加在default mode中,当界面上有一些scroll view的滚动频繁发生导致run loop运行在UItraking mode中,从而这个timer没能如期望那般的运行。所以,我们就可以把这个timer加到NSRunLoopCommonModes中来解决(iOS中)。
备注:* runloop的模式,使得runloop显得更加灵活,适应更多的应用场景。
上面提到的事件源,都是处于特定的模式下的,如果和当前runloop的模式不一致则不会得到响应,
举个例子:
如果定时器处于mode1,而runloop运行在mode2,则定时器不会触发,只有runloop运行在mode1时,定时器才会触发。
runloop的模式,使得runloop显得更加灵活,适应更多的应用场景。
* 除了系统给我们的模式,我们自己也可以自定义。
NSRunLoopMode的类型为字符串类型,定义:typedef NSString * NSRunLoopMode,自定义类型就很简单了,
1 2 3 4 5 6 7 8 9 10 11 | //自定义NSRunLoopModel<br>- (void)modeTestTimer { NSLog (@ "mode:%@" ,[[ NSRunLoop currentRunLoop] currentMode]); } /// 这里使用非主线程,主要考虑如果一直处于customMode模式,则主线瘫痪 - ( void )runLoopModeTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer *tickTimer = [[ NSTimer alloc] initWithFireDate:[ NSDate date] interval:2 target: self selector: @selector (modeTestTimer) userInfo: nil repeats: YES ]; [[ NSRunLoop currentRunLoop] addTimer:tickTimer forMode:@ "customMode" ]; [[ NSRunLoop currentRunLoop] runMode:@ "customMode" beforeDate:[ NSDate distantFuture]]; }); } |
runloop模式的切换
对于非主线程,我们可以退出当前模式,然后再进入另一个模式,也可以直接进入另一个模式,即嵌套
对于主线程,我们当然也可以像上面一样操作,但是主线程有其特殊性,有很多系统的事件。系统会做一些切换,我们更关心的是系统是如何切换的?系统切换模式时,并没有使用嵌套
根据以上
最后总结下,thread--runloop--mode--event sources,关系可以表示如下:
三,OSX/IOS系统中的RunLoop生命周期
可以分为三步:创建->运行(开启,内部循环)->退出
1. runloop创建
苹果是不允许开发人员手动创建runloop,runloop是伴随着线程的创建而创建,线程与runloop是一一对应的,具有唯一性,另外创建还区分是否为主线程
主线程:系统会自动创建
非主线程:系统不会自动创建,开发人员必须显示的调用[NSRunLoop currentRunLoop]方法来获取runloop的时候,系统才会创建,类似懒加载
系统只提供了两种方法获取runloop,currentRunLoop和mainRunLoop,可以看出非主线程只有在自己的线程内才能获得runloop。
2. runloop运行
开启:主线程系统会自动运行,那么非主线程也是需要开发人员显式调用的,可以通过如下方法
NSRunLoop提供的方法:
1 2 3 | - ( void )run; // 默认模式 - ( void )runUntilDate:( NSDate *)limitDate; - ( BOOL )runMode:( NSRunLoopMode )mode beforeDate:( NSDate *)limitDate; |
CFRunLoop提供的函数:
1 2 3 4 | /// 默认模式 void CFRunLoopRun( void ); /// 在指定模式,指定时间,运行 CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled); |
当执行了上面的运行方法后,如果runloop所在的模式没有对应的事件源,即上面图中提到的input sources、timer sources,会直接退出当前runloop(注意:是当前)。另外注意的是,input sources里面的Selector Sources,它有一些特殊情况,上面也提到了。这些情况下runloop还是会直接退出。
网上的经典图—内部循环
3. runloop退出
可以用以下方式退出runloop
1>设置最大时间到期:推荐使用这种方式
2>modeItem(事件源)为空:但并不推荐这样退出,因为一些系统的Item我们并不知道
3>调用CFRunLoopStop,退出runloop并将程序控制权交给调用者(如果runloop有嵌套,则只退出最内层runloop),一些情况下,CFRunLoopStop并不能真正的退出runloop,比如你使用下面的2种方法开启runloop:
1 2 | - ( void )run; // 默认模式 - ( void )runUntilDate:( NSDate *)limitDate; // |
当执行NSRunLoop的run方法,一旦成功(默认模式下有事件源),那么run会不停的调用runMode:beforeDate:来运行runloop,那么即便CFRunLoopStop退出了一个runloop,很快会有另一个runloop执行。即:如果你想退出一个runloop,那么你就不该调用run方法来开启runloop
runUntilDate:与run一样不停的执行runMode:beforeDate:方法,CFRunLoopStop也是退不出来的,不同的是runUntilDate:自己有个期限,超过这个期限会自动退出
很明显,你会想到利用事件源为空来退出,这种方法不推荐。。。
runloop本身的释放。
runloop退出后,是不会被释放的(或者说立即),它大概很可能是伴随着线程的释放而释。
参考:https://www.jianshu.com/p/4263188ed940
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)