Runloop 是和线程紧密相关的一个基础组件,是很多线程有关功能的幕后功臣。 本文将从以下几个方面来总结runloop:
- 什么是runloop
- runloop的作用
- runloop和线程的关系
- runloop详细介绍及源码分析
- runloop原理分析
- runloop应用
什么是runloop
runloop 苹果官方文档地址
- Runloop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的do..while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待。
runloop
的run
方法源码如下所示,是一个do..while循环
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 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
-
runloop
实际上是一个对象,这个对象提供了一个入口函数。
runloop的作用
- 保持程序的持续运行,循环避免线程销毁
- 处理APP的各种事件(触摸、定时器、performSelector)
- 节省cpu资源、提供程序的性能(该做事就做事,该休息就休息)
runloop在系统里的使用
在iOS系统里,下面的这些都有使用runloop,通过断点查看堆栈可以看到调用的方法名:
- block应用: CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
- 调用timer: CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
- 响应source0: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
- 响应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
断点查看runloop信息
在timer
的block
里添加断点,然后左边箭头指示的按钮不选中(默认是选中的),可以看到runloop的调用信息__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);
}
getpid(); // thwart tail-call optimization
}
关于上面总结的其他几种调用的runloop方法名,都可以用上面的这种调试方式查看一下。
runloop和线程的关系
- runloop和线程是一一对应的
- runloop在首次被线程获取时创建,在线程结束时被销毁
- 主线程默认启动runloop,子线程手动启动(程序启动时,启动主线程runloop,
[[NSRunLoop currentRunLoop] run]
)
图中展现了 Runloop 在线程中的作用:从 input source
和 timer source
接受事件,然后在线程中处理事件。
获取runloop
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
源码里调用了_CFRunLoopGet0()
,这里是传一个主线程pthread_main_thread_np()
进去,如下定义了它是主线程
#if DEPLOYMENT_TARGET_WINDOWS || DEPLOYMENT_TARGET_IPHONESIMULATOR
CF_EXPORT pthread_t _CF_pthread_main_thread_np(void);
#define pthread_main_thread_np() _CF_pthread_main_thread_np()
还有一个获取当前线程runloop的方法:同样是调用了_CFRunLoopGet0
,只不过传进去的是当前线程pthread_self()
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
接下来看获取线程runloop的函数_CFRunLoopGet0
(包括主线程和子线程)的源码
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
//根据线程获取runloop
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
//如果存储RunLoop的字典不存在
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//创建主线程的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);
__CFSpinLock(&loopsLock);
}
//字典里找runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFSpinUnlock(&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);
}
}
- 如果当前存储的字典容器不存在,首先就创建了一个容器
CFMutableDictionaryRef
可变字典 - 第二步使用主线程创建了一个主线程runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
- 第三步
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
把主线程和它的runloop用key-value形式保存在这个CFMutableDictionaryRef
字典容器里 - 以上说明,第一次进来的时候,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建
- 再看到
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
,可以用线程
把保存在字典里的runloop
取出来 - 如果字典里没有找到
runloop
,就根据当前的子线程创建一个新的runloop
对象并保存到字典里 - 最后一步
if (pthread_equal(t, pthread_self())) {...}
判断当前的线程是不是传递进来的线程,如果是则创建一个回调,如果线程销毁,就销毁当前的runloop - 这里验证了上面的结论1和2: runloop和线程是一一对应的(字典保存)。 runloop在首次被线程获取时创建(并且: 不管获取的是主线程runloop还是子线程runloop,总是会创建主线程的runloop),在线程结束时被销毁(通过回调销毁)
runloop代码验证
在AppDelegate
打断点,可以看到主线程是有调用__CFRunloopRun
方法的,所以证明了上面的结论三: 主线程是默认开启runloop
的 [图片上传失败...(image-ad314f-1571322546197)]) 测试runloop
代码如下
- (vod)viewDidLoad {
super viewDidLoad];
DLThread *thread = [[DLThread alloc]initWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
[NSTimer scheduledTimerWithTimeInterval:1repeats:YES block:^(NSTimer * _Nonnul) {
NSLog(@"timer");
}];
}];
thread.name = @"Test";
[thread start];
DLThread.m
里只写了如下代码
-(void)dealloc{
NSLog(@"线程销毁了");
}
运行上面的代码,发现timer
并没有打印,说明子线程里开启timer
没成功,然后添加了代码运行当前线程的runloop,如下所示:
DLThread *thread = [[DLThread alloc] initWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
}];
[[NSRunLoop currentRunLoop] run];
}];
thread.name = @"Test";
[thread start];
发现timer
一直在打印了,这里证明了两个结论: timer
的运行是和runloop
有关的,子线程的runloop
是需要手动开启的
那么如何停止timer
呢?新增了一个标记值isStopping
用来退出线程
DLThread *thread = [[DLThread alloc] initWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
if(self.isStopping){
[NSThread exit];
}
}];
[[NSRunLoop currentRunLoop] run];
}];
thread.name = @"Test";
[thread start];
运行发现,在线程销毁后,timer
也停止了,这里侧面证明了上面的结论二: runloop
是在线程结束时销毁的
runloop源码分析
点击下载runloop源码:密码 3kww
在runloop源码里需要探索的:
- CFRunLoop
- CFRunLoopMode
- CFRunLoopSource
- CFRunLoopObserver
- CFRunLoopTimer
CFRunLoop
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; //doblocks的时候用到
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
可以看到,其实runloop就是一个结构体对象,里面包含了一个线程,一个当前正在运行的mode, N个mode,N个commonMode。
- runloop和线程一一对应
- runloop包含多个mode, mode包含多个 mode item(sources,timers,observers)
-
runloop一次只能运行在一个model下:
- 切换mode:停止loop -> 设置mode -> 重启runloop
- runloop通过切换mode来筛选要处理的事件,让其互不影响
- iOS运行流畅的关键
CFRunLoopMode
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
CFMutableSetRef _sources1; //sources1
CFMutableArrayRef _observers; //通知
CFMutableArrayRef _timers; //定时器
CFMutableDictionaryRef _portToV1SourceMap; //字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet; //保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
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,N个source0、N个source1、timer、observer和port,可见事件都是由Mode
在管理,而RunLoop
管理Mode
。
它们之间的关系如下图:
mode
是允许定制的,不过至少要包含一个mode item
(source/timer/observer)。 同一个mode item
可以被多个mode持有
苹果公开的三种 RunLoop Mode:
- NSDefaultRunLoopMode(kCFRunloopDefaultMode):默认状态,app通常在这个mode下运行
- UITrackingRunLoopMode:界面跟踪mode(例如滑动scrollview时不被其他mode影响)
- NSRunLoopCommonModes(kCFRunLoopCommonModes):是前两个mode的集合,可以把自定义mode用CFRunLoopAddCommonMode函数加入到集合中
还有两种mode,只需做了解即可:
- GSEventReceiveRunLoopMode:接收系统内部mode,通常用不到
- UIInitializationRunLoopMode:私有,只在app启动时使用,使用完就不在集合中了
CFRunLoopSource
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; ////用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
CFRunloopSourceRef
是runloop的数据源抽象类对象(protocol),由源码可以看到共用体(union:在相同的内存位置存储不同的数据类型),可见Source分为两类:
Source0
typedef struct {
CFIndex version;
void * 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);
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
source0: 处理App内部事件、APP自己负责管理(触发)例如:UIEvent CFSocket。 打断点基本都会看到它。
-
source0
是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。 -
CFRunLoopSourceSignal
(source)将这个事件标记为待处理 -
CFRunLoopWakeUp
来唤醒runloop,让他处理事件
自定义source实现步骤:
- 创建一个底层source0 源
CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
- 把我们的创建的source0添加到runloop
CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode)
- 执行信号,标记待处理
CFRunLoopSourceSignal
- 唤醒runloop去处理
CFRunLoopWakeUp
- 取消移除源
CFRunLoopRemoveSource
- 释放runloop
CFRelease(rlp)
Source1
typedef struct {
CFIndex version;
void * 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);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
source1:
- 由runloop和 Mach port管理,Mach port驱动,包含一个 mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。
- 它能够主动唤醒RunLoop(由操作系统内核进行管理,例如: CFMachPort,CFMessagePort)
- 还允许实现自己的Source,但一般不会这么做
CFRunLoopObserver
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
它是一个观察者,能够监听Runloop的状态改变,可以向外部报告runloop状态的更改,框架中很多机制都由它触发(如CAAnimation)
在CFRunloop.h
文件里可以看到observer监听的状态如下:
/* Run Loop Observer Activities */
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
};
正好和下图runloop流程里的observer所对应:
CFRunLoopTimer
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
-
CFRunLoopTimer
是定时器,可以在设定的时间点抛出回调 -
CFRunLoopTimer
和NSTimer
是toll-free bridged的,可以相互转换 -
CFRunLoopTimer
的封装有三种: NSTimer,performSelector和CADisplayLink
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument
afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
简单总结了这三种timer,如下图:
runloop原理分析
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
CFRunLoopRun
和CFRunLoopRunInMode
都调用了CFRunLoopRunSpecific
函数
CFRunLoopRunSpecific
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);
/// 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
/// 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
上面的源码是简化代码后的源码,实际源码复杂一些,根据源码可得出如下结论:
- 在进入run loop之前通知observer,状态为kCFRunLoopEntry
- 在退出run loop之后通知observer,状态为kCFRunLoopExit
- 进入runloop的时候调用了
__CFRunLoopRun
函数
__CFRunLoopRun(核心重点)
/// 核心函数
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
/// 通知 Observers: 即将处理timer事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
/// 通知 Observers: 即将处理Source事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
/// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
/// 处理sources0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
/// 处理sources0返回为YES
if (sourceHandledThisLoop) {
/// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
/// 判断有无端口消息(Source1)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
/// 处理消息
goto handle_msg;
}
/// 通知 Observers: 即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
/// 等待被唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
/// 通知 Observers: 被唤醒,结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被Timer唤醒) {
/// 处理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被GCD唤醒) {
/// 处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else if (被Source1唤醒) {
/// 被Source1唤醒,处理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
/// 处理block
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
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)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
以上是runloop
核心函数的简写源码(比较清晰易懂)点击下载runloop源码:密码 3kww 还有一个监听唤醒端口消息的函数__CFRunLoopServiceMachPort
比较重要,系统内核将这个线程挂起,停留在mach_msg_trap状态,等待接受 mach_port(用于唤醒的端口) 的消息。线程将进入休眠, 直到被其他线程或另一个进程的某个线程向该端口发送mach_msg消息唤醒
__CFRunLoopServiceMachPort
/**
* 接收指定内核端口的消息
*
* @param port 接收消息的端口
* @param buffer 消息缓冲区
* @param buffer_size 消息缓冲区大小
* @param livePort 暂且理解为活动的端口,接收消息成功时候值为msg->msgh_local_port,超时时为MACH_PORT_NULL
* @param timeout 超时时间,单位是ms,如果超时,则RunLoop进入休眠状态
*
* @return 接收消息成功时返回true 其他情况返回false
*/
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
for (;;) { /* In that sleep of death what nightmares may come ... */
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
msg->msgh_bits = 0; //消息头的标志位
msg->msgh_local_port = port; //源(发出的消息)或者目标(接收的消息)
msg->msgh_remote_port = MACH_PORT_NULL; //目标(发出的消息)或者源(接收的消息)
msg->msgh_size = buffer_size;//消息缓冲区大小,单位是字节
msg->msgh_id = 0;
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
//通过mach_msg发送或者接收的消息都是指针,
//如果直接发送或者接收消息体,会频繁进行内存复制,损耗性能
//所以XNU使用了单一内核的方式来解决该问题,所有内核组件都共享同一个地址空间,因此传递消息时候只需要传递消息的指针
ret = mach_msg(msg, MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
CFRUNLOOP_WAKEUP(ret);
//接收/发送消息成功,给livePort赋值为msgh_local_port
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
//MACH_RCV_TIMEOUT
//超出timeout时间没有收到消息,返回MACH_RCV_TIMED_OUT
//此时释放缓冲区,把livePort赋值为MACH_PORT_NULL
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
//MACH_RCV_LARGE
//如果接收缓冲区太小,则将过大的消息放在队列中,并且出错返回MACH_RCV_TOO_LARGE,
//这种情况下,只返回消息头,调用者可以分配更多的内存
if (MACH_RCV_TOO_LARGE != ret) break;
//此处给buffer分配更大内存
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
runloop应用
事件响应
- 当一个硬件事件(触摸/锁屏/摇晃/加速)发生后,首先有
IOKit.framework
生成一个IOHIDEvent
事件并由SpringBoard
接受,之后由mach port
转发给需要的App进程。 - 苹果注册了一个 Source1 来接受系统事件,通过回调函数触发Source0(所以Event实际上是基于Source0)的,调用
_UIApplicationHandleEventQueue()
进行应用内部的分发。_UIApplicationHandleEventQueue()
会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。
手势识别
- 当上面的
_UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用 Cancel 将当前的touchesBegin/Move/End
系列回调打断。随后系统将对应的UIGestureRecognizer
标记为待处理。 - 苹果注册了一个 Observer 监测
BeforeWaiting
(Loop即将进入休眠) 事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的GestureRecognizer
,并执行GestureRecognizer
的回调。 - 当有
UIGestureRecognizer
的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面刷新
- 当UI发生改变时(Frame变化,UIView/CALayer的结构变化)时,或手动调用了
UIView/CALayer的setNeedsLayout/setNeedsDisplay
方法后,这个UIView/CALayer就被标记为待处理。 - 苹果注册了一个用来监听
BeforeWaiting
和Exit
的Observer,在他的回调函数里会遍历所有待处理的UIView/CALayer
来执行实际的绘制和调整,并更新UI界面。
AutoreleasePool
- 主线程Runloop注册了两个Observers,其回调都是
_wrapRunloopWithAutoreleasePoolHandler
-
Observers1
监听Entry
事件: 优先级最高,确保在所有的回调前创建释放池,回调内调用_objc_autoreleasePoolPush()
创建自动释放池 -
Observers2
监听BeforeWaiting
和Exit
事件: 优先级最低,保证在所有回调后释放释放池。BeforeWaiting
事件:调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧池并创建新池,Exit
事件: 调用_objc_autoreleasePoolPop()
,释放自动释放池
tableView延迟加载图片,保证流畅
给ImageView
加载图片的方法用PerformSelector
设置当前线程的RunLoop的运行模式kCFRunLoopDefaultMode
,这样滑动时候就不会执行加载图片的方法 [self.imgView performSelector:@selector(setImage:) withObject:cellImg afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
Timer不被ScrollView的滑动影响
-
+timerWihtTimerInterval...
创建timer -
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]
把timer加到当前runloop,使用占位模式 -
runloop run/runUntilData
手动开启子线程runloop - 使用GCD创建定时器,GCD创建的定时器不会受RunLoop的影响
// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
// 比当前时间晚1秒开始执行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒执行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
});
// 启动定时器
dispatch_resume(self.timer);
GCD
-
dispatch_async(dispatch_get_main_queue)
使用到了RunLoop -
libDispatch
向主线程的Runloop
发送消息将其唤醒,并从消息中取得block,并在回调__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里执行这个block
NSURLConnection
- 使用
NSURLConnection
时,你会传入一个 Delegate,当调用了[connection start]
后,这个Delegate
就会不停收到事件回调。 -
start
这个函数的内部会会获取CurrentRunLoop
,然后在其中的DefaultMode
添加了4个Source0
(即需要手动触发的Source)。CFMultiplexerSource
是负责各种 Delegate 回调的,CFHTTPCookieStorage
是处理各种 Cookie 的。 - 当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:
com.apple.NSURLConnectionLoader
和com.apple.CFSocket.private
。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader
这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
AFNetworking
- 使用runloop开启常驻线程
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
- 给 runloop 添加
[NSMachPort port](source1)
使runloop不退出,实际并没有给这个port发消息
AsyncDisplayKit
仿照 QuartzCore/UIKit
框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting
和 kCFRunLoopExit
事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
卡顿检测
- dispatch_semaphore_t 是一个信号量机制,信号量到达、或者 超时会继续向下进行,否则等待,如果超时则返回的结果必定不为0,信号量到达结果为0。GCD信号量-dispatch_semaphore_t
- 通过监听mainRunloop的状态和信号量阻塞线程的特点来检测卡顿,通过
kCFRunLoopBeforeSource
和kCFRunLoopBeforeWaiting
的间隔时长超过自定义阀值则记录堆栈信息。 - 推荐文章: RunLoop实战:实时卡顿监控
FPS检测
- 创建CADisplayLink对象的时候会指定一个selector,把创建的CADisplayLink对象加入runloop,所以就实现了以屏幕刷新的频率调用某个方法。
- 在调用的方法中计算执行的次数,用次数除以时间,就算出了FPS。
- 注:iOS正常刷新率为每秒60次。
@implementation ViewController {
UILabel *_fpsLbe;
CADisplayLink *_link;
NSTimeInterval _lastTime;
float _fps;
}
- (void)startMonitoring {
if (_link) {
[_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_link invalidate];
_link = nil;
}
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
self.count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
_fps = _count / delta;
NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps);
self.count = 0;
_fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}
防崩溃处理
-
NSSetUncaughtExceptionHandler(&HandleException);
监听异常信号SIGILL
,SIGTRAP
,SIGABRT
,SIGBUS
,SIGSEGV
,SIGFPE
- 回调方法内创建一个Runloop,将主线程的所有Runmode都拿过来跑,作为应用程序主Runloop的替代。
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allModesRef = CFRunLoopCopyAllModes(runloop);
while (captor.needKeepAlive) {
for (NSString *mode in (__bridge NSArray *)allModesRef) {
if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
continue;
}
CFStringRef modeRef = (__bridge CFStringRef)mode;
CFRunLoopRunInMode(modeRef, keepAliveReloadRenderingInterval, false);
}
}
- 可以记录堆栈信息,上传服务器或者弹出友好提示页面等一系列操作。
常驻线程
可以把自己创建的线程添加到Runloop中,做一些频繁处理的任务,例如:检测网络状态,定时上传一些信息等。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
NSLog(@"----------run----%@", [NSThread currentThread]);
@autoreleasepool{
/*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
[[NSRunLoop currentRunLoop] run];
}
// 方法2
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// 方法3
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
NSLog(@"---------");
}
- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
以上均为个人对runloop的资料收集及部分理解,如有错误请指正,欢迎讨论。
欢迎加入iOS开发交流学习群(密码123),我们一起共同学习,共同成长
收录:原文地址