使用RunLoop的目的:
1) 使用端口或自定义输入源来 和其他线程通信
2) 使用线程的定时器; ( 在子线程中添加定时器 )
3) cocoa中使用任何performSelector...的方法
4) 使线程长期性工作
否则,开启一个线程的RunLoop没有意义
一 获取/创建RunLoop对象
苹果不允许直接创建RunLoop,它提供了两个自动获取的函数: CFRunLoopGetMain()和CFRunLoopGetCurrent(). 线程和RunLoop之间是一一对应的。线程创建时并没有RunLoop,且如果不去获取,那么RunLoop一直都不存在. RunLoop的创建发生在第一次获取的时候( 除了主线程的RunLoop,你只能在一个线程的内部获取自己对应的RunLoop ) ;RunLoop的销毁发生在线程结束后;
1) [NSRunLoop currentRunLoop];
2) [NSRunLoop mainRunLoop];
3) CFRunLoopGetCurrent();
4) CFRunLoopGetMain();
5) [NSRunLoop currentRunLoop].getCFRunLoop; //NSRunLoop转CFRunLoopRef
主线程的RunLoop是程序开始就创建的;子线程的RunLoop在第一次获取RunLoop对象时创建的;
二. 配置RunLoop ( 输入源source0,source1, timer, observer )
2.1 RunLoop的组成结构
CFRunLoopRef RunLoop对象
CFRunLoopModeRef RunLoop中的模式,每次只能运行在一种模式下
CFRunLoopSourceRef 输入源source0,source1
CFRunLoopTimerRef 输入源timer
CFRunLoopObserverRef RunLoop的观察者对象
RunLoop结构体的内部结构:
struct __CFRunLoop {
CFStringSetRef _commonModes; //set
CFMutableSetRef _commonModeItems; //set
CFMutableSetRef _modes; //set
CFRunLoopMideRef _currentMode ; //当前运行的mode
}
1. commonModeItems 能增加common表示的mode; modes也能增加多个RunLoopModeRef;
2. RunLoop支持查询当前线程所运行的Mode, currentMode;
3. RunLoop中存在一个common集合,用来组合几种mode,让其在commonMode时能并存运行;
RunLoopMode结构体的内部结构
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _source0; //set
CFMutableSetRef _source1; //set
CFMutableArrayRef _observers; //Array
CFMutableArrayRef _timers; //Array
}
1.每个Mode都有一个名字,用来区分不同的Mode, 以及加入commonMode;
2.管理事件输入源集合: source0, source1;
3. 管理timer输入源: timer;
4.该Mode运行时,RunLoop所处状态的观察者集合,用来获取RunLoop所处的不同状态;
可以打印 NSLog(@"%@",[NSRunLoop currentRunLoop]);来知道
(1) RunLoop的运行状态(stop or run),
(2) 在哪种mode(KCFRunLoopDefaultMode)下运行
(3) 以及注册为CommondMode的源或者timer;
(4) source, timer,observer的数量
关系:
1. CFRunLoopRef代表 RunLoop的运行模式;
2. RunLoop包含若干个Mode, 而每个Mode中又注册了若干个
3. 如果需要切换Mode,只能退出runLoop,重新指定一个Mode进入。这样不同组Mode注册的source,timer,observer相互独立;
CFRunLoopModeRef 没有对外暴露,只有系统注册的5中Mode;
CFRunLoopSourceRef
是事件产生的地方. source有两个版本:source0和source1.
(1) source0, 只包含一个回调(函数指针),它并不能主动触发事件。使用时,需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWajeUp(runLoop)来唤醒RunLoop,让其处理这个事件;
(2) source1, 包含一个mach_port和一个回调,被用于通过内核和其他线程相互发送消息.这种source能主动唤醒RunLoop的线程;
CFRunLoopTimerRef
是基于时间的触发器,和NSTimer可以混用.其包含一个时间长度和回调;当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调.
CFRunLoopObserverRef
是观察者,每个Observer都包含一个回调, 当RunLoop的状态发生变化时,观察者就能通过回调接受这个变化;
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
上面的source,timer,obsercer被统称为modeItem, 一个item可以被同时加入多个mode,一个item被重复加入同一个mode时不会有效果.如果一个mode中一个item都没有,则RunLoop会直接退出,不进入循环;
2.1 RunLoop是以指定的Mode运行的,指定的Mode必须存在一个输入源或者Timer,否则在进入Loop之前RunLoop就退出了;
1) 输入源 source0, source1
2) 定时器 timer
3) 观察者observer
2.2 生成观察者observer
observer只能通过CFRunLoopRef对象的API去添加; 生成并向RunLoop中加入observer:
CFRunLoopObserverContext context = {0, (__bridge void *)(self),NULL,NULL,NULL}; //不注册任何回调函数
CFRunLoopObserverRef observer1 = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, test, &context);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer1, kCFRunLoopDefaultMode);
void test( CFRunLoopObserverRef observer, CFRunLoopActivity activity, void * info ){ }
2.3 生成自定义事件source (source0)
2.3.1 CFRunLoopSourceContext
typedef struct {
CFIndexversion;
void *info;
const void *(*retain)(const void *info);
void(*release)(const void *info);
CFStringRef(*copyDescription)(const void *info);
Boolean(*equal)(const void *info1, const void *info2);
CFHashCode(*hash)(const void *info);
void(*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void(*cancel)(void *info, CFRunLoopRef rl,CFRunLoopMode mode);
void(*perform)(void *info);
} CFRunLoopSourceContext;
//输入源的上下文对象,用来当该输入源状态发生变化时,进行回调
CFRunLoopSourceContext context = {
0,
(__bridge void *)(self),
NULL,
NULL,
NULL,
NULL,
NULL,
/* source注册到Mode时候的回调 void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){}*/
schedule,
/* source从Mode中删除时候的回调 void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){}*/
cancel,
/* source在RunLoop中执行时的回调 void perform( void * info ){}*/
perform
};
void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){ }
void cancel( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){ }
void perform( void * info ){ }
2.3.2 CFRunLoopSourceRef
NSTimer,CFRunLoopTimerRef 或 CFRunLoopObserverRef注册在RunLoop中的某种Mode下之后,会根据时间,或者runLoop的状态自动执行回调,但是source0却需要程序员自己在其他线程中去发送信号;, 下面介绍CFRunLoopSourceRef(source0)的注册,回调使用,以及移除;
(1)创建source的环境,设置注册,执行,删除三个对应的回调;
CFRunLoopSourceContext context = {
0,
(__bridge void *)(self),
NULL,
NULL,
NULL,
NULL,
NULL,
schedule,
cancel,
perform
};
(2) 创建source (source0)
self.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
(3) 注册到指定Mode中
self.runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource( self.runLoop, self.source, kCFRunLoopDefaultMode );
(4) Source所处状态回调:( 3中状态:注册,执行,删除 )
因为注册,删除都会自动触发对应的回调方法;
关于"执行"回调的调用: 因为我们注册的事件都是source0,所以没有对应的信号能够唤醒你注册的事件,而且CFRunLoopSouorceSignal没有唤醒RunLoop对应线程的能力 ,那么只有你自己去唤醒了; 使用如下方法去唤醒该source;
CFRunLoopSouorceSignal( self.source );
CFRunLoopWakeup( self.runLoop );
(5) Source从Mode中移除:
CFRunLoopSourceInvalidate(self.source); 此时会有删除回调;
2.3.3 生成基于时间的Timer
NSTimer在RunLoop中注册:
NSTimer * timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
CFRunLoopTimerRef在RunLoop中注册
CFRunLoopTimerContext context = { 0, NULL,NULL,NULL,NULL };
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 1, 0.1, 0, 0, callback, &context);
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
2.3.4 生成基于端口的源:
NSPort, NSMachPort,CFMessagePortRef
现在ios系统不允许生成带有名字的port,否则直接报错并且crash; 只能使用匿名port,匿名port是不会被回调的;现在的唯一作用就是如AFNetworking中使用一样,注册一个匿名port,让RunLoop能进入Loop不退出;
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
2.2 线程安全与RunLoop对象
CFRunLoopRef线程安全
NSRunLoop线程不安全,所以操作应该在runloop自身对应的线程的中完成;
三.启动RunLoop
3.1 启动的API:
1) [[NSRunLoop currentRunLoop] run]; //无条件且以默认的NSDefaultRunLoopMode启动
2) [[NSRunLoop currentRunLoop] runUntilDate:[NSDate new]]; //指定过期时间且以默认的NSDefaultRunLoopMode启动
3) [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate new]];//指定过期时间,指定启动方式
4) CFRunLoopRun(); //子线程的runLoop需要启动
5) CFRunLoopRunInMode(<#CFRunLoopMode mode#>, <#CFTimeInterval seconds#>, <#Boolean returnAfterSourceHandled#>)
3.2 Int32 result = CFRunLoopRunInMode( kCFRunLoopDefaultMode. 10, YES );
if( result==kcFRunLoopRunStopped || result==kCFRunLoopFinished )
{ … }
注意: RunLoop启动的Mode,必须存在至少一个输入源或者Timer,否则无法进入Loop,其中并不包括Observer;
四.退出RunLoop
让RunLoop退出的方法:
(1)给RunLoop设置超时时间;
(2) 通知RunLoop停止
CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
(3) 删除Mode中的所有输入源; (这种方法不太好用)
五.杂谈
1. 因为Mode是以stringModeName去对应相应的CFRunLoopModeRef,且CFRunLoopModeRef并没有创建方式,所以我们能使用的只有两种, KCFRunLoopDefaultMode 和 UITrackingRunLoopMode; 一般就是用的KcFRunLoopDefaultMode;
2. 关于kCFRunLoopCommonModes,
为什么这个不算能使用中的一种,这种只是timer,source添加进Mode中的一种方式,被添加进来的timer,source会被具有common标记 的Mode所共有;即RunLoop 并不会有kCFRunLoopCommonModes这种运行状态,只是将该Mode下注册的timer,source0让其他mode共有;
至于如何让其他Mode具有common标志,不用担心,你知道的KCFRunLoopDefaultMode 和 UITrackingRunLoopMode都已经加上了common标志;
3.关于定时器与页面滑动:
很多人在不懂之前,写的定时器与页面滑动的事件相冲突,即页面滑动时,定时器不工作: 主要原因是页面滑动时,主线程的RunLoop会停止,并且以 UITrackingRunLoopMode形式启动,这个时候schedule方式生成的NSTimer处在KCFRunLoopDefaultMode下,所以不会被运行;解决办法时,将NSTimer注册到kCFRunLoopCommonModes下,则NSTimer在Mode切换时仍然可以运行;
4.runLoop的执行流程:
(1) 以指定Mode启动之后,根据ModeNameString生成对应Mode,检查当前Mode中是否存在Item(source,timer),(observer不算),如果没有,RunLoop直接返回;如果存在Item,则(2);
(2) 通知Observer,RunLoop即将进入Loop; 在 CFRunLoopRun()之后的程序表达式在RunLoop运行时不会指定到的,因为进入了loop;
(3) 通知Observer,即将触发timer回调;
(4) 通知observer,即将出发source0(非port)回调;
(5) 执行source0的回调;
(6) 检查是否有source1(基于port)处于ready状态,如果有就直接处理source1,然后跳转去消息处理;
(7) 如果runLoop没有任何任务,就通知observer,RunLoop即将进入休眠(sleep),否则跳过休眠;
(8)在休眠的RunLoop被唤醒 , 比如CFRunLoopWakeup( self.runLoop );唤醒之后,处理该事件,并且开始新的一轮loop;
5. 主线程中的RunLoop状态函数:
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
尤其注意 mach_msg() -> mach_msg_trap() 睡眠状态;
6 关于定时器NSTimer, CFRunLoopTimerRef
NSTimer有个属性叫做tolerance(宽容度),标记了当时间到点之后容许的最大误差;如果这个时间点错过了,那么就需要等待下一个时间点到来了;
重点:
(1)关于UI界面刷新:
当在操作UI时,比如改变frame,更新了UIView/CALayer的层次时,或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标 记为待处理,并被提交到一个全局的容器中. 苹果注册了一个Observer监听BeforeWaiting(即将进入休眠)和Exit(即将退出Loop)事件,回调去执行一个很长的函数。
ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。
这个函数里会便利所有待处理的UIView/CALayer以执行实际的绘制和调整,并且更新UI界面。( UIView/CALayer的实际绘制和调整必须在主线程中,但是待处理标记可以在其他线程中操作 );
(2)网络中的运行 NSRULConnection
source1 RunLoop CFMultiplexer CFHTTPCooklie storage
CFSocket —-—--------------------------------———-> NSRULconnectionLoader ——————————------------------------——> delegate
在使用NSURLConnection时,你会传入一个Delegate,当调用[connection start]后,这个Delegate就会不停地收到事件回调。 实际上,start这个函数的内部会获取CurrentRunLoop,然后在其中的KCFDefaultMode中添加4个source0(需要手动触发的source). CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的.
当开始网络传输时,我们可以看到NSURLConnection创建了两个新的线程Lcom.apple.NSURLConnectionLoader和com,apple,CFSocked.private.CFSocket负责最底层的socket连接,NSURLConnectionLoader这个线程内部会使用RunLoop来接受底层socket的事件,并通过之前添加的Source0通知上层的Delegate; NSURLConnectionLoader中的RunLoop通过一些基于mach port的source接受来自底层CFSocket的通知,当收到通知后,其会在适合的时机向CFMutliplexSource等source0发送通知,同时唤醒Delegate线程的RunLoop来让其处理这些通知.CFMultiplexerSource会在Delegate线程的RunLoop对Delegate执行实际回调;
7. 当子线程进入runLoop,如何注册事件或抛给该线程执行某些任务:
由于IOS禁止了基于端口port的通信,所以只能用系统自带的方法:
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>];
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>];
将任务抛给指定的线程,且会注册到对应的RunLoop,这种方法会被立刻执行;