目录
什么是RunLoop
字面意思来看,就是运行循环的意思,即在程序运行过程中循环做一些事情
作用:保证程序持续处于运行状态,处理App中的各种事件,节省CPU资源、提高程序性能
比如一个ViewController的View有一个Button,我现在没有点击它、也没有任何网络请求,像这种情况下,RunLoop就会让iOS程序的当前线程就进入休眠,CPU就不会花时间在这条线程(这个程序)上面,这条线程(这个程序)也就不会消耗资源(CPU不会给这个程序分配资源),达到节省CPU资源的目的;一旦点击了Button触发了事件,RunLoop就会唤醒这条线程,进入while循环里面做事情
这些技术都需要在RunLoop下才能进行:定时器(Timer)、PerformSelector、GCD Async Main Queue、事件响应、手势识别、界面刷新、网络请求、AutoreleasePool
在main
函数中,如果没有RunLoop,程序执行一次后就会退出;有了RunLoop,程序就会保持运行,不会退出:
objectivec
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
// iOS程序默认是有RunLoop的,因为返回了UIApplicationMain,程序会一直保持运行状态,不会退出
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
// 没有RunLoop,程序一启动就会退出
// return 0;
}
RunLoop运行逻辑(伪代码):
objectivec
// 等效伪代码
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
int retVal = 0;
do {
// 睡眠中等待消息
int message = sleep_and_wait();
// 处理消息
retVal = process_message(message);
} while (0 == retVal);
return 0;
}
}
获取RunLoop对象
iOS中有2套API
来访问和使用RunLoop
:
-
Core Foundation:CFRunLoopRef
objectivecCFRunLoopRef currentRunLoopCF = CFRunLoopGetCurrent(); // 获取当前线程RunLoop对象 CFRunLoopRef mainRunLoopCF = CFRunLoopGetMain(); // 获取主线程RunLoop对象
-
Foundation:NSRunLoop
objectivecNSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop]; // 获取当前线程RunLoop对象 NSRunLoop* mainRunLoop = [NSRunLoop mainRunLoop]; // 获取主线程RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC封装,真正底层的RunLoop对象还是得看CFRunLoopRef的地址
NSArray是基于CFArrayRef的一层封装,NSString是基于CFStringRef的一层封装
RunLoop与线程
- 每条线程都有
唯一
的一个与之对应的RunLoop对象 - RunLoop保存在一个全局的
Dictionary
里,线程作为key
,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
主线程的RunLoop会在main函数
执行UIApplicationMain
的时候自动创建,所以调用[NSRunLoop currentRunLoop]
时已经有了RunLoop对象
子线程中,默认是没有RunLoop对象的,需要调用[NSRunLoop currentRunLoop]
才会创建
源码CFRunloop.c
文件中查看对应实现:
objectivec
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// 如果没有__CFRunLoops字典,先创建一个
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 然后创建主线程的runloop,对应主线程的key存储到字典中
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 从字典__CFRunLoops里面获取runloop
// key: pthreadPointer
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果没有与之对应的runloop对象,就创建一个新的runloop
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
// 再次判断是否有该runloop对象
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 如果没有,就将新创建的runloop赋值进去
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
RunLoop底层结构
CFRunLoopRef
上面提到NSRunLoop
是CFRunLoopRef
的一层包装,所以探究其本质应查看CFRunLoopRef
源码
CFRunLoopRef
本质是一个__RunLoop
结构体类型:
(删去了不必了解的代码)
RunLoop
最核心的部分就是CFRunLoopModeRef
,表示RunLoop
的运行模式
RunLoop
对象里面的所有Mode
存在一个Set集合
里面,看下图关系:
CFRunLoopModeRef
Mode
代表RunLoop的运行模式,其作用就是将不同模式下的一些操作隔离开来
好处是:
如果应用里面有个TableView,能够滚动,RunLoop就可以办到,当TableView滚动的时候,RunLoop切换到滚动模式 ,只需要专心处理滚动模式下的事情,就不用处理默认模式下的事件,这样就能保证滚动TableView时更加流畅,当手松开停止滑动时,切回默认模式
一个RunLoop
包含若干个Mode
,每个Mode
由包含若干个Source0/Source1/Timers/Observers
,RunLoop启动时只能选择其中一个Mode
作为currentMode
如果需要切换Mode
,只能退出当前Loop
,再重新选择一个Mode
进入(切换Mode
程序不会退出,也是在内部做的切换),不同组的Mode
是分隔开来的,互不影响
如果Mode
里没有任何Source0/Source1/Timers/Observers
,RunLoop
会立马退出
常见Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode)
:App的默认Mode,通常主线程是在这个Mode下运行UITrackingRunLoopMode
:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响kCFRunLoopCommonModes
默认包括kCFRunLoopDefaultMode、UITrackingRunLoopMode
Mode中成员的用途
source0
:触摸事件处理、performSelector:onThread:source1
:基于端口Port
的线程间通信、系统事件捕捉(分发到source0去处理)Timers
:NSTimer、performSelector:withObject:afterDelay:Observers
:监听RunLoop的状态(在RunLoop
进入休眠之前唤醒刷新UI、在RunLoop
将要进入睡眠时唤醒进行内存释放)
RunLoop运行逻辑
添加观察者Observer
的方式来监听RunLoop
的状态以及模式变化
通过代码监听状态
通过函数调用的方式来监听
objectivec
// 创建Observer
// kCFRunLoopAllActivities:监听所有状态变化
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放(C语言中create创建后要对应释放观察者)
CFRelease(observer);
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
通过block
回调来监听
objectivec
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: { // 一旦有局部变量,花括号确定局部变量的作用域
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode); // copy来的要release
CFRelease(mode);
break;
}
case kCFRunLoopBeforeTimers: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopBeforeTimers - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopBeforeSources: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopBeforeSources - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopBeforeWaiting: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopBeforeWaiting - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopAfterWaiting: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopAfterWaiting - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
下面是RunLoop
状态变化的枚举注释:
通过源码分析原理
核心是RunLoopRunSpecific函数,一进入此函数
-
首先会通知Observers:进入RunLoop,并伴有当前模式currentMode
-
接下来会执行RunLoopRun函数,也就是具体要做的事情,内部主体是一个
do {...} while (!retVal)
循环- 首先会通知Observers:即将处理timers、即将处理Sources
- 处理Blocks
- 处理Source0,可能会再次处理Blocks
- 判断有无Source1
- 如果有,直接跳转到handle_msg(被Source1唤醒)
- 如果无,通知Observers:即将休眠,RunLoop进入睡眠,也就是会阻塞当前进程,等待别的消息来唤醒当前线程,通知Observers:结束休眠
- 调用handle_msg
- 如果被timer唤醒,处理timers
- 如果被GCD唤醒,处理GCD相关事情
- 如果被Source1唤醒,处理Source1
- 处理Blocks
-
根据执行结果retVal来决定是否退出
do {...} while ()
循环 -
通知Observes:退出Loop
注:RunLoop休眠时的阻塞__CFRunLoopServiceMachPort
是真正的阻塞,线程会进入休息,不再做任何事情,和写个死循环的阻塞是不一样的,死循环还是会执行代码,线程还是要一直工作的
让RunLoop进入睡眠的__CFRunLoopServiceMachPort
函数内部会调用内核层面的api
来进行真正的休眠,调用mach_msg()
我们可以分为用户态 和内核态的两种状态切换,RunLoop进入睡眠就是从用户态切换到内核态的调用,然后有消息唤醒线程时再切换到用户态来处理事件
RunLoop会处理GCD回到主线程的调用情况(GCD Async To Main),其他时候GCD都是有自己的逻辑去处理的,所以这是一个特殊情况
RunLoop的实际应用
解决NSTimer在滑动时停止工作的问题
定时器和滚动页面同时存在的情况下,可能会遇到因为页面滚动,而定时器无法响应的问题:
objectivec
[NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
该方法创建的NSTimer
,一旦开始滑动scrollView(或其子类)
,RunLoop就会只处理UITrackingRunLoopMode
下的事件,NSTimer就会失效,停止打印count
据此,可得出解决方案为,让两种模式下都嫩好处理NSTimer事件:
objectivec
NSTimer* timer = [NSTimer timerWithTimeInterval: 1.0 repeats: YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}]; // 仅仅返回了一个定时器
// CFRunLoopAddTimer(<#CFRunLoopRef rl#>, <#CFRunLoopTimerRef timer#>, <#CFRunLoopMode mode#>);
// [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] addTimer: timer forMode: UITrackingRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer: timer forMode: NSRunLoopCommonModes];
NSRunLoopCommonModes
并不是真的模式,而是一种标记,会让定时器同时能在这两种模式下都进行工作
选择通用模式会将这两种模式都添加到RunLoop底层__CFRunLoop结构体里的_commonModes
成员变量里,_commonModes意味着在通用模式下可以执行的模式列表
定时器timer也会被加入到_commonModeItems
列表中;没有设置为通用模式的定时器timer只存在于__CFRunLoopMode里面的成员变量timers
这个列表中
能同时在commonModes里工作的东西,都会放到commonModeItems里去
控制线程生命周期(线程保活)
著名的网络请求框架AFNetworking
就是使用RunLoop来控制线程生命周期的,创建一个子线程,让其一直停留在内存中,需要其做事情就通知它,因为不断创建销毁线程是非常消耗性能的
细节分析
- 控制器增加一个
Thread *thread
的属性来在多个方法能操作同一条线程 - 通过
block
的方式来创建线程是为了避免@selector
里面会对Controller
进行引用,Controller
又持有thread
,会造成循环引用,无法释放 - 在子线程里创建一个
RunLoop
对象,如果没有任何成员变量那么创建完就会马上退出,所以添加一个空白的任务Source1
,也就是[[NSPort alloc] init]
,让RunLoop
不会退出
添加的任务是个空的任务,RunLoop
其实没有任何事情要执行,所以会进入到睡眠状态,所以会卡住线程不再往下执行代码 - 采用循环创建
RunLoop
而不使用[[NSRunLoop currentRunLoop] run]
是因为run
方法的底层就是死循环进行runMode: beforeDate:
的调用,那么就无法停止所有的RunLoop
,所以我们自己实现while
循环,并通过状态值来控制什么时候可以停止创建RunLoop
- 添加任务和结束
RunLoop
都要在同一个子线程来完成 waitUntilDone:
表示是否执行完selector
的函数调用再继续往下执行代码,如果为NO
,那么执行完performSelector
,控制器就真的销毁了,这时再去调用到self
就会报野指针的错误;设置为YES
,代码会等子线程的stopThread
执行完毕才会去真正销毁控制器while
循环里进行self
的判断是为了防止执行了dealloc
方法会将弱指针weakSelf
已经置为nil
了,那么循环的判断就会为true
,再次进入创建RunLoop
;所以要确保self
有值的情况下判断条件才成立
[NSDate distantFuture]
表示的是未来无限长的时间