第二十八节—RunLoop(三)
本文为L_Ares个人写作,以任何形式转载请表明原文出处。
经历过前两节对RunLoop对象的本质结构和RunLoop逻辑的探索,对RunLoop的基本概念和性质也有了初步的了解,本节就开始根据平常常见的RunLoop的使用场景,探索一些RunLoop的应用场景。
一、RunLoop与线程
1. RunLoop和线程的关系
先说一下RunLoop和线程的关系 :
- RunLoop与线程是一一对应的,一个RunLoop对应一个核心线程。为什么说对应一个核心线程,是因为RunLoop是可以嵌套的,但是核心的线程只有一个,也就是第一节在
CFRunLoop
结构体里面看到的pthread
。- RunLoop和线程的关系是存储在一个全局的Dictionary里面的,线程是
key
,RunLoop是value
。- RunLoop是用来管理线程的,当线程的RunLoop被开启后,线程会在执行完成任务后进入休眠状态,直到有新的消息来唤醒线程执行新消息。
- RunLoop的创建是通过第一次获取它来完成的,在线程任务结束时被销毁。
- 对于主线程来说,RunLoop是程序一启动的时候就通过
UIApplicationMain
中的[NSRunLoop currentRunLoop]
来获取,如果没有就创建。- 对于子线程来说,RunLoop是懒加载的,只有我们获取子线程的RunLoop的时候才会被创建。
2. 线程获取RunLoop的实现
这里我们看一下,在第一节RunLoop提到的获取线程的方式 :
Core Foundation框架下获取RunLoop
CFRunLoopGetMain(); //获取主线程的RunLoop对象
CFRunLoopGetCurrent(); //获取当前线程的RunLoop对象
Foundation框架下获取RunLoop
[NSRunLoop mainRunLoop]; //获取主线程的RunLoop对象
[NSRunLoop currentRunLoop]; //获取当前线程的RunLoop对象
因为找不到Foundation框架
的开源源码,所以这里我们看一下Core Foundation框架
中的两个获取RunLoop对象的方法是怎么实现的。
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
//一个静态的mainRunLoop
static CFRunLoopRef __main = NULL; // no retain needed
//如果mainRunLoop不存在,则去创建主线程的RunLoop
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
//返回mainRunLoop
return __main;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
//获取当前线程的RunLoop
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
//如果当前线程的RunLoop是存在的,直接返回
if (rl) return rl;
//如果当前线程的RunLoop不存在了,则创建当前线程的RunLoop再返回
return _CFRunLoopGet0(pthread_self());
}
也就是说获取线程的核心方法是_CFRunLoopGet0();
看一下它的实现 :
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//t==0等同于让主线程始终工作
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//判断传入的线程是不是t==0
if (pthread_equal(t, kNilPthreadT)) {
//如果是t==0,那么获取主线程
t = pthread_main_thread_np();
}
//加一个runloop用的锁
__CFLock(&loopsLock);
//如果不存在保存RunLoop和线程关系的字典
if (!__CFRunLoops) {
//解锁
__CFUnlock(&loopsLock);
//创建一个全局的可变字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//创建主线程的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//设置key=主线程的地址指针,value=主线程的RunLoop,存入全局可变字典
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//从字典里面获取当前线程的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
//解锁
__CFUnlock(&loopsLock);
//判断如果当前线程的RunLoop不存在
if (!loop) {
//给当前线程创建一个新的RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
//上锁
__CFLock(&loopsLock);
//重新获取当前线程的RunLoop
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
//如果RunLoop并没有在创建期间被加入全局字典
if (!loop) {
//那么就把创建的新的RunLoop作为value,当前线程作为key,存入到全局字典中去
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
//并且设置当前线程的RunLoop是新创建的RunLoop
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
//不要在loopsLock中释放RunLoop,因为CFRunLoopDeallocate可能最终会使用它
__CFUnlock(&loopsLock);
//解锁了再释放
CFRelease(newLoop);
}
//判断一下现在还是不是在当前线程操作
if (pthread_equal(t, pthread_self())) {
//设置当前线程的RunLoop
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
//返回RunLoop
return loop;
}
里面都有加了注释,理解起来并不困难,就不多说了。
3. RunLoop常驻线程
为什么要常驻线程?
因为如果你要频繁的使用子线程的时候,最好不要频繁的创建和销毁,这样很消耗性能。
思路就是利用输入源一直传输事件,不让RunLoop休眠。因为如果一个线程任务执行完毕了,RunLoop没有事件源要处理了,RunLoop会随着线程的消亡而消亡。
#import "ViewController.h"
#import "JDThread.h"
@interface ViewController ()
@property (nonatomic,strong) JDThread *myThread;
@property (nonatomic,assign) BOOL thread_stop;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread_stop = NO;
[self jd_runloop_alwaysLoop];
// Do any additional setup after loading the view.
}
- (void)jd_runloop_alwaysLoop
{
//上面给了属性是strong,强引用持有,所以给个弱引用
__weak typeof(self) weakSelf = self;
//创建线程
self.myThread = [[JDThread alloc] initWithBlock:^{
NSLog(@"开始 : --- 当前线程 : %@",[NSThread currentThread]);
//获取当前线程的RunLoop
//并且添加一个source/port : 事件源
//或者也可以添加observer或者timer,总之目的就是维持mode的存在,有事情能做
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//只要当前控制器还没释放,并且没有让线程停止,那么就一直循环执行一个事件
while (weakSelf && !weakSelf.thread_stop) {
//可以用[[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"结束 : --- 当前线程 : %@",[NSThread currentThread]);
}];
//别忘了开始执行线程
[self.myThread start];
}
- (void)testRun
{
NSLog(@"当前线程 : %@ --- 执行的方法 : %s",[NSThread currentThread],__func__);
}
- (void)stop_thread
{
self.thread_stop = YES;
//停止线程
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"当前线程 : %@ --- 执行的方法 : %s",[NSThread currentThread],__func__);
self.myThread = nil;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (!self.myThread) return;
[self performSelector:@selector(testRun) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)dealloc
{
NSLog(@"当前方法 : %s",__func__);
if (!self.myThread) return;
[self performSelector:@selector(stop_thread) onThread:self.myThread withObject:nil waitUntilDone:YES];
}
@end
JDThread.m
:
#import "JDThread.h"
@implementation JDThread
- (void)dealloc
{
NSLog(@"%s",__func__);
}
@end
二、RunLoop与NSTimer
1. RunLoop与NSTimer简单实用
- 在前面的文章中,我们已经知道了
NSTimer
本质上是一个CFRunLoopTimerRef
,而它的本质又是指向__CFRunLoopTimer
结构体的指针。- 与主线程不同,子线程的RunLoop因为不是默认就开启的,所以当我们在子线程上使用
NSTimer
的时候,需要手动的将NSTimer
添加到子线程的RunLoop上。- 即便是主线程,根据
NSTimer
的初始化方法不同,也不是全部都可以直接被加入到主线程的。
那么NSTimer
和RunLoop到底存在着怎样的关系,我们可以通过NSTimer
的初始化方法来探索。
一般情况下,被经常使用的NSTimer
的创建方式有两种 :
- 以
timerWithTimeInterval
开头的 :
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
不会直接将
timer
加入到当前线程,当调用了[timer fire]
之后,即使是repeat:YES
也仅被调用一次。
- 以
scheduledTimerWithTimeInterval
开头的 :
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
会直接将
timer
加入到当前的RunLoop中,mode
为 :NSDefaultRunLoopMode
。
问题 :
那么经常会出现的问题是在操作UI的时候,如果将timer
加入到主线程的RunLoop,并且只将mode
设置为 :NSDefaultRunLoopMode
,则timer
不会执行事件,一直到主线程不再被占用,并且到达了给timer
设置的执行时间点的情况下才会调用。
解决方法 :
解决的方法是将mode
变为 :NSRunLoopCommonModes
,因为这个模式是一个集合,包括了NSDefaultRunLoopMode
和UITrackingRunLoopMode
,所以两个mode的事件源都会响应,并且都是在主线程。
2. 为什么NSRunLoopCommonModes可以解决上面的问题
看一下Core Foundation
的源码,搜索CFRunLoopAddTimer
:
/**
* @param CFRunLoopRef rl : 添加timer的runloop对象
* @param CFRunLoopTimerRef rlt : timer对象
* @param CFStringRef modeName : runloopmode的名字
**/
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
//如果要添加timer的Runloop对象已经正在释放了,就不要添加了,直接返回
if (__CFRunLoopIsDeallocating(rl)) return;
//判断timer对象是否存在,timer关联的runloop是否存在,
//并且timer当前关联的runloop不能是要添加它的runloop,如果是的话直接返回,不需要添加了
if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
//给当前的runloop加锁,防止在其他地方操作
__CFRunLoopLock(rl);
//如果当前要将timer添加到Runloop的commonModes集合下的话
if (modeName == kCFRunLoopCommonModes) {
//先判断Runloop对象是否有commonModes集合
//如果有 : 则直接拿到集合,否则set为NULL
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
//如果RunLoop对象没有commonModesItems
if (NULL == rl->_commonModeItems) {
//创建一个RunLoop的commonModes集合
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
//将timer添加到RunLoop的commonModeItems集合里面
CFSetAddValue(rl->_commonModeItems, rlt);
//如果RunLoop的commonModes集合不为空
if (NULL != set) {
//把runloop对象和timer包装成数组
CFTypeRef context[2] = {rl, rlt};
//添加一个新的commonModesItems,也就是添加一个新的事件到RunLoop里面
/* add new item to all common-modes */
//这里就是遍历commonModes集合,然后对每一个标示(defaultMode和trackingMode)调用
//第二个参数那个函数,也就是在每一个commonModes的mode对象中都注册了一个timer的事件源
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
//如果timer不是加到commonModes集合下面
//根据传入的mode名字查找runLoop里面对应的mode
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
//如果可以找到这个mode
if (NULL != rlm) {
//并且这个mode的timers数组为空
if (NULL == rlm->_timers) {
//这是一个存放回调的集合。
//存放着当CFArray数组中的元素都是CFType类型时,适用的回调
CFArrayCallBacks cb = kCFTypeArrayCallBacks;
cb.equal = NULL;
//创建一个可变的RunLoopMode的定时器数组
rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
}
}
//如果RunLoop没有这个mode,并且timer的集合中也不包含这个mode
if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
//给timer加个锁
__CFRunLoopTimerLock(rlt);
//判断如果timer结构中的runloop不存在
if (NULL == rlt->_runLoop) {
//将timer结构中的runloop设置成当前的runloop对象
rlt->_runLoop = rl;
} else if (rl != rlt->_runLoop) {
//如果timer结构中的runloop和当前的runloop对象不一样
//解锁
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
//直接返回
return;
}
//给mode中的定时器数组添加这个timer
CFSetAddValue(rlt->_rlModes, rlm->_name);
__CFRunLoopTimerUnlock(rlt);
//执行一次
__CFRunLoopTimerFireTSRLock();
__CFRepositionTimerInMode(rlm, rlt, false);
__CFRunLoopTimerFireTSRUnlock();
if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {
// Normally we don't do this on behalf of clients, but for
// backwards compatibility due to the change in timer handling...
if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
}
注释写的很详细了,重点看一下注释,然后你会发现,就算是通过NSRunLoopCommonModes
进来,也会有一行代码 : CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
,这句代码看一下第二个参数__CFRunLoopAddItemToCommonModes
的源码 :
static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
CFStringRef modeName = (CFStringRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
//只看这里就够了
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}
就看我加了注释的那一句,就算你是
NSRunLoopCommonModes
进来,也会给NSDefaultRunLoopMode
和UITrackingRunLoopMode
全部都添加timer
事件源。
所以,NSRunLoopCommonModes
下,commonModes里面的所有元素都会有timer
事件源,也就都会执行NSTimer
事件。