一、runloop 简介
RunLoop
是通过内部维护的 事件循环(Event Loop
) 来对 事件/消息 进行管理的一个对象。
- 没有消息处理时,休眠已避免资源占用,由用户态切换到内核态。
- 有消息需要处理时,立刻被唤醒,由内核态切换到用户态。
runloop
的官方文档在thread
篇章Run Loops,也就从侧面说明了runloop
是与线程息息相关的。
1.1 runloop 输入源与处理机制
官方有如下一张图:
`runloop`与`source`结构
线程的输入源:
-
Port Source
:基于端口的输入源。 -
Custom Source
:自定义输入源 -
performSelector
:Cocoa
执行Selector
的源。 -
Timer Source
:定时源。
线程针对输入源的处理机制:
-
handlePort
:处理基于端口的输入源。 -
customSrc
:处理用户自定义输入源。 -
mySelector
:处理Selector
的源。 -
timerFired
:处理定时源。
有以下案例:
- (void)sourcesTest {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationAction:) name:@"notificationTest" object:nil];
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer action");
}];
[self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:1.0];
//
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
// __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__
void (^block)(void) = ^{
NSLog(@"block action");
};
block();
// __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"main queue");
});
}
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
- (void)performSelectorAction {
NSLog(@"timer action");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
NSLog(@"touches action");
[[NSNotificationCenter defaultCenter] postNotificationName:@"notificationTest" object:nil];
}
- (void)notificationAction:(NSNotification *)noti {
// __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__
NSLog(@"notification action");
}
timer
与performSelector
对应的回调都是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
:
后续的调用函数不同。
block
对应__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
:
不过后续调用到了
__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__
。
主线程对应__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
:
系统触摸事件对应__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
:
通知事件对应__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__
:
小结:
-
调用
timer
:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
。NSTimer
与performSelector
都属于timer
。 -
响应
source0
:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
。block
,系统触摸事件(触摸事件先交给source1
唤醒runloop
然后交给source0
处理)。 -
响应
source1
:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
。处理系统事件。 -
GCD
主队列:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
。 -
observer
源:__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
。通知。
二、CFRunloop 的使用
2.1 CFRunLoopMode 验证
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(@"scrollViewDidScroll: %@",[NSRunLoop currentRunLoop].currentMode);
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
NSLog(@"scrollViewDidEndDecelerating: %@",[NSRunLoop currentRunLoop].currentMode);
}
- (void)runloopModeTest {
//获取当前runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//获取当前mode
CFRunLoopMode runloopMode = CFRunLoopCopyCurrentMode(runloop);
NSLog(@"runloopMode: %@",runloopMode);
//获取所有 mode
CFArrayRef modeArray= CFRunLoopCopyAllModes(runloop);
NSLog(@"modeArray: %@",modeArray);
NSTimer *timer = [NSTimer timerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timerWithTimeInterval mode: %@",[[NSRunLoop currentRunLoop] currentMode]);
}];
//timer 添加到 runloop 的 commonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
滚动页面输出:
runloopMode: kCFRunLoopDefaultMode
//主线程runloop modes
modeArray: (
UITrackingRunLoopMode,
GSEventReceiveRunLoopMode,
kCFRunLoopDefaultMode,
kCFRunLoopCommonModes
)
//页面静止
timerWithTimeInterval mode: kCFRunLoopDefaultMode
//页面滚动
scrollViewDidScroll: UITrackingRunLoopMode
scrollViewDidScroll: UITrackingRunLoopMode
timerWithTimeInterval mode: UITrackingRunLoopMode
scrollViewDidScroll: UITrackingRunLoopMode
scrollViewDidEndDecelerating: UITrackingRunLoopMode
//页面静止
timerWithTimeInterval mode: kCFRunLoopDefaultMode
页面滚动过程中处于UITrackingRunLoopMode
,静止状态处于kCFRunLoopDefaultMode
。
2.2 CFRunLoopTimerRef
@interface ViewController () {
CFRunLoopTimerRef timerRef;
}
@end
- (void)cfTimerTest {
/** CFRunLoopTimerContext timer 上下文
version: 版本
info: 传递参数
void *(*retain)(const void *info): retain 操作
void (*release)(const void *info): release 操作
(*copyDescription)(const void *info): copy 描述信息
*/
CFRunLoopTimerContext context = {
0,
((__bridge void *)self),
NULL,
NULL,
NULL
};
//获取当前 runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
/** CFRunLoopTimerCreate 创建 timer
allocator: 用于分配对象的内存
fireDate:在什么是触发 (距离现在)
interval: 每隔多少时间触发一次
flags: 未来参数
order: CFRunLoopObserver的优先级 当在Runloop同一运行阶段中有多个CFRunLoopObserver 正常情况下使用0
callout: 回调,比如触发事件。
context:上下文记录信息
*/
//创建timer
timerRef = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 3, 0, 0, _runLoopTimerCallBack, &context);
CFRunLoopAddTimer(runloop, timerRef, kCFRunLoopCommonModes);
}
// CFRunLoopTimerCallBack typedef void (*CFRunLoopTimerCallBack)(CFRunLoopTimerRef timer, void *info);
void _runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info) {// info 为 context 中的 info
NSLog(@"_runLoopTimerCallBack:%@,%@,%@",timer,info,[NSRunLoop currentRunLoop].currentMode);
}
- (void)dealloc {
[self invalidCFTimer];
}
// 移除 timer
- (void)invalidCFTimer {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
if (timerRef && CFRunLoopContainsTimer(runloop, timerRef, kCFRunLoopCommonModes)) {
CFRunLoopRemoveTimer(runloop, timerRef, kCFRunLoopCommonModes);
}
}
-
CFRunLoopTimerContext
创建上下文用于传递参数,info
就是最终callback
回调中的info
信息。 -
CFRunLoopTimerCreate
主要是回调函数以及时间间隔的设置。 -
dealloc
中需要移除timer
,不释放有可能crash
,不会造成循环引用。需要判断timer
是否存在以及是否在要移除的runloop
中。
2.3 CFRunLoopObserverRef
CFRunLoopObserverRef observerRef;
- (void)cfObserverTest {
//context 与 timer 的相同
CFRunLoopObserverContext context = {
0,
((__bridge void *)self),
NULL,
NULL,
NULL
};
CFRunLoopRef runloop = CFRunLoopGetCurrent();
/**
allocator: 用于分配对象的内存
activities: 要关注的事件,与 runloop 循环中处理的状态一致。
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
};
repeats: CFRunLoopObserver 是否循环调用,不循环只会有一次回调。NO的情况下 CFRunLoopObserverRef 会自动移除。
order: CFRunLoopObserver 的优先级 当在Runloop同一运行阶段中有多个 CFRunLoopObserver 正常情况下使用 0
callout: 回调
context: 上下文记录信息
*/
observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, NO, 0, _runLoopObserverCallBack, &context);
CFRunLoopAddObserver(runloop, observerRef, kCFRunLoopDefaultMode);
}
//typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
void _runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
NSLog(@"observerCallBack: %@,%lu,%@",observer,activity,info);
}
- (void)removeRunloopObserver {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
if (observerRef && CFRunLoopContainsObserver(runloop, observerRef, kCFRunLoopDefaultMode)) {
CFRunLoopRemoveObserver(runloop, observerRef, kCFRunLoopDefaultMode);
}
}
- (void)dealloc {
[self removeRunloopObserver];
}
-
CFRunLoopObserverContext
与CFRunLoopTimerContext
结构相同。 -
CFRunLoopObserverCreate
可以只监听某些状态。-
repeats
控制是否循环调用,不循环的情况下回调只会调用一次(所有状态假加起来),并且会自动从runloop
中移除。
-
可以通过
observer
进行卡顿检测相关逻辑处理。
2.4 CFRunLoopSourceRef
2.4.1 source0
- (void)source0Test {
/**
typedef struct {
CFIndex version; //版本
void * info; //回调info信息
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2); //判等
CFHashCode (*hash)(const void *info); //hash code
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); //准备代发回调
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); //取消回调
void (*perform)(void *info); // 执行回调
} CFRunLoopSourceContext;
*/
CFRunLoopSourceContext context = {
0,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
schedule,
cancel,
perform,
};
/**
allocator: 传递NULL或 kCFAllocatorDefault 以使用当前默认分配器。
order: 优先级索引,指示处理运行循环源的顺序。这里传0为了的就是自主回调
context: 为运行循环源保存上下文信息的结构
*/
//创建source
CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//source 指定了 runloop 与 mode 就进去就绪状态了。schedule 调用
CFRunLoopAddSource(runloop, source0, kCFRunLoopDefaultMode);
//一个执行信号 perform
CFRunLoopSourceSignal(source0);
//这里加延迟为了让 perform 有机会执行。
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//唤醒 runloop 防止沉睡状态
CFRunLoopWakeUp(runloop);
//取消移除 source,执行 cancel 回调
CFRunLoopRemoveSource(runloop, source0, kCFRunLoopDefaultMode);
});
}
void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"schedule 准备代发");
}
void perform(void *info){
NSLog(@"source0 执行");
}
void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"source0 cancel");
}
输出:
schedule 准备代发
source0 执行
source0 cancel
-
CFRunLoopSourceContext
相比timer
和observer
多了判等以及哈希相关计算外。更重要的是schedule 、cancel 、perform 3
个回调。 -
CFRunLoopSourceCreate
相比就比较简单了,重要参数是context
。
2.4.2 source1 线程间通信
@interface ViewController ()<NSPortDelegate>
@property (nonatomic, strong) NSPort* subThreadPort;
@property (nonatomic, strong) NSPort* mainThreadPort;
@end
- (void)source1Test {
self.mainThreadPort = [NSPort port];
self.mainThreadPort.delegate = self;
// port - source1 -- runloop
[[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
[self task];
}
- (void)task {
__weak typeof(self) weakSelf = self;
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"initWithBlock:%@", [NSThread currentThread]); // 5
weakSelf.subThreadPort = [NSPort port];
weakSelf.subThreadPort.delegate = weakSelf;
[[NSRunLoop currentRunLoop] addPort:weakSelf.subThreadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
}
#pragma mark --- NSPortDelegate ---
- (void)handlePortMessage:(id)message {//NSPortMessage 是macos中的类。
NSLog(@"handlePortMessage: %@, thread:%@",message,[NSThread currentThread]);
NSArray *componentsArray = [message valueForKey:@"components"];
for (NSInteger i = 0; i < componentsArray.count; i++) {
NSData *data = componentsArray[i];
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"dataStr: %@",dataStr);
}
//runtime 获取ivar。
// unsigned int count = 0;
// Ivar *ivars = class_copyIvarList([message class], &count);
// for (int i = 0; i < count; i++) {
// NSString *name = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
// NSLog(@"data: %@",name);
// }
sleep(1);
if (![[NSThread currentThread] isMainThread]) {
NSMutableArray *components = [NSMutableArray array];
//必须转成data,否则会被忽略。
NSData *data = [@"hotpot" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
//从 subport 给 mainport 发送数据。
NSLog(@"mainport send data to subport");
[self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"cat" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
//从 mainport 给 subThreadPort 发送数据。
NSLog(@"subport send data to mainport");
[self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}
输出:
image.png
-
mainThreadPort
是在主线程创建添加进runloop
的,subThreadPort
是在子线程创建添加进runloop
的。 -
handlePortMessage :
为NSPortDelegate
的代理方法。参数NSPortMessage
是定义在macOS
中的。iOS
没有该类的定义。 - 通过
sendBeforeDate:components:from: reserved :
进行线程间数据传递。从from port
给调用方port
线程发送数据。 - 数据需要包装为
NSData
才有效。
image.png
NSPortMessage
定义在macOS
中:
三、runloop 结构
既然runloop
是一个事件循环,那么它与普通的循环有什么区别呢?
普通循环:
runloop
循环:
那么可以得到以下结论:
-
runloop
能保持程序的持续运行。 - 处理
APP
中的各种事件(触摸、定时器、performSelector
)。 - 节省
cpu
资源、提高程序的性能。有休眠和唤醒状态。
3.1 runloop 源码定位
那么runloop
是怎么做到的呢?
通常我们会通过NSRunLoop
去获取当前的runloop
:
[NSRunLoop currentRunLoop];
定义如下:
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
给currentRunLoop
下符号断点:
可以看到
NSRunLoop
是对CFRunLoop
的封装。
image.png
通过之前的分析已经定位到了runloop
是在CoreFoundation
中的 CoreFoundation源码。正好CoreFoundation
开源了CFRunLoop
:
3.2 CFRunLoopRun
//CFRunLoopRun 是对 do...while 的封装
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
//run
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
//不是完成或者结束
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
-
runloop
底层是对do...while
的封装。 - 当状态为完成或者结束后退出循环。
那么核心逻辑就在CFRunLoopRunSpecific
中。还有一个疑问是runloop
可以休眠,那么它是如何实现的呢?
3.3 runloop 数据结构
要了解runloop
的实现原理,首先要清楚它的数据结构。
3.3.1 线程与runloop的关系
CFRunLoopRunSpecific
的第一个参数是CFRunLoopGetCurrent()
:
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
//通过 key-value 形式获取 CFRunLoopRef
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
//没有缓存通过线程获取
return _CFRunLoopGet0(pthread_self());
}
- 去缓存中获取
CFRunLoopRef
。 - 缓存中不存在通过线程去获取。
_CFRunLoopGet0
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//没有传 pthread_t,则默认为主线程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
//创建可变字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//通过 主线程 创建 mainLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 进行绑定,存储 thread(key) - runloop(value): dict[@"pthread_main_thread_np"] = mainLoop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//通过 thread 获取 runloop(非main runloop)
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {//runloop不存在
//创建runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//存储 runloop
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
与线程是一一对应的,每个runloop
对应一个线程。线程并不一定有runloop
,在有的情况下是一一对应的。 -
runloop
底层是存储在可变字典中的,key
为线程,value
为runloop
。 -
runloop
是CFRunLoopRef
类型,通过__CFRunLoopCreate
创建。
3.3.2 CFRunLoopRef
image.png可以看到在创建
CFRunLoopRef
的时候有对应的modes
、items
、_pthread
。并且创建完成后调用__CFRunLoopFindMode
去找mode
。
CFRunLoopRef
的定义如下:
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
实际上底层它是__CFRunLoop
类型:
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;//线程
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;//集合类型 Items
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;//集合类型 Modes
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
- 一个
runloop
对应多个modes
以及items
。
对于timer
而言:
[[NSRunLoop currentRunLoop] addTimer:<#(nonnull NSTimer *)#> forMode:<#(nonnull NSRunLoopMode)#>];
显然它是要依赖mode
的。
CFRunLoopMode
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;//set source0
CFMutableSetRef _sources1;//set source1
CFMutableArrayRef _observers;//array observe
CFMutableArrayRef _timers;//array times
CFMutableDictionaryRef _portToV1SourceMap;//dic port
__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 */
};
而一个mode
下又对应多个items(source0、source1、timers、observers)
,所以就有如下关系:
1
个runloop
对应1
个线程。1
个runloop
对应多个mode
。1
个mode
对应多个source
、timer
、observer
。
3.3.3 RunLoop Modes
既然有多种mode
,那么都有哪些呢?
源码中有如下定义:
CF_EXPORT const CFRunLoopMode kCFRunLoopDefaultMode;
CF_EXPORT const CFRunLoopMode kCFRunLoopCommonModes;
它们对应Foundation
中的:
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;
FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes ;
我们都清楚在页面滚动的时候有一个UITrackingRunLoopMode
:
UIKIT_EXTERN NSRunLoopMode const UITrackingRunLoopMode;
除了以上3
种mode
还有两个私有mode
:
UIInitializationRunLoopMode
GSEventReceiveRunLoopMode
当RunLoop
运行在Mode1
上时,是无法接受处理Mode2
或Mode3
上的Source、Timer、Observer
事件的。
-
kCFRunLoopDefaultMode/NSDefaultRunLoopMode
:默认模式,主线程是在这个运行模式下运行。 -
kCFRunLoopCommonModes/NSRunLoopCommonModes
:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer
到多个Mode
中。 -
UITrackingRunLoopMode
:跟踪用户交互事件(用于ScrollView
追踪触摸滑动,保证界面滑动时不受其他Mode
影响)。对于macOS
对应NSEventTrackingRunLoopMode
-
UIInitializationRunLoopMode
:在刚启动App
时第进入的第一个Mode
,启动完成后就不再使用。 -
GSEventReceiveRunLoopMode
:接受系统内部事件,通常用不到。
四、runloop 事务处理
4.1 timer 事务处理
以timer
为例,将timer
加入到runloop
中:
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timerWithTimeInterval block -- %@",[[NSRunLoop currentRunLoop] currentMode]);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
底层调用了CFRunLoopAddTimer
:
4.1.1 CFRunLoopAddTimer
//runloop timer mode
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
__CFRunLoopLock(rl);
//kCFRunLoopCommonModes 集合mode
if (modeName == kCFRunLoopCommonModes) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
//将 timer 加入 modeItems 中。
CFSetAddValue(rl->_commonModeItems, rlt);
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt};
/* add new item to all common-modes */
//加入所有的command modes 中。
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {//非common mode
//从 runloop 中找到对应的 mode
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm) {//mode存在
if (NULL == rlm->_timers) {//不存在timer
CFArrayCallBacks cb = kCFTypeArrayCallBacks;
cb.equal = NULL;
//创建timer
rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
}
}
if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
__CFRunLoopTimerLock(rlt);
//timer没有runloop
if (NULL == rlt->_runLoop) {
//设置runloop
rlt->_runLoop = rl;
} else if (rl != rlt->_runLoop) {//timer所有的runloop与当前runloop不同直接返回。
__CFRunLoopTimerUnlock(rlt);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
return;
}
//将timer 加入 mode中。
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...
//唤醒runloop
if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
}
根据要加入的mode
区分是common mode
和非common mode
将timer
加入mode
中。这个时候只是将timer
加入了mode
中,要执行肯定要调用CFRunLoopRun
,最终要调用CFRunLoopRunSpecific
。
4.1.2 CFRunLoopRunSpecific
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
//对应声明
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
-
CFRunLoopGetCurrent()
创建runloop
。 -
mode
默认给的是kCFRunLoopDefaultMode
。 -
1.0e10
(1 * 1010)表示超时时间。 -
returnAfterSourceHandled
表示source
处理后是否返回。
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//根据 modeName 找到本次运行的 mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
//如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
//取上一次运行的 mode
CFRunLoopModeRef previousMode = rl->_currentMode;
//如果本次mode和上次的mode一致
rl->_currentMode = currentMode;
//初始化一个result为kCFRunLoopRunFinished
int32_t result = kCFRunLoopRunFinished;
if (currentMode->_observerMask & kCFRunLoopEntry )
// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//run
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
if (currentMode->_observerMask & kCFRunLoopExit )
// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
- 根据
modeName
找到本次运行的mode
。 - 1.通知
Observers
RunLoop
即将进入loop
。 - 调用
__CFRunLoopRun
运行循环。 - 10.通知
Observers
RunLoop
即将退出。
4.1.3 __CFRunLoopRun
在__CFRunLoopRun
中调用了__CFRunLoopDoTimers
:
// rl and rlm are locked on entry and exit
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) { /* DOES CALLOUT */
Boolean timerHandled = false;
CFMutableArrayRef timers = NULL;
//从mode中获取timers进行处理
for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx);
if (__CFIsValid(rlt) && !__CFRunLoopTimerIsFiring(rlt)) {
if (rlt->_fireTSR <= limitTSR) {
if (!timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
CFArrayAppendValue(timers, rlt);
}
}
}
for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
//执行timer
Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
timerHandled = timerHandled || did;
}
if (timers) CFRelease(timers);
return timerHandled;
}
找到mode
中的所有timer
然后调用__CFRunLoopDoTimer
。
4.1.4 __CFRunLoopDoTimer
image.png在
__CFRunLoopDoTimer
中进行了时间的判断以及timer
回调的调用并且重新计算了_fireTSR
。这样整个调用流程就与回调堆栈吻合了。
CFRunLoopAddTimer -> CFRunLoopRunSpecific -> __CFRunLoopRun -> __CFRunLoopDoTimers -> __CFRunLoopDoTimer -> __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
。
4.2 source 事务处理
与timer
相同source
会调用CFRunLoopAddSource
:
将
source
加入mode
中。同样调用是在__CFRunLoopRun
中。
4.2.1 __CFRunLoopDoSources0
image.png在
__CFRunLoopDoSources0
中调用了__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
。
4.2.2 __CFRunLoopDoSources1
image.png__CFRunLoopDoSources1
最终调用了__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
CFRunLoopAddSource -> CFRunLoopRunSpecific -> __CFRunLoopRun -> __CFRunLoopDoSources0/__CFRunLoopDoSources1 -> __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ /__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
4.3 observer 事务处理
同理observer
会调用CFRunLoopAddObserver
。
4.3.1 CFRunLoopAddObserver
image.png将
observer
加入_observers
或者_commonModeItems
中。同样调用是在__CFRunLoopRun
中。
4.3.2 __CFRunLoopDoObservers
image.png__CFRunLoopDoObservers
最终调用__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
传递了状态参数。
CFRunLoopAddObserver -> CFRunLoopRunSpecific -> __CFRunLoopRun -> __CFRunLoopDoObservers(状态参数) -> __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(状态参数)
4.4 回调函数
4.4.1 CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
if (func) {
func(timer, info);
}
asm __volatile__(""); // thwart tail-call optimization
}
timer
直接调用回调函数,传递timer
参数。
4.4.2 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
if (perform) {
perform(info);
}
asm __volatile__(""); // thwart tail-call optimization
}
source0
直接调用perform
。
4.4.3 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
#else
void (*perform)(void *),
#endif
void *info) {
if (perform) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
*reply = perform(msg, size, kCFAllocatorSystemDefault, info);
#else
perform(info);
#endif
}
asm __volatile__(""); // thwart tail-call optimization
}
source1
不同架构处理不同,iOS
上与source0
处理相同。
4.4.4 CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (func) {
func(observer, activity, info);
}
asm __volatile__(""); // thwart tail-call optimization
}
observer
调用回调函数其中传递了activity
状态参数。
4.4.5 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() __attribute__((noinline));
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg) {
_dispatch_main_queue_callback_4CF(msg);
asm __volatile__(""); // thwart tail-call optimization
}
直接调用_dispatch_main_queue_callback_4CF
。
4.5.6 source1 与 source0
点击触摸事件会先调用source1
:
然后交给source0
处理:
之后才会进入触摸回调。
系统先通过source1
唤醒runloop
然后交给source0
处理事件。
五、runloop 原理
在CFRunLoopRun
的过程中do...while
的条件是根据返回的状态判断的:
enum {
kCFRunLoopRunFinished = 1,//完成
kCFRunLoopRunStopped = 2,//结束
kCFRunLoopRunTimedOut = 3,//超时
kCFRunLoopRunHandledSource = 4//处理完source
};
在CFRunLoopRunSpecific
的过程中也有状态的切换:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//启动
kCFRunLoopBeforeTimers = (1UL << 1),//将要处理 timer 事件
kCFRunLoopBeforeSources = (1UL << 2),//将要处理 Source 事件
kCFRunLoopBeforeWaiting = (1UL << 5),//将要进入休眠状态,即将由用户态切换到内核态
kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒,即从内核态切换到用户态后
kCFRunLoopExit = (1UL << 7),//退出
kCFRunLoopAllActivities = 0x0FFFFFFFU //监听所有状态
};
可以通过CFRunLoopActivity
监听整个runloop
的生命周期。
runloop
的整个核心逻辑就在__CFRunLoopRun
中:
整个流程伪代码如下:
//获取 mode 处理
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
/// 1.通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
/// 10.通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
//核心函数
/**
* 运行run loop
*
* @param rl 运行的RunLoop对象
* @param rlm 运行的mode
* @param seconds run loop超时时间
* @param stopAfterHandle true:run loop处理完事件就退出 false:一直运行直到超时或者被手动终止
* @param previousMode 上一次运行的mode
*
* @return 返回4种状态
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
//一些超时逻辑相关的处理
int32_t retVal = 0;
do { //itmes do
/// 2. 通知 Observers: 即将处理timer事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
/// 3.通知 Observers: 即将处理Source0(非port)事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
/// 执行被加入的 block
__CFRunLoopDoBlocks(rl, rlm);
/// 4.处理sources0 (非port)事件
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
/// 处理sources0返回为YES
if (sourceHandledThisLoop) {
/// 执行被加入的block
__CFRunLoopDoBlocks(rl, rlm);
}
/// 5.如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息(9)。
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
/// 处理消息
goto handle_msg;
}
/// 6.通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 设置RunLoop为休眠状态。
__CFRunLoopSetSleeping(rl);
// 内循环,用于接收等待端口的消息
// 进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
do {
/// 7.等待被唤醒, 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// 7.1 一个基于 port 的 Source1 的事件。
/// 7.2 一个 Timer 到时间了
/// 7.3 RunLoop 自身的超时时间到了
/// 7.4 被其他什么调用者手动唤醒
// mach 事务 - 指令
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while(1)
// 取消runloop的休眠状态,也就是唤醒。
__CFRunLoopUnsetSleeping(rl);
/// 8.通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
/// 9.处理唤醒时收到的消息,之后跳转 步骤2
handle_msg:
//9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (被Timer唤醒) {
//处理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被GCD唤醒) {
//9.2 如果有dispatch到main_queue的block,执行block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else if (被Source1唤醒) {
//9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件。(被source1唤醒)
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
//进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
//超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
//被外部调用者强制停止了
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
//自动停止了
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
//source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
根据源码有以下流程图:
runloop循环流程
同时官方文档中也给出了流程总结:
image.png
5.1 runloop 休眠与唤醒
在第7
步前后分别调用了__CFRunLoopSetSleeping
与__CFRunLoopUnsetSleeping
进行休眠与唤醒操作:
这里是通过Darwin
中的Mach
来进行内核态和用户态的切换:
通过mach_msg()
函数接收、发送消息,本质上是调用mach_msg_trap()
。在用户态调用 mach_msg_trap()
时会切换到内核态,内核态中内核实现的mach_msg()
函数会完成实际的工作。
比如触摸屏幕摸到硬件(屏幕)将事件先包装成Event
告诉source1(port)
,source1
唤醒RunLoop
然后将事件Event
分发给source0
由source0
来处理。
5.2 mode 切换
默认情况下runloop
运行后是在kCFRunLoopDefaultMode
模式下的,那么runloop
是如何切换mode
的呢?
既然要切换mode
那么肯定要改变CFRunLoopRunSpecific
的参数,搜索CFRunLoopRunSpecific
后有在CFRunLoopRunInMode
中有调用:
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
而CFRunLoopRunInMode
并没有找到调用的时机,给CFRunLoopRunInMode
下符号断点:
可以看到是在-[NSRunLoop(NSRunLoop) runMode:beforeDate:]
中以及GSEventRunModal
中进行切换的。
六、runloop 应用场景
6.1 runloop 与 mode
6.1.1 mode 与 timer
常用的一个场景是在TableView
中的定时器在滚动的时候回调是不执行的。因为这个时候runloop
的mode
从NSDefaultRunLoopMode
切换到了UITrackingRunLoopMode
。而timer
默认情况下是加在NSDefaultRunLoopMode
下的。
这个时候就需要将timer
同时加到NSDefaultRunLoopMode
与UITrackingRunLoopMode
下:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
加入NSRunLoopCommonModes
下就可以了。当然也可以使用GCD
的timer
实现计时器。
6.1.2 mode 与 页面刷新
怎样保证子线程数据回来更新UI
的时候不打断用户的滑动操作?
不打断用户操作那么当runloop
在NSDefaultRunLoopMode
模式的时候页面就不在滑动状态。那么就当主线程RunLoop
由UITrackingRunLoopMode
切换到NSDefaultRunLoopMode
时再去更新UI
:
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
6.2 runloop 与 线程
RunLoop
与线程时是一一对应的,数据以key(线程)-value(runloop)
存储在全局的字典中。默认情况下线程时不开启runloop
的(主线程除外)。
6.2.1 线程 与 timer
有如下案例(在主线程调用):
- (void)testRunloop {
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:10];
NSLog(@"3");
});
NSLog(@"4");
}
- (void)performSelectorAction {
NSLog(@"5");
}
输出:
1
4
2
3
由于performSelector
带了延迟函数(即使延迟时间为0
),内部创建了timer
,而子线程没有开启runloop
添加timer
会失败,该方法也就失效了。
当然如果是主队列则没有问题,同步函数则结果依赖调用同步函数的线程。
修改代码如下:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[[NSRunLoop currentRunLoop] run];
[self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:10];
NSLog(@"3");
});
这个时候performSelectorAction
仍然不执行,run
后runloop
中没有事务导致runloop
退出了。退出后再添加timer
显然不会执行。
那么将run
的逻辑在添加timer
后就可以了:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
[self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:10];
[[NSRunLoop currentRunLoop] run];
NSLog(@"3");
});
这个时候输出:
1
4
2
5
3
这个时候即使delay
传0
也没问题,进一步说明了NStimer
是不准的。
那么有个疑问,既然performSelector: withObject : afterDelay:
底层是对timer
的封装,那么肯定会调用CFRunLoopAddTimer
。而runloop
不存在的情况下这个函数会直接返回:
按照理解应该先创建
runloop
然后添加timer
再启动:
[NSRunLoop currentRunLoop];
[self performSelector:@selector(performSelectorAction) withObject:nil afterDelay:0];
[[NSRunLoop currentRunLoop] run];
为什么先添加timer
也没问题呢?
performSelector: withObject : afterDelay:
内部会先创建runloop
然后添加timer
,我们只需要在添加timer
后启动runloop
。- 自己启动
RunLoop
,一定要在添加item
后。
6.2.2 线程常驻
线程保活在实际开发中经常会遇到一些耗时且需要频繁处理的工作,这些工作和UI
无关,比如大文件的下载、后台进行数据的上报等。线程常驻的好处是不用频繁的开辟销毁线程节省资源。
6.2.2.1 线程释放验证
创建一个HPThread
继承自NSThread
,只重写dealloc
方便验证线程是否销毁:
- (void)dealloc {
NSLog(@"%s",__func__);
}
调用如下:
HPThread *thread = [[HPThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
[thread start];
- (void)threadAction {
@autoreleasepool {
for (int i = 0; i < 100; i++) {
NSLog(@"子线程任务 %d - %@",i,[NSThread currentThread]);
}
NSLog(@"子线程任务结束- %@",[NSThread currentThread]);
}
}
这个时候在threadAction
中任务执行完毕后HPThread
就释放了:
这个时候如果HPThread
改为属性被持有:
@property (nonatomic, strong) HPThread *thread;
HPThread
的dealloc
就不会执行了,那么这个时候线程释放了么?
创建一个新任务在self. thread
中执行:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
[self.view addGestureRecognizer:tap];
- (void)tapAction:(UITapGestureRecognizer *)tap {
//waitUntilDone:YES 执行完 otherChildThreadAction 才执行后续的逻辑,为 NO 就一起直接执行了。
[self performSelector:@selector(otherChildThreadAction) onThread:self.thread withObject:nil waitUntilDone:NO];
NSLog(@"单点事件执行完毕");
}
- (void)otherChildThreadAction {
@autoreleasepool {
for (int i = 0; i < 10; i++) {
NSLog(@"otherChildThreadAction 子线程任务 %d - %@",i,[NSThread currentThread]);
}
NSLog(@"otherChildThreadAction 子线程任务结束- %@",[NSThread currentThread]);
}
}
这个时候otherChildThreadAction
并没有执行:
说明
HPThread
创建的线程已经释放了。那么说明仅仅持有HPThread
并不能保证线程存活。
6.2.2.2 线程保活
image.png显然pthread_create
创建的线程在任务执行完毕后就被释放了,要确保线程执行完后不被释放,那么就要持有它。那么就有两种方式:
- 1.使用
pthread
代替HPThread
实现线程创建逻辑。 - 2.使用
runloop
持有thread
。为了让thread
不释放,runloop
要一直有事务。
使用 pthread
比较麻烦更好的方案是使用runloop
的方案。
修改threadAction
如下:
- (void)threadAction {
@autoreleasepool {
for (int i = 0; i < 100; i++) {
NSLog(@"子线程任务 %d - %@",i,[NSThread currentThread]);
}
// RunLoop 任务执行完毕后线程就销毁了,线程保活需要加入 runloop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//为了 runloop 不退出
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
//end 不会执行,因为跑do...while 循环了。
NSLog(@"子线程任务结束- %@",[NSThread currentThread]);
}
}
这样就保证了线程不被释放:
image.png
但是这个时候又存在一个问题了。self -> thread -> self
造成了循环引用。需要在结束任务的时候退出线程打破循环引用:
@property (nonatomic, assign) BOOL stopped;
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTapAction:)];
doubleTap.numberOfTapsRequired = 2;
[self.view addGestureRecognizer:doubleTap];
- (void)doubleTapAction:(UITapGestureRecognizer *)tap {
[self performSelector:@selector(exitThread) onThread:self.thread withObject:nil waitUntilDone:NO];
NSLog(@"双击事件执行完毕");
}
- (void)exitThread {
self.stopped = YES;
//停止RunLoop,这样只会停止当次的。
CFRunLoopStop(CFRunLoopGetCurrent());
[self.thread cancel];
//打破循环引用
self.thread = nil;
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
thread
也可以使用- (instancetype)initWithBlock:(void (^)(void))block
来创建。虽然没有循环引用了,vc
也能释放,但是runloop
持有了thread
导致线程和runloop
不能释放。
这个时候双击然后返回页面仍然不能释放VC
,由于runloop
是通过run
开启的(runUntilDate
)也一样,run
一旦成功会不停的调用runMode:beforeDate:
来运行runloop
,而于CFRunLoopStop
只停止了一次runloop
(runloop
仍然持有了线程)。修改threadAction
如下:
- (void)threadAction {
@autoreleasepool {
for (int i = 0; i < 100; i++) {
NSLog(@"子线程任务 %d - %@",i,[NSThread currentThread]);
}
// RunLoop 任务执行完毕后线程就销毁了,线程保活需要加入 runloop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//为了 runloop 不退出
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.stopped) {
// 这个方法在没有任务时就睡眠 任务完成了就会退出loop
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
//不会执行,因为跑do...while 循环了。 self.stopped
NSLog(@"子线程任务结束- %@",[NSThread currentThread]);
}
}
这个时候通过self.stopped
变量控制是否继续run
就解决问题了。
image.png
runMode: beforeDate:
只控制执行一次:
但是当我们将runMode: beforeDate:
的mode
修改为NSRunLoopCommonModes
后:
给
thread
添加任务也不执行,并且cup
占满。
等价于UITrackingRunLoopMode
:
所以在while
循环中不能一直run
在UITrackingRunLoopMode
模式。
当然也可以使用CFRunLoop
相关函数实现:
// 创建上下文
CFRunLoopSourceContext context = {0};
// 创建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// Runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 销毁source
CFRelease(source);
// 启动
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
那么CFRunLoop
尝试使用common modes
:
直接报错
CFRunLoopRunSpecific
模式只能传递特定模式。这也就是runMode: beforeDate:
启动后给thread
添加任务也不执行的原因。
CFRunLoopRunInMode
与runMode: beforeDate:
只能运行在特定的模式下。CFRunLoopStop
只能退出单次的runloop
。
总结:
-
runloop
是一个事件循环,分为内核态和用户态。底层是对do...while
的封装。与do...while
的区别是它有休眠和唤醒逻辑,从而节省cpu
资源、提高程序的性能。 -
NSRunloop
是对CFRunloop
的封装。 -
runloop
与线程一一对应,开启runloop
依赖于线程,线程不一定开启runloop
(主线程默认开启,子线程需要手动开启)。底层是存储在可变字典中key
为线程,value
为runloop
。 -
runloop
与mode
是一对多的关系。-
kCFRunLoopDefaultMode/NSDefaultRunLoopMode
:默认模式。 -
UITrackingRunLoopMode
:跟踪用户交互事件。 -
kCFRunLoopCommonModes/NSRunLoopCommonModes
:伪模式,不是一种真正的运行模式,本质上是同步Source/Timer/Observer
到多个Mode
中。
-
-
mode
与source
、timer
、observer
也是一对多的关系。-
CFRunLoopSource
分为source0
与source1
。-
source0
:基于非port
也就是用户触发的事件,需要手动唤醒RunLoop
,将当前线程从内核态切换到用户态。 -
source1
:基于port
,包含一个mach_port
和一个回调。可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop
,接收分发系统事件。具备唤醒线程的能力。
-
-
CFRunLoopTimer
基于时间的触发器,在预设的时间点唤醒RunLoop
执行回调。基于runloop
所以不是实时的(RunLoop
只负责分发消息,如果线程当前正在处理繁重的任务,有可能导致Timer
本次延时或者少执行一次)。 -
CFRunLoopObserver
可以监听runloop
的状态。
-
网友评论