共计 22868 个字符,预计需要花费 58 分钟才能阅读完成。
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),我们一起共同学习,共同成长
收录:原文地址