Runloop
- 使程序一直运行并接受用户输入
- 决定程序在何时应该处理哪些Event
- 调用解耦(Message Queue)
- 节省CPU时间
RunLoop对象
- iOS中有2套API来访问和使用RunLoop
- Foundation NSRunLoop
- Core Foundation CFRunLoopRef
- NSRunLoop和CFRunLoopRef都代表着RunLoop对象
- NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
主线程几乎所有函数都从以下六个之一的函数调起
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
RunLoop与线程
每条线程都有唯一的一个与之对应的RunLoop对象
主线程的RunLoop已经自动创建好了 子线程的RunLoop需要主动创建
RunLoop在第一次获取时创建,在线程结束时销毁
RunLoop相关类
Core Foundation中关于RunLoop的5个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
构成元素
CFRunLoopSourceRef
CFRunLoopSourceRef是事件源(输入源)
以前的分法
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
现在的分法
Source0:非基于Port的
Source1:基于Port的
CFRunLoopSource(事件源/输入源)
- Source是RunLoop的数据源抽象类(protocol)
- RunLoop定义了两个Version的Source:
- Source0:处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket
- Source1:由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
CFRunLoopTimer(定时源)
- RunLoopTimer的封装
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
CFRunLoopObserver(观察源)
向外部报告RunLoop当前状态的更改 框架中很多机制都由RunLoopObserver触发
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
RunLoopObserver 与 Autorelease Pool
UIKit通过RunLoopObserver在RunLoop两次Sleep间
对AutoreleasePool进行Pop和Push
将这次Loop中产生的Autorelease对象释放
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式
一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
系统默认注册了5个Mode:
kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受 其他 Mode 影响
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
CFRunLoopMode
- RunLoop在同一段时间只能且必须在一种特定Mode下Run
- 更换Mode时,需要停止当前Loop,然后重启新Loop
- Mode是iOS App滑动顺畅的关键
- 可以定制自己的Mode
NSDefaultRunLoopMode
默认状态、空闲状态UITrackingRunLoopMode
滑动ScrollView时UIInitializationRunLoopMode
私有,App启动时NSRunLoopCommonModes
Mode集合
RunLoop的挂起与唤醒
- 指定用于唤醒的mach_port端口
- 调用mach_msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach_msg_trap状态
- 由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活
RunLoop迭代执行顺序
//设定过期时间
SetupThisRunLoopRunTimeOutTimer(); //by GCD timer
do{
//通知Observer要跑timer跟source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
//运行到此刻,去检测当前加到消息队列source0的消息,此方法遍历source0去执行
__CFRunLoopDoSource0();
//询问GCD有没有分到主线程的东西需要调用
CheckIfExistMessageInMainDispatchQueue(); //GCD
//通知Observer要进入睡眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
//此刻获取到是哪个端口把我叫醒
var wakeUpPort = SleepAndWaitForWakingUpPorts();
// mach_msg_trap
// Zzz...
// Received mach_msg, wake up!
//通知Observer我要醒了~
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//Handler msgs
if(wakeUpPort == timerPort){
//如果是timer唤醒就去执行timer
__CFRunLoopDoTimer();
}else if(wakeUpPort == mainDispatchQueuePort){
//GCD需要我,就去调GCD的事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE();
}else{
//比如说网络来数据了就会用这个端口唤醒,然后做数据处理
__CFRunloopDoSource1();
}
__CFRunLoopDoBlocks();
}while (!stop && !timeOut);//如果没被外部干掉或者时间没到,继续循环
AFNetworking中RunLoop的创建
这是创建一个常驻服务线程的方法
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread =
[[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
一个TableView延迟加载图片的新思路
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
让Crash的App回光返照
接到Crash的Signal后手动重启RunLoop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {
for (NSString *mode in allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
Async Test Case
RunLoop sleep前验证
- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
__block Boolean fulfilled = NO;
void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
fulfilled = block();
if (fulfilled) {
CFRunLoopStop(CFRunLoopGetCurrent());
}
};
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// Run!
CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);
CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
return fulfilled;
}