前言:一般代码运行完就结束了,为何APP就一直能保持运行状态呢?这个秘诀就是RunLoop,本文先介绍了RunLoop的概念,引出它的作用,并在CoreFoundation框架源码查看它的底层结构;然后介绍RunLoop和线程的关系,并介绍它运行的各种模式;最后介绍RunLoop在实际项目中的应用,比如解决NSTimer在滑动时停止工作的问题,线程保活和性能优化等问题。
一、RunLoop概念
1、RunLoop概念:
顾名思义,运行循环,在程序运行过程中循环做一些事情。应用范畴有:定时器(Timer)、PerformSelector、GCD、事件响应、手势识别、界面刷新、网络请求、AutoreleasePool等。
2、RunLoop作用:
如果没有RunLoop,运行代码
int main(int argc, char * argv[]) {
@autoreleasepool {
// return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"Hello world!");
}
return 0;
}
程序运行完就结束了,App也就退出了,所以RunLoop作用有以下几条:
1)保持程序的持续运行;
2)处理App中的各种事件(比如触摸事件、定时器事件等);
3)节省CPU资源,该做事时做事,该休息时休息等;
3、RunLoop构成:
下载CoreFoundation框架源码, 在CFRunLoop.c文件中找到RunLoop的底层结构:
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list 线程锁 */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //RunLoop对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes; //记录所有标记为common的mode
CFMutableSetRef _commonModeItems; //存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; //当前运行的mode
CFMutableSetRef _modes; //存储的是CFRunLoopModeRef
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
可见,RunLoop底层是一个结构体,主要包含了一个线程,当前运行的Mode,若干个commonMode,若干个commonModeItem等。
二、RunLoop和线程
程序运行的时候需要一个常驻线程,可以让线程接收到事件的时候干活,没事的时候休眠。我们执行下面的伪代码,一直等待消息,线程就不会退出了。
do {
// 睡眠中等待消息
// 接收消息
// 处理消息
} while (消息 != 退出)
那么RunLoop和线程的关系是怎样呢?
1)每条线程都有唯一的一个与之对应的RunLoop对象;
2)RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value;
3)线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;
4)RunLoop会在线程结束时销毁;
5)主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop;
在RunLoop官方文档中,可以看到RunLoop和线程的关系
图中展现了RunLoop 在线程中的作用:从 input sources 和 timer sources 接受事件,然后在线程中处理事件。
三、RunLoop对象
1、获取RunLoop对象:
iOS中有两套来访问和使用RunLoop。
1)Foundation框架:NSRunLoop,NSRunLoop是基于CFRunLoopRef的OC的封装;
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
2)CoreFoundation框架:CFRunLoopRef;
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
2、CFRunLoopModeRef-运行模式:
Mode模式可以看做事件的管家,一个Mode管理着各种事件,在CFRunLoop.c文件中结构:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode名称
Boolean _stopped; //mode是否被终止
char _padding[3];
CFMutableSetRef _sources0; //sources0 触摸事件处理,performSelector:onThread:
CFMutableSetRef _sources1; //sources1 基于port的线程通信,系统事件捕捉
CFMutableArrayRef _observers; //观察者 用于监听RunLoop状态,UI刷新,AutoreleasePool
CFMutableArrayRef _timers; //定时器
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet; //端口
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
一个CFRunLoopMode对象有一个name,若干个sources0、sources1、observers、timers和ports,可见事件都是由Mode在管理,而RunLoop管理Mode。RunLoop运行时总是指定一种Mode,就是currentMode,当切换Mode时必须退出当前Mode,重新进入RunLoop。注意:实际开发中有三个使用
1)NSDefaultRunLoopMode:这个是默认模式,使用最多;
2)UITrackingRunLoopMode:界面追踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响;
3)NSRunLoopCommonModes:这个不是一种模式,默认包括NSDefaultRunLoopMode和UITrackingRunLoopMode;
比如常用定时器代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 创建tableView
[self tableView];
// 创建一个定时器,并把它放在当前的RunLoop上,模式是NSDefaultRunLoopMode
// self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
// 创建一个定时器,但是必须手动把它添加到RunLoop中,添加的时候可以指定Mode
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
// 默认模式,运行执行timerAction,当滑动tableView时,timerAction就不会执行
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
// 通用模式组合,运行执行timerAction,当滑动tableView时,timerAction依然继续执行
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
// UI追踪模式,此时默认不执行timerAction,当滑动tableView时,才会执行timerAction
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:UITrackingRunLoopMode];
}
四、RunLoop运行逻辑
1、RunLoop运行逻辑
在CFRunLoop.c文件中可以找到CFRunLoopRun()、CFRunLoopRunInMode()、CFRunLoopRunSpecific等函数。
// 用DefaultMode启动
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
如上图所示,RunLoop大概运行逻辑是:
1、通知Observers:即将进入Loop,kCFRunLoopEntry;
2、通知Observers:即将处理Timers,kCFRunLoopBeforeTimers;
3、通知Observers:即将处理Sources,kCFRunLoopBeforeSources;
4、处理blocks和source0,_CFRunLoopDoBlocks和__CFRunLoopDoSources0;
5、如果有source1,跳转到第9步;
6、通知Observer:线程即将休眠,kCFRunLoopBeforeWaiting;
7、休眠,等待唤醒,等待Timer或者source1事件或者被手动唤醒,__CFRunLoopServiceMachPort;
8、通知Observer:线程刚被唤醒,kCFRunLoopAfterWaiting;
9、处理唤醒时收到的消息,之后跳转到第2步,_ CFRunLoopDoTimers、_CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE和__CFRunLoopDoSource1;
10、通知Observer:即将退出Loop,kCFRunLoopExit;
2、RunLoop状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timers
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
3、RunLoop各个事件的作用
1)Source0:
- performSelector:onThread:;
2)Source1:
- 基于Port的线程间通信;
- 系统事件捕捉,事件响应(严格来说,Source0也参与了事件响应);
3)Timers:
- NSTimer;
- performSelector:withObject:afterDelay:;
4)Observers:
- 用于监听RunLoop的状态;
- UI刷新(BeforeWaiting);
- Autorelease pool(BeforeWaiting);
五、RunLoop在系统的应用
通过打印主线程RunLoop,可以看到,系统默认注册了5个Mode:
- kCFRunLoopDefaultMode:App的默认 Mode,通常主线程是在这个 Mode 下运行的。
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
- GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到。
- kCFRunLoopCommonModes:这是一个占位的 Mode,没有实际作用。
- UIInitializationRunLoopMode:刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。这个Mode实际测试中没找到。
1)AutoreleasePool:App启动后,苹果在主线程 RunLoop 里注册了两个 Observer。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
2)事件响应:苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件,Source1 接收IOHIDEvent,之后再回调__IOHIDEventSystemClientQueueCallback()内触发的Source0,Source0再触发的 _UIApplicationHandleEventQueue()。
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin /Move /End /Cancel 事件都是在这个回调中完成的。
3)手势识别:当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
4)界面更新:当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数,这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
5)NSTimer、CADisplayLink:都使用到了RunLoop,并受到RunLoop的影响。想知道更多细节,请移步-NSTimer、GCD定时器、CADisplayLink详细分析。
6)PerformSelecter:当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
7)关于GCD:当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并执行该block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
六、RunLoop项目应用
1、解决NSTimer在滑动时停止工作的问题
如上讲述,创建timer后,将它放进当前的RunLoop,Mode格式为NSRunLoopCommonModes,NSRunLoopCommonModes包含NSDefaultRunLoopMode(没有触摸事件)和UITrackingRunLoopMode(有触摸事件),所以不管有没有滑动事件都会执行timer。NSTimer有个属性tolerance容忍度,NSTimer依赖于RunLoop,所以执行timer的时间不会非常准确,使用的时候需要注意这点。
如果想知道NSTimer、GCD定时器、CADisplayLink更加详细分析,请移步-NSTimer、GCD定时器、CADisplayLink详细分析。
2、控制线程生命周期(线程保活)
有时候我们需要常驻线程来处理频繁的事务,比如早期的AFNetworking创建一个常驻线程处理网络事务,比如监测网络状态等。
默认情况一个线程创建出来,运行完要做的事情,线程就会消亡。而程序启动的时候,创建的主线程已经加入到mainRunLoop中,所以主线程不会消亡。
线程保活代码:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"mainThread = %@", [NSThread mainThread]);
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
self.thread = thread;
thread.name = @"KeepThread";
[self.thread start];
}
// 子线程运行
- (void)runThread {
NSLog(@"runThread = %@", [NSThread currentThread]);
// 给runloop添加一个NSPort,就是添加一个事件源,也可以添加一个timer,或者observer,让runloop不会挂掉
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"-----runThread end-----"); //不会执行
}
// 测试线程保活
- (void)testThread {
NSLog(@"testThread = %@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(testThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
点击屏幕触发performSelector,打印结果:
mainThread = <NSThread: 0x600001488680>{number = 1, name = main} //主线程
runThread = <NSThread: 0x6000014f9100>{number = 5, name = KeepThread} //运行子线程
testThread = <NSThread: 0x6000014f9100>{number = 5, name = KeepThread} //子线程保活
3、性能优化
1)tableView图片显示优化:由于图片渲染到屏幕需要消耗较多资源,为了提高用户体验,当用户滚动Tableview的时候,只在后台下载图片,但是不显示图片,当用户停下来的时候才显示图片。
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:1.0 inModes:@[NSDefaultRunLoopMode]];
2)AsyncDisplayKit:AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架(最新的改为Texture),其原理大致如下:
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。
ASDK 仿照QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
3)第二条是Facebook写好的框架,下面自己手动写下计算cell的预缓存高度的伪代码:
- (void)viewDidLoad {
[super viewDidLoad];
CFRunLoopRef currentLoop = CFRunLoopGetCurrent(); //获取当前的RunLoop
CFStringRef mode = kCFRunLoopDefaultMode; //mode
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// 在RunLoop处于“空闲”状态时进行计算
NSLog(@"Thread = %@", [NSThread currentThread]);
sleep(1);
CFRunLoopRemoveObserver(currentLoop, observer, mode); //移除observer
});
CFRunLoopAddObserver(currentLoop, observer, mode); //添加observer
}
觉得写的不错,有些启发或帮助,点个赞哦!