[TOC]
为什么是RunLoop
因为iOS是事件驱动,类似需要一个死循环在底下跑着,没事就闲着,有事才唤醒干活。
- 使程序一直活着,并接受用户输入
- 决定程序在何时应该处理哪些事件
- 调用解耦(Message Queue)
- 节省CPU时间
Run Loop in Cocoa
- Foundation
- NSRunLoop
- Core Foundation
- CFRunLoop
System: GCD(有点不一样),mach kernel,Block,Pthread...
六个被调起方法
主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起:
- CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
CFRunloop is calling out to an abserver callback function
用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation
- CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
CFRunloop is calling out to a block
消息通知、非延迟的perform、dispatch调用、block回调、KVO
- CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
- CFRunloop is servicing the main desipatch queue
- CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
CFRunloop is calling out to a timer callback function
延迟的perform, 延迟dispatch调用
- CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
CFRunloop is calling out to a source 0 perform function
处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用
- CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
CFRunloop is calling out to a source 1 perform function
由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
(lldb) bt
// 例子 打印iOS在普通状态下直接点击暂停
CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
frame #29: 0x00000001bc08414c CoreFoundation`__CFRunLoopDoSource0 + 88
frame #30: 0x00000001bc083a30 CoreFoundation`__CFRunLoopDoSources0 + 176
frame #31: 0x00000001bc07e8fc CoreFoundation`__CFRunLoopRun + 1040
frame #32: 0x00000001bc07e1cc CoreFoundation`CFRunLoopRunSpecific + 436
frame #33: 0x00000001be2f5584 GraphicsServices`GSEventRunModal + 100
frame #34: 0x00000001e9179054 UIKitCore`UIApplicationMain + 212
frame #35: 0x0000000102cba7a0 testcopy`main(argc=1, argv=0x000000016d14b970) at main.m:14
frame #36: 0x00000001bbb3ebb4 libdyld.dylib`start + 4
Run Loop的构成
RunLoopTimer
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:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
/* Create a new display link object for the main display. It will
* invoke the method called 'sel' on 'target', the method has the
* signature '(void)selector:(CADisplayLink *)sender'. */
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
/* Adds the receiver to the given run-loop and mode. Unless paused, it
* will fire every vsync until removed. Each object may only be added
* to a single run-loop, but it may be added in multiple modes at once.
* While added to a run-loop it will implicitly be retained. */
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
source
- Source是RunLoop的数据源抽象类(protocol)
- RunLoop定义了两个Version的Source;
- Source0:处理App内部事件,App自己负责管理(触发),如UIevent、CFSocket
- Source1:由RunLoop和内核管理,Mach port驱动,如CFMachport和CFMessagePort
Observer
- kCFRunLoopEntry -- 进入runloop循环
- kCFRunLoopBeforeTimers -- 处理定时调用前回调
- kCFRunLoopBeforeSources -- 处理input sources的事件
- kCFRunLoopBeforeWaiting -- runloop睡眠前调用
- kCFRunLoopAfterWaiting -- runloop唤醒后调用
- kCFRunLoopExit -- 退出runloop
/* Run Loop Observer Activities */
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
};
知识点
Observer 与autoreleasePool
UIKit通过RunLoopObserver在RunLoop在两次Sleep之间对AutoReleasePool进行Pop和Push,将这次Loop中产生的AutoRelease对象释放
App启动之后,系统启动主线程并创建了RunLoop,在 main thread 中注册了两个 observer ,回调都是_wrapRunLoopWithAutoreleasePoolHandler()
- 第一个observer监听了一个事件:
- 即将进入Loop(kCFRunLoopEntry)其回调会调用 _objc_autoreleasePoolPush() 创建一个栈自动释放池,这个优先级最高,保证创建释放池在其他操作之前。
- 第二个observer监听了两个事件:
- 准备进入休眠(kCFRunLoopBeforeWaiting)此时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 来释放旧的池并创建新的池。
- 即将退出Loop(kCFRunLoopExit)此时调用 _objc_autoreleasePoolPop()释放自动释放池。这个 observer 的优先级最低,确保池子释放在所有回调之后。
在主线程中执行代码一般都是写在事件回调或Timer回调中的,这些回调都被加入了main thread的自动释放池中,所以在ARC模式下我们不用关心对象什么时候释放,也不用去创建和管理pool。(如果事件不在主线程中要注意创建自动释放池,否则可能会出现内存泄漏)。
RunLoop的挂起与唤醒
指定用于唤醒的mach_port端口
调用mach_port监听唤醒端口,被唤醒前系统内核,被唤醒前系统内核将这个线程挂起,停留在mach_msg_trap状态
由另一个线程向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续工作
CFRunloopMode
特点
- RunLoop在同一段时间只能且必须在一种特定的Mode下run
- 更换Mode时,需要停止当前Loop,然后重启Loop
- Mode是iOS App流畅的关键
RunLoop的mode类型
- NSDefauleRunLoopMode :默认状态,不滑动,空闲状态下程序就会自动切换到这个mode
- UITrackingRunLoopMode :滑动状态
- UIInitializationRunLoopMode :私有的,可以追踪的,app启动的时候就是这个状态,第一个页面加载之后才回到defaultMode
- NSRunLoopCommonModes:默认状态下包括NSDefauleRunLoopMode和UITrackingRunLoopMode
定时器与scrollview卡顿问题
默认定时器是加到NSDefaultRunLoopMode中的,而scrollview滑动的时候是在UITrackingRunLoopMode。
[NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer * _Nonnull timer) {
//do something
}];
如果不希望Timer被scrollview的滑动影响,需要添加到NSRunLoopCommonModes下就可以了。
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer * _Nonnull timer) {
//do something
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
RunLoop与GCD
dispatch_get_main_queue()
GCD中dispatch到main queue的block被分发到main RunLoop执行
//关键堆栈--->_dispatch_main_queue_callback_,__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000104cd6690 testcopy`__29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x0000000104cd80b0) at ViewController.m:37
frame #1: 0x00000001050b3824 libdispatch.dylib`_dispatch_call_block_and_release + 24
frame #2: 0x00000001050b4dc8 libdispatch.dylib`_dispatch_client_callout + 16
frame #3: 0x00000001050c2a78 libdispatch.dylib`_dispatch_main_queue_callback_4CF + 1360
frame #4: 0x00000001bc083dd0 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
frame #5: 0x00000001bc07ec98 CoreFoundation`__CFRunLoopRun + 1964
frame #6: 0x00000001bc07e1cc CoreFoundation`CFRunLoopRunSpecific + 436
frame #7: 0x00000001be2f5584 GraphicsServices`GSEventRunModal + 100
frame #8: 0x00000001e9179054 UIKitCore`UIApplicationMain + 212
frame #9: 0x0000000104cd6758 testcopy`main(argc=1, argv=0x000000016b12f970) at main.m:14
frame #10: 0x00000001bbb3ebb4 libdyld.dylib`start + 4
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);//如果没被外部干掉或者时间没到,继续循环
过程思路
- 跑while循环之前需要设置GCD来设置时间,不然会成为死循环
- 然后告诉Observer要跑timer和source
- 然后遍历消息队列中的source0的消息并执行
- 询问GCD有没有分到主线程的东西需要调用
- 通知进入睡眠挂起状态
- 然后卡在函数SleepAndWaitForWakingUpPorts这里直至被唤醒
-
通知被唤醒,根据端口类型去执行处理的事件
RunLoop实践
RunLoop与AFNetworking
//旧版2.6之前,使用AFURLConnectionOperation类,自己创建线程并添加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;
}
Topic: Tableview卡顿与RunLoop
思路:将需要的耗时动作类似图片下载放到RunLoop中defaultRunLoopMode中处理,因为滑动是在UItrackMode下的,就不会在滑动的线程下下载,只有滑动完毕回到defaultRunLoopMode下才会调用
UIImage *downLoadImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
Topic: 让Crash的app回光返照
- program received signal:SIGABRT SIGABRT一般是过度release或者发送unrecogized selector导致。
- EXC_BAD_ACCESS是访问已被释放的内存导致,野指针错误。
由 SIGABRT 引起的Crash 是系统发这个signal给App,程序收到这个signal后,就会把主线程的RunLoop杀死,程序就Crash了 该例只针对 SIGABRT引起的Crash有效。
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//获取所有Mode,因为可能有很多Mode,每个Mode都需要跑,此处可以选择提交下崩溃信息之类的
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"程序崩溃了" message:@"崩溃信息" delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];
[alertView show];
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runloop));
while (1) {
//快速切换Mode
for (NSString *mode in allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
资料来源
孙源的Runloop视频整理
iOS线下分享Runloop--孙源
IOS开发日志之RunLoop的原理和使用
RunLoop的前世今生