概述
- 主要参考一些面试中提问的点,来从核心角度理解runloop
- 想要全面理解,可以参考经典文章:深入理解Runloop
- 文章源码参考swift版开源代码swift-corelibs-foundation,这个版本更新
1. 原理
1.1 runloop与线程的关系
要想理解这个还是得参考源码,创建runloop时,内部调用的其实是_CFRunLoopGet0这个方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// 1.如果__CFRunLoops这个全局字典不存在,就创建一个
if (!__CFRunLoops) {
// 1.1 创建全局字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 1.2创建主线程的runloop,保存在全局字典中
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);
}
// 2. 创建一个runloop,先从全局字典中找,没有就创建并加入字典中
CFRunLoopRef newLoop = NULL;
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
newLoop = __CFRunLoopCreate(t);
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFUnlock(&loopsLock);
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
if (newLoop) { CFRelease(newLoop); }
// 3.检查如果是当前线程(就是说在当前线程创建了其runloop),则做额外处理
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是一对一的,主线程的runloop默认会创建,其他线程的runloop默认不创建;同一个线程的runloop是唯一的,是说已经创建了就不会再创建
- 创建时机:当线程第一次获取其runloop时,才会创建对应的runloop(我原来的理解有误)
- 销毁时机:可以肯定线程结束,就没有runloop了;具体销毁时机,暂时不明
1.2 runloop的mode作用是什么
两方面理解:
- 每种mode决定每次runloop循环在何种mode下执行,然后能够执行该mode下的操作,可以在CFRunLoopRunSpecific方法中看到runloop mode的运行逻辑
- 对于加入runloop的多个mode,还起到一个优先级的作用,就像在滚动时,系统让UITrackingRunLoopMode下的操作执行,让其他非commonModes下的操作暂停,以此保证滚动的流畅性
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
// 1.如果modeName不合法,直接return
if (modeName == NULL || modeName == kCFRunLoopCommonModes || CFEqual(modeName, kCFRunLoopCommonModes)) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CFLog(kCFLogLevelError, CFSTR("invalid mode '%@' provided to CFRunLoopRunSpecific - break on _CFRunLoopError_RunCalledWithInvalidMode to debug. This message will only appear once per execution."), modeName);
_CFRunLoopError_RunCalledWithInvalidMode();
});
return kCFRunLoopRunFinished;
}
// 2.如果runloop正在释放,return finish
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
// 3.找到了对应的mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 3.1 合理性判断
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
// 3.2 初始配置
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
// 3.3 runloop的状态是entry(就是进入),然后运行该runloop,核心逻辑就是调用__CFRunLoopRun()方法
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_START, rl, currentMode, seconds, previousMode);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_END, rl, currentMode, seconds, previousMode);
// 3.4 如果状态是exit,就销毁其中的perData等数据(可以理解是退出时释放其持有的数据)
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
理解:
- runloop每次都是在对应的mode下运行,至于在该mode下如何运行,则要继续参考函数static int32_t __CFRunLoopRun()的实现
1.2.1 runloop与mode的关系
- 一个runloop可以运行多个不同的mode,参考runloop的实现原理,每次都要在指定的mode下才能运行
- 一个mode可以有多个source/timer/observer(统称为mode item),拿timer来理解,可以在主线程中创建多个timer,将其加入commonModes中,那么每次do{}while循环到指定时间每个timer都会触发其对应的事件
- 一个mode item可以加入多个mode中,比如一个timer可以加入不同的mode中,我们加入commonModes实际上就是加入了多个mode,因为commonModes是多个mode的集合
此处的source分source0和source1两种,source1就是port,用于线程间传递数据 - mode常见的有两种:
NSRunLoopDefaultMode runloop默认就在此mode类型下运行
UITrackingRunLoopMode UIScrollView滚动时在该mode下运行
NSRunLoopCommonModes 是一个mode集合,不算是单独的mode类型 - 如何切换runloop的mode?
由下边代码可以看到,一个runloop一旦开始运行,该运行期内的mode是确定的,所以应该stop掉该runloop,再重新指定mode
1.3 runloop的实现原理
__CFRunLoopRun()函数太长,具体解释参考Run Loop记录与源码注释,用伪代码(参考:深入理解Runloop)表示:
// 0. 先判断mode里有没有source/timer/observer,如果没有,就直接返回。
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
if (__CFRunLoopModeIsEmpty(currentMode)) return;
// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 2. 进入loop
do{
// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 4. 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 5. 如果有source1,直接处理source1
__CFRunLoopServiceMachPort(dispatchPort, &msg)
// 6. 通知即将进入休眠,其实如果没有timer、mainQueue的block等任务,本次循环就算执行完了
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
// 7. 等待mach_port消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
// 8. 通知线程被唤醒了
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
// 9.1 timer时间到,执行timer
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
// 9.2.执行mainQueue中的block
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 9.3 执行source1的事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
// 10.执行加入到其中的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 11.都执行完毕,修改状态为stopped/finished/超时等
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
}while(retVal == 0);
从上边runloop的内部逻辑可以看出,runloop其实就是一个死循环,让线程一直运行或者处于休眠状态;直到超时或者停止才会结束循环
1.4 runloop的数据结构
- runloop会存在全局的dictionary中
- runloop本身结构如下:
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //线程,runloop整个是基于pthread_t的
uint32_t _winthread;
CFMutableSetRef _commonModes; //加入该runloop的commonModes
CFMutableSetRef _commonModeItems; // 所有commonModes里的items
CFRunLoopModeRef _currentMode; //当前的mode
CFMutableSetRef _modes; //加入该runloop的所有modes
struct _block_item *_blocks_head; //存放 CFRunLoopPerformBlock 函数添加的 block 的双向链表的头指针
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
2. 应用
2.1. 解决NSTimer不执行
- 大家都很6,不多写了;要注意的是加commonModes不是defaultMode,这样UISrollView滚动时它还是不会走的
- 由上方代码可知,timer执行依赖于runloop,timer执行的时间在runloop中是确定的,如果到这个点没执行,该次timer就不再执行了(这个逻辑还要结合timer的源码确定)
2.2. tableView优化
- 由于runloop会有空闲时间,我们根据runloop的回调是可以拿到进入休闲和唤醒的时间的,所以可以利用这段空闲时间做点儿事情,比如计算下cell的高度缓存下来,因为heightForRow会在滚动时频繁调用,可以参考著明的# UITableView-FDTemplateLayoutCell实现
2.3. 线程保活
- 可以用NSMachPort来做保活,相当于给runloop加了一个包含source0的mode:
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
- runloop的启动与停止,由上边分析可以看到,子线程runloop默认是不创建的,要想创建并运行可以有多种方法,最好的方法是使用CFRunloopRun()这个函数,因为这个方法创建的runloop,可以随时调用CFRunloopStop()停止该loop,参考:深入研究Runloop 与线程保活
在AFNetWorking使用NSUrlConnection时代,有著名的例子
+ (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;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
AFN该线程是一个单例,所以不需要考虑停止runloop,也就使用[[NSRunLoop currentRunLoop] run]这个就可以满足要求了
2.4. 代码监测卡顿
这个类似于tableView的优化,思路就是在子线程监听runloop的休眠和唤醒周期,计算两个时间间隔,如果间隔连续多次(比如5次)超过50ms,就认为出现了卡顿,然后获取当前runloop线程的堆栈信息,上报给监控平台
代码参考:iOS实时卡顿监控
网友评论