RunLoop的学习总结.docx
- 文档编号:29675300
- 上传时间:2023-07-26
- 格式:DOCX
- 页数:22
- 大小:71.23KB
RunLoop的学习总结.docx
《RunLoop的学习总结.docx》由会员分享,可在线阅读,更多相关《RunLoop的学习总结.docx(22页珍藏版)》请在冰豆网上搜索。
RunLoop的学习总结
RunLoop的学习总结
一.RunLoop相关概念
1.什么是RunLoop
RunLoop与线程相关且是基础框架的一部分。
一个RunLoop就代表一个事件处理循环,它可以不停的调度工作以及处理输入事件。
使用RunLoop的目的是有效的控制线程的执行和休眠,让线程在有工作的时候忙于工作,而在没工作的时候处于休眠状态。
如果不使用RunLoop类似的循环机制,线程执行完当前任务队列中的任务就结束了,程序不能持续运行。
也可以把RunLoop理解成一个高级的死循环,这个死循环可以让程序持续运行,且可以时刻监听和处理各种事件。
每一个线程都有唯一对应的RunLoop,主线程的RunLoop是默认开启的;子线程的RunLoop要显示开启且至少添加一个事件源source。
我们不需要显示的创建RunLoop,因为RunLoop是懒加载的,在Cocoa和CoreFundation中都提供了关于RunLoop对象的API来帮助配置和管理线程对应的RunLoop。
2.RunLoop的简单剖析
下图是RunLoop与Source的联系图,图中的左边方框代表一个线程,线程的开始Start到结束End之间有一个RunLoop,当这个RunLoop一直存在的时候,线程就不会销毁。
方框中黄色的圈圈代表一个RunLoop,圈圈左边代表当有事件源时,RunLoop就会被唤醒(线程也随之被唤醒),RunLoop会先检测定时事件源,再检测关于performSelector:
onThread:
的事件源,再检测自定义事件源,最后检测基于端口的事件源。
圈圈右边代表当没有事件源时,RunLoop和线程都处于睡眠状态。
当然RunLoop也可以通过runUntilDate:
方法设定过期时间来退出,当时间到的时候,RunLoop退出,线程也随之销毁。
图中的右边代表事件源的类型,它们分别是:
基于端口的事件源、自定义事件源、关于performSelector:
onThread:
的事件源、定时事件源,前三种又统称为输入事件源。
只有当RunLoop存在时,才能保证这些事件源能被处理;如果RunLoop不存在,当前线程运行到End时,线程就会被销毁了,之后如果再有事件源尝试在这个线程中处理事件,系统就会崩溃报错。
RunLoop与Source的联系示意图:
提示:
输入事件源传递异步事件,通常消息来自于其它线程或程序;定时事件源传递同步事件,事件发生在特定时间或者重复的时间间隔。
RunLoop会在处理事件之前发出通知,但要监听这些通知,必须注册一个观察者observer添加到RunLoop中才可监听。
3.RunLoop的运行模式
一个RunLoop要能运行,必须要有一个运行模式。
RunLoop的一个运行模式是所有要监听的输入源、定时源、观察者的集合。
当要运行一个RunLoop时,必须指定(无论显示还是隐式指定)一个运行模式。
在RunLoop的运行过程中,只有和模式相关的输入源和定时源才会被处理,只有和模式相关的观察者才会被激活。
和其它运行模式相关的输入源、定时源、观察者,只有在其相关的模式下才能被运行,否则处于暂停状态。
通过指定RunLoop的运行模式可以使得RunLoop在某一阶段过滤来源于源的事件。
大多数时候,RunLoop都是运行在系统定义的默认模式上。
CFRunLoopModeRef对象代表RunLoop的一个运行模式,一个Runloop对象可以有多个运行模式,但至少有一个运行模式,每个运行模式内又包含若干个Source/Timer/Observer,运行模式内的Source/Timer/Observer可以没有,但是如果没有,RunLoop对象运行时就直接退出了。
当要切换运行模式时,必须先停止当前的运行模式,才能启动新的运行模式,这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。
系统默认注册的5个运行模式:
NSDefaultRunLoopMode:
App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:
界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode的影响
UIInitializationRunLoopMode:
刚启动App时进入的第一个Mode,启动完成后就不再使用,开发中一般不用
GSEventReceiveRunLoopMode:
接受系统事件的内部Mode,通常用不到
提示:
在CoreFoundation的底层有一个mutableset类型的集合CommonModes,集合中保存着NSDefaultRunLoopMode和UITrackingRunLoopMode,NSRunLoopCommonModes是用来标记它们的,只要使用了NSRunLoopCommonModes,就相当于同时使用NSDefaultRunLoopMode和UITrackingRunLoopMode
4.Source的分类
按照官方文档:
a.基于端口的输入事件源
b.自定义输入事件源
c.Cocoa关于performSelector的事件源
d.定时事件源
按照函数调用栈:
a.Source0:
非基于Port的源
b.Source1:
基于Port的源,通过内核和其他线程通信,能接收和分发系统的事件
c.定时事件源
5.输入事件源
输入源异步的发送消息给你的线程。
事件来源可以分为两种:
基于端口的输入源和自定义输入源。
基于端口的输入源监听程序相应的端口。
自定义输入源则监听自定义的事件源。
RunLoop不关心输入源的是基于端口还是自定义的。
两类输入源的区别在于是谁发送:
基于端口的输入源由内核自动发送,而自定义的输入源则需要人工从其他线程发送。
创建好了事件源,还需要把事件源分配给RunLoop的一个或多个运行模式,当RunLoop运行在被添加到的运行模式时,事件源才会被监听到。
基于端口的输入事件源
Cocoa和CoreFoundation支持与端口相关的对象和函数,用它们来创建的基于端口的源。
在Cocoa中不需要直接创建输入源,只要简单地创建端口对象,并使用NSPort的方法把该端口添加到RunLoop中,端口对象会自己创建和配置输入源,基于端口的事件源处理完后不会自动从RunLoop中移除。
在CoreFoundation中,必须人工创建端口和它的事件源,可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建相关对象。
voidcreatePortSource(){
//创建消息端口
CFMessagePortRefmessagePort=CFMessagePortCreateLocal(kCFAllocatorDefault,CFSTR("com.someport"),myCallbackFunc,NULL,NULL);
//创建自定义基于端口的源
CFRunLoopSourceRefsourcePort=CFMessagePortCreateRunLoopSource(kCFAllocatorDefault,messagePort,0);
//把自定义基于端口的源加入RunLoop
CFRunLoopAddSource(CFRunLoopGetCurrent(),sourcePort,kCFRunLoopCommonModes);
//运行RunLoop
CFRunLoopRun();
//RunLoop退出后移除自定义基于端口的源
CFRunLoopRemoveSource(CFRunLoopGetCurrent(),source,kCFRunLoopDefaultMode);
//撤销引用
CFRelease(source);
}
自定义输入事件源
要创建自定义输入源,只能使用CoreFoundation里面与CFRunLoopSourceRef类型相关的函数来创建。
可以使用回调函数来配置自定义输入源,当CoreFundation配置源时,会在多处地方调用回调函数,处理输入事件,当事件源被移除的时要清理它。
除了定义自定义输入源的行为,还要定义消息传递机制;事件源的消息传递在线程里面实现,并负责在数据等待处理的时候传递数据给事件源并通知它处理数据;消息传递机制的定义逻辑取是随意的,但最好不要过于复杂。
voidcreateCustomSource(){
//定义自定义输入源的上下文
CFRunLoopSourceContextcontext={0,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL};
//创建自定义输入源
CFRunLoopSourceRefsource=CFRunLoopSourceCreate(kCFAllocatorDefault,0,&context);
//把自定义输入源加入RunLoop
CFRunLoopAddSource(CFRunLoopGetCurrent(),source,kCFRunLoopDefaultMode);
//运行RunLoop
CFRunLoopRun();
//RunLoop退出后移除自定义输入源
CFRunLoopRemoveSource(CFRunLoopGetCurrent(),source,kCFRunLoopDefaultMode);
//撤销引用
CFRelease(source);
}
Cocoa关于performSelector的事件源
在Cocoa中,可以使用关于performSelector的方法自定义事件源,一个selector执行完后会自动从RunLoop里面移除。
关于performSelector:
onThread:
…的方法,目标线程要有一个活动的RunLoop,否则系统可能崩溃,因为目标线程可能已经被销毁。
当目标线程是非主线程时,要显示开启RunLoop,才能保证程序正常执行。
关于performSelector:
…afterDelay:
…的方法,也需要有一个活动的RunLoop,因为它会自动创建一个NSTimer对象加入当前的RunLoop,如果在子线程中使用,需要手动启动RunLoop,事件才会被处理。
//在主线程中执行selector
performSelectorOnMainThread:
withObject:
waitUntilDone:
performSelectorOnMainThread:
withObject:
waitUntilDone:
modes:
//在指定线程中执行selector
//目标线程必须要有一个活动的RunLoop,否则可能崩溃,因为目标线程可能已经被销毁
performSelector:
onThread:
withObject:
waitUntilDone:
performSelector:
onThread:
withObject:
waitUntilDone:
modes:
//在当前线程执行selector,可设置延迟时间
//会自动创建一个NSTimer对象加入当前的RunLoop,如果在子线程中使用,需要手动启动RunLoop,事件才会被处理
performSelector:
withObject:
afterDelay:
performSelector:
withObject:
afterDelay:
inModes:
//在当前线程执行selector
performSelector:
performSelector:
withObject:
performSelector:
withObject:
withObject:
//撤销消息
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:
selector:
object:
6.定时事件源
定时事件源在预设的时间点以同步方式传递消息。
定时器可以通知线程在某一时间处理某件事,但定时器并不是实时机制。
定时器可以配置成仅工作一次或重复工作,当它是仅工作一次时,处理完事件后定时器就会被自动移除出RunLoop;当是重复工作时,会一直存在于RunLoop中。
如果定时器所在的运行模式不是当前RunLoop的运行模式,那么定时器将不会开始,只有RunLoop的运行模式是定时器所在的运行模式定时器才会被启动。
类似的,如果定时器在运行期间,RunLoop的运行模式被切换了,定时器不在被切换的运行模式,定时器就会暂停。
如果定时器在切换模式时,被延迟以至于它错过了一个或多个触发时间,那么被错过的触发事件会被忽略,定时器会在下一个最近的触发时间重新启动,后面的触发事件会正常的按照时间间隔执行。
7.RunLoop的观察者
事件源是在同步或异步事件发生时触发的,而RunLoop的观察者则是在RunLoop本身运行的特定情况下触发的。
RunLoop的观察者是在事件即将被触发之前收到通知的,所以可以使用RunLoop的观察者来监听某一事件即将被触发,且可以在这些事件被触发前做一些准备工作。
RunLoop的观察者可以仅用一次或循环使用,若仅用一次,那么在它被触发后,会把它自己从RunLoop里面移除,而循环使用的观察者则不会。
要往RunLoop中添加观察者,只能使用CoreFundation的相关函数创建和添加观察者。
观察者可以监听的状态为:
typedefCF_OPTIONS(CFOptionFlags,CFRunLoopActivity){
kCFRunLoopEntry=(1UL<<0),//即将进入RunLoop,枚举成员对应的整数为1
kCFRunLoopBeforeTimers=(1UL<<1),//即将处理Timer,枚举成员对应的整数为2
kCFRunLoopBeforeSources=(1UL<<2),//即将处理Source,枚举成员对应的整数为4
kCFRunLoopBeforeWaiting=(1UL<<5),//即将进入休眠,枚举成员对应的整数为32
kCFRunLoopAfterWaiting=(1UL<<6),//刚从睡眠中唤醒,枚举成员对应的整数为64
kCFRunLoopExit=(1UL<<7),//即将退出RunLoop,枚举成员对应的整数为128
kCFRunLoopAllActivities=0x0FFFFFFFU//监听所有状态
};
一般使用下面两个函数添加观察者,函数原型为:
//allocator:
用于分配observer的内存空间
//activities:
用以设置observer要监听的状态
//repeats:
用于设置是否只监听一次
//order:
用于设置observer的优先级,一般为0
//block:
用于设置observer的回调代码
//observer:
当前的observer对象
//activity:
当前的状态
CFRunLoopObserverRefCFRunLoopObserverCreateWithHandler(CFAllocatorRefallocator,CFOptionFlagsactivities,Booleanrepeats,CFIndexorder,void(^block)(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity));
//rl:
observer要加入的RunLoop
//observer:
要加入的观察者observer
//mode:
要加入的运行模式
voidCFRunLoopAddObserver(CFRunLoopRefrl,CFRunLoopObserverRefobserver,CFStringRefmode);
添加观察者举例:
//创建观察者observer
CFRunLoopObserverRefobserver=CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities,YES,0,^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity){
//回调,即将要切换到activity时调用
NSLog(@"----监听到RunLoop状态发生改变---%zd",activity);
});
//添加观察者,监听RunLoop在kCFRunLoopDefaultMode下的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(),observer,kCFRunLoopDefaultMode);
//释放Observer
CFRelease(observer);
提示:
在使用CoreFundation中的函数时,凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release。
release的方法:
CFRelease(对象);
8.RunLoop的事件队列处理逻辑
每次运行RunLoop时,线程的RunLoop会自动处理之前未处理的消息,并通知相关的观察者。
具体的顺序如下:
判断当前的运行模式是否为空,为空直接退出RunLoop
通知观察者RunLoop已经启动
通知观察者任何即将要开始的定时器
通知观察者任何即将要启动的非基于端口的源
启动任何准备好的非基于端口的源
如果基于端口的源准备好并处于等待状态,立即启动。
并进入步骤9
通知观察者线程进入休眠
将线程置于休眠直到有下面的任一事件发生:
a.某一事件到达基于端口的源
b.定时器启动
c.RunLoop设置的退出时间已到
d.RunLoop被显式唤醒
通知观察者线程将被唤醒
处理未处理的事件
a.如果用户定义的定时器启动,处理定时器事件并重启RunLoop。
进入步骤3
b.如果输入源启动,传递相应的消息
c.如果RunLoop被显式唤醒而且退出时间未到,重启RunLoop。
进入步骤3
通知观察者RunLoop结束
上面RunLoop的事件队列处理逻辑,可以通过分析Apple开源的CoreFundation代码中去理解,可以参考我的博客【关于RunLoop部分源码的注释】,也可以【在线浏览CoreFundation开源代码】和【下载CoreFundation开源代码】,还可以查看官方文档【ThreadingProgrammingGuide】里面的RunLoop部分的内容。
补充:
因为输入源和定时器的观察者是在相应的事件发生之前传递消息的,所以通知的时间和实际事件发生的时间之间可能存在误差。
在运行RunLoop时,定时器和其它周期性事件经常需要被传递,当撤销RunLoop时,也会终止消息传递。
二.RunLoop的使用场景
仅当在子线程中才需要显式启动RunLoop,主线程是默认开启的。
对于子线程,RunLoop的启动不是必须的,仅当在需要的时候才去配置并启动它。
不需要在任何情况下都去配置和启动一个线程的RunLoop,因为当此线程在处理完所以任务时,它不能被自动释放,这就会占用一定的内存空间。
比如,当使用子线程来处理一个预先定义的需要长时间运行的任务时,没必要启动RunLoop,因为这条线程所处理的任务会在特定的时间内会结束,线程运行期间不会自动销毁,只有在任务结束时才会被销毁,所以不需要用到RunLoop,用RunLoop的目的是在线程还需要处理任务时保证它不被销毁。
RunLoop是在要和线程有更多的交互时才需要,比如以下情况:
使用端口或自定义输入源来和其他线程通信
使用线程的定时器
在Cocoa中使用以performSelector开头的部分方法(上文已经解释)
使线程周期性工作,即让线程常驻内存,等待其他线程发来消息,处理其他事件
在特定的运行模式下执行特殊任务(如:
图片延迟加载,默认模式不加载,滚动时才去加载图片)
三.RunLoop对象
在iOS中有2套API来访问和使用RunLoop,分别是Foundation和CoreFoundation,它们都提供了配置输入源、定时器和RunLoop的观察者以及启动RunLoop的接口,NSRunLoop是基于CFRunLoopRef的一层OC包装。
RunLoop在Cocoa中被称为NSRunLoop类的一个实例,而在Carbon或BSD程序中则是一个指向CFRunLoopRef类型的指针。
每个线程都有唯一的与之关联的RunLoop对象。
RunLoop对象不用人工显示创建,只要是第一次使用的时候系统会自动创建一个RunLoop对象,并会把它保存在一个字典中,当再次使用的时候直接从字典中取,这种机制也称为懒加载。
可以通过官方开放源码进行分析。
1.获取RunLoop对象
获取RunLoop对象有下面两种方式:
在Cocoa程序中,使用NSRunLoop的currentRunLoop类方法来获取/访问NSRunLoop对象
//获得当前线程的RunLoop对象
NSRunLoop*runloop=[NSRunLoopcurrentRunLoop];
//获得主线程的RunLoop对象
NSRunLoop*runloopMain=[NSRunLoopmainRunLoop];
使用CoreFoundation的函数获取/访问RunLoop对象
//获得当前线程的RunLoop对象
CFRunLoopRefrunloop=CFRunLoopGetCurrent();
//获得主线程的RunLoop对象
CFRunLoopRefrunloopMain=CFRunLoopGetMain();
虽然上面两种创建方式不一样,但是它们都指向同一个RunLoop,这两种获取方式可以在需要的时候混合使用。
NSRunLoop类定义了一个getCFRunLoop方法,该方法可以返回一个CFRunLoopRef类型的RunLoop。
CFRunLoopRefrunloop=[[NSRunLoopcurrentRunLoop]getCFRunLoop];
2.配置RunLoop
在子线程中,要启动RunLoop,至少要在运行模式mode中添加一个输入源或定时器,因为RunLoop启动时先检查运行模式是否为空,如果为空就直接退出RunLoop。
上文已经介绍过观察者的创建和添加,下面程序将用另外一个函数创建观察者,其中SJMThread类是继承自NSThread类,重写main方法,在RunLoop中添加定时器和观察者。
#import"SJMThread.h"
@implementationSJMThread
/**观察者的回调函数*/
voidobserverCallBack(CFRunLoopObserverRefobserver,
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- RunLoop 学习 总结