1、RunLoop
1.1 RunLoop 对象-底层就是一个CFRunLoopRef结构体
RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象; 事件循环就是对事件/消息进行管理,做到以下三点:
- 没有消息需要处理时,休眠线程以避免资源占用。从用户态切换到内核态,等待消息;
- 有消息需要处理时,立刻唤醒线程,回到用户态处理消息;
- 通过调用mach_msg()函数来转移当前线程的控制权给内核态/用户态。TODO:mach_msg
- RunLoop不是一个简单的do...while循环,它涉及到用户态和内核态之间的切换。
重点在于模式CFRunLoopMode和事件源(输入源(Input Sources)和定时器源(Timer Sources)、观察源(Observe)。输入源(Input Sources)又分为Source0和Source1两种)。
// CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;
// CFRunLoop.c
struct __CFRunLoop {
pthread_t _pthread; // 与线程一一对应,RunLoop与线程是一一对应关系
CFMutableSetRef _commonModes; // 存储着 NSString 对象的集合(Mode 的名称)
CFMutableSetRef _commonModeItems; // 存储着被标记为通用模式的Source0/Source1/Timer/Observer
CFRunLoopModeRef _currentMode; // :RunLoop当前的运行模式
CFMutableSetRef _modes; // 存储着RunLoop所有的 Mode(CFRunLoopModeRef)模式 --- NSMutableSet
...
};
// CFRunLoopModeRef 代表RunLoop的运行模式;
// 一个RunLoop包含若干个 Mode,每个 Mode 又包含若干个Source0/Source1/Timer/Observer;--CFMutableSetRef
// RunLoop启动时只能选择其中一个 Mode,作为 currentMode;如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入,切换模式不会导致程序退出;
// 不同 Mode 中的Source0/Source1/Timer/Observer能分隔开来,互不影响;
// 如果 Mode 里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
struct __CFRunLoopMode {
CFStringRef _name; // mode 类型,如:NSDefaultRunLoopMode
CFMutableSetRef _sources0; // CFRunLoopSourceRef
CFMutableSetRef _sources1; // CFRunLoopSourceRef
CFMutableArrayRef _observers; // CFRunLoopObserverRef
CFMutableArrayRef _timers; // CFRunLoopTimerRef
...
};
1.2 RunLoop使用
iOS 中有 2 套 API 来访问和使用RunLoop: ① Foundation:NSRunLoop(是CFRunLoopRef的封装,提供了面向对象的 API)
② Core Foundation:CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象,但也存在区别:
- NSRunLoop不开源,而CFRunLoopRef是开源的:Core Foundation 源码
- CFRunLoopRef纯C实现,线程安全;NSRunLoop面向对象(使用方便),但不是线程安全的。---TODO
获取RunLoop对象的方式:
// Foundation
[NSRunLoop mainRunLoop]; // 获取主线程的 RunLoop 对象
[NSRunLoop currentRunLoop]; // 获取当前线程的 RunLoop 对象
// Core Foundation
CFRunLoopGetMain(); // 获取主线程的 RunLoop 对象
CFRunLoopGetCurrent(); // 获取当前线程的 RunLoop 对象
1.3 RunLoop 的常见模式--CFRunLoopMode🌟
ModeName | 描述 |
---|---|
NSDefaultRunLoopMode / KCFRunLoopDefaultMode | 默认模式 |
UITrackingRunLoopMode | 界面追踪模式,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响; |
NSRunLoopCommonModes / KCFRunLoopCommonModes | 通用模式(默认包含 KCFRunLoopDefaultMode 和 UITrackingRunLoopMode) 该模式不是实际存在的一种模式,它只是一个特殊的标记,是同步Source0/Source1/Timer/Observer到多个 Mode 中的技术方案。被标记为通用模式的Source0/Source1/Timer/Observer都会存放到 _commonModeItems 集合中,会同步这些Source0/Source1/Timer/Observer到多个 Mode 中。 |
- NSDefaultRunLoopMode和NSRunLoopCommonModes属于Foundation框架;KCFRunLoopDefaultMode和KCFRunLoopCommonModes属于Core Foundation框架;前者是对后者的封装,作用相同。
1.3 面试题
Q1:🌟CFRunLoopModeRef这样设计有什么好处?Runloop为什么会有多个 Mode?---TODO:
Mode做到了屏蔽的效果,当RunLoop运行在 Mode1 下面的时候,是处理不了 Mode2 的事件的;如NSDefaultRunLoopMode默认模式和UITrackingRunLoopMode滚动模式,滚动屏幕的时候就会切换到滚动模式,就不用去处理默认模式下的事件了,保证了 UITableView 的滚动顺畅。
Q2:一个Runloop不能选多个Mode,开销?UITrackingRunLoopMode是在主线程么?
RunLoop 通过 Mode 来组织和过滤不同类型的事件和消息,确保程序在处理某一类事件时不会被其他类型的事件打断。
Q3:为什么一个 RunLoop 不能同时选多个 Mode?
一个 RunLoop 在同一时间只能运行在一个特定的 Mode 下,这是由 RunLoop 的设计决定的。这样做的主要原因是为了简化事件处理逻辑和提高程序运行效率。
如果 RunLoop 同时运行在多个 Mode 下,它需要同时监听和处理多个 Mode 的事件源和定时器,这不仅会增加程序的复杂度,还可能导致事件处理的优先级问题,影响程序的响应性能。
Q4:滚动屏幕时的 RunLoop Mode
当用户滚动屏幕时,RunLoop 会自动切换到 UITrackingRunLoopMode
。这个 Mode 主要用于跟踪用户界面事件(如触摸滑动),以确保用户界面的流畅滚动。在这个 Mode 下,RunLoop 会暂时忽略其他 Mode(如默认的 NSDefaultRunLoopMode
)下的事件源和定时器,从而避免滚动时的卡顿现象。
1、是否在主线程?
是的,用户界面的更新(包括滚动)通常发生在主线程的 RunLoop 中。无论是 NSDefaultRunLoopMode
还是 UITrackingRunLoopMode
,它们都是在主线程的 RunLoop 下运行的。这是因为 UIKit 和许多其他 UI 框架都不是线程安全的,所以 UI 相关的操作必须在主线程执行。
2、为什么不能处理默认模式下的事件?
当 RunLoop 运行在 UITrackingRunLoopMode
下时,它会暂时忽略 NSDefaultRunLoopMode
下的事件源和定时器,这是为了确保用户界面滚动的流畅性。如果在用户滚动界面时还需要处理其他模式下的事件(如网络请求回调),可以考虑将这些任务放在其他线程执行,或者将事件源添加到 NSRunLoopCommonModes
中。NSRunLoopCommonModes
是一个占位 Mode,它实际上代表了一组 Mode,包括 NSDefaultRunLoopMode
和 UITrackingRunLoopMode
,这样即使在滚动时也能处理这些事件。
3、总结
RunLoop 的设计确保了程序在处理特定类型的事件时的专注性和效率,通过 Mode 的切换来优化用户界面的流畅性。虽然在某一时刻 RunLoop 只能运行在一个 Mode 下,但通过合理的设计和 Mode 的选择,可以有效地处理不同类型的任务和事件,同时保持程序的响应性和性能。
1.4 事件源
事件源分为CFRunLoopSourceRef输入源(Input Sources)和定时器源(Timer Sources)两种;输入源(Input Sources)又分为Source0和Source1两种,以下__CFRunLoopSource中的共用体union中的version0和version1就分别对应Source0和Source1。
1.4.1 CFRunLoopSourceRef
// CFRunLoop.h
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
// CFRunLoop.m
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
Source0 和 Source1 的区别:
Input Sources | 区别 | 数据结构 |
---|---|---|
Source0 | 需要手动唤醒线程:添加Source0到RunLoop并不会主动唤醒线程,需要手动唤醒) ① 触摸、滑动、点击事件处理(非基于Port) ② performSelector:onThread: | 数组,用来存储事件源 |
Source1 | 具备唤醒线程的能力 ① 基于 Port 的线程间通信② 系统事件捕捉:系统事件捕捉是由Source1来处理,然后再交给Source0处理 | key:machPort, value:事件源 |
扩展
基于Port的线程间通信是一种在不同线程或进程间传递消息的机制。在多线程和多进程编程中,线程间通信(IPC)是一个重要的概念,它允许数据在不同的执行线程或进程间共享和传递。Port基于消息传递机制,提供了一种灵活的通信方式。### 基本概念- Port(端口):是一种抽象的通信通道,可以用于发送和接收消息。每个Port都有一个唯一的标识,用于区分不同的通信通道。- 消息:是在Port间传递的数据单元,通常包含有关通信的信息,如数据内容、发送者标识等。- 消息队列:每个Port都有一个与之关联的消息队列,用于存储待处理的消息。### 通信流程1. 创建Port:首先,需要在通信的两端(线程或进程)创建Port。在多线程环境下,这通常意味着创建一对互相连接的Port,每个线程持有一个。2. 发送消息:发送方将消息和相关数据打包,通过自己持有的Port发送出去。3. 接收消息:接收方通过监听自己持有的Port,获取消息队列中的消息,并进行处理。### 在不同系统中的实现- macOS和iOS(基于Mach消息):在macOS和iOS中 ,基于Port的通信很大程度上依赖于底层的Mach消息机制。Mach是一个微内核,提供了强大的进程间通信机制。在这些系统中,可以使用mach_port_t
类型的Port进行消息传递。- Windows:Windows平台提供了不同的IPC机制,如管道、邮槽等。虽然没有直接对应于Port的概念,但可以通过这些机制实现类似的通信功能。### 示例以macOS或iOS中基于Mach Port的通信为例:
#include <mach/mach.h>
// 创建Port
mach_port_t myPort;
kern_return_t result = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &myPort);
// 发送消息
mach_msg_header_t message;
message.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
message.msgh_size = sizeof(message);
message.msgh_remote_port = remotePort; // 接收方的Port
message.msgh_local_port = MACH_PORT_NULL;
message.msgh_id = 100; // 消息ID
mach_msg_send(&message);
// 接收消息
mach_msg(&message, MACH_RCV_MSG, 0, sizeof(message), myPort, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
在实际应用中,基于Port的通信可以实现复杂的数据交换和同步机制,但同时也需要注意消息格式的设计、错误处理和资源管理等问题。
1.4.2 CFRunLoopTimerRef
CFRunloopTimer和NSTimer是 toll-free bridged 的,可以相互转换;
toll-free bridged
Toll-Free Bridging(无缝桥接)是指在Core Foundation框架和Foundation框架之间,某些类型的对象可以无缝地在两个框架之间转换和使用的技术。这意味着,对于支持无缝桥接的类型,你可以将一个框架中的对象当作另一个框架中的对象来使用,而不需要进行显式的类型转换。这主要是因为这些**对象在内存中的表示是兼容**的。
例如,`NSString`(Foundation框架中的)和`CFStringRef`(Core Foundation框架中的)之间就存在无缝桥接。这意味着你可以在需要`CFStringRef`的地方使用`NSString`对象,反之亦然,而不需要**进行任何显式的转换**。这种转换在编译时是不可见的,由编译器和运行时系统自动处理。
NSString *nsString = @"Hello, World!";
CFStringRef cfString = (__bridge CFStringRef)nsString; // 将NSString转换为CFStringRef
NSString *nsStringAgain = (__bridge NSString *)cfString; // 将CFStringRef转换回NSString
在上面的代码中, __bridge
转换告诉编译器,我们只是在不同的框架之间传递对象的引用,而不改变对象的所有权或引用计数。这种转换是安全的,因为NSString
和CFStringRef
在内存布局上是兼容的,系统能够理解这两种类型的对象实际上是相同的。
需要注意的是,并不是所有的Core Foundation类型都有与之对应的Foundation类型,只有部分类型支持无缝桥接。此外,虽然无缝桥接在使用上非常方便,但开发者仍需注意对象的所有权和内存管理问题,特别是在非ARC(自动引用计数)环境下。
performSelector:withObject:afterDelay:方法会创建timer并添加到RunLoop中。
// CFRunLoop.h
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
// CFRunLoop.c
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; // 添加该 timer 的 RunLoop
CFMutableSetRef _rlModes; // 所有包含该 timer 的 modeName
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable 理想时间间隔 */
CFTimeInterval _tolerance; /* mutable 时间偏差 */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable 回调入口 */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
在Objective-C中,除了使用performSelector:withObject:afterDelay:
方法外,还有其他几种方式可以创建定时器(timer)并将其添加到RunLoop中。
以下是一些常见的方法:
1. 使用`NSTimer`类直接创建
`NSTimer`提供了多种方法来创建定时器,并可以选择是否立即将其添加到当前线程的RunLoop中。
- **不自动添加到RunLoop**
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
这种方式创建的定时器需要手动添加到RunLoop中。`timerWithTimeInterval:target:selector:userInfo:repeats:`方法创建了一个定时器,但没有自动将其添加到RunLoop中,需要通过`addTimer:forMode:`方法手动添加。
- **自动添加到RunLoop**
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
使用`scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:`方法创建的定时器会被自动添加到当前RunLoop的默认模式(`NSDefaultRunLoopMode`)中。
2. 使用`CADisplayLink`
`CADisplayLink`是一个特殊类型的定时器,用于同步屏幕刷新率进行定期调用。它通常用于高性能图形渲染和动画,确保动画的平滑性。
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
3. 使用`dispatch_after`和GCD
虽然这种方式并不是直接添加到RunLoop中,但它提供了另一种延迟执行代码的方法,可以在某些情况下替代NSTimer。
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// 延迟执行的代码
});
4. 使用`performSelector:withObject:afterDelay:`的变体
除了最初提到的`performSelector:withObject:afterDelay:`方法外,还有一些相关的方法,如`performSelector:onThread:withObject:waitUntilDone:modes:`,它允许你指定执行方法的线程和RunLoop模式。
[self performSelector:@selector(myMethod:) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
这些方法提供了灵活的方式来在特定的线程和RunLoop模式下延迟执行方法。
总的来说,Objective-C提供了多种方式来创建和管理定时器,可以根据具体的需求和场景选择最合��的方法。
1.4.3 CFRunLoopObserverRef
作用: CFRunLoopObserverRef用来监听RunLoop的 6 种活动状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timers
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 表示以上所有状态
};
// CFRunLoop.h
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
// CFRunLoop.c
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; // 添加该 observer 的 RunLoop
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable 监听的活动状态 */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable 回调入口 */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
CFRunLoopObserverRef中的 _activities用来保存RunLoop的活动状态。当RunLoop的状态发生改变时,通过回调_callout通知所有监听这个状态的Observer。
- UI 刷新(BeforeWaiting)
- Autorelease pool(BeforeWaiting)
1.5 主线程的 RunLoop 的启动过程
iOS 程序能保持持续运行的原因就是在main()函数中调用了UIApplicationMain函数,这个函数内部会启动主线程的RunLoop。bt指令
CFRunLoopRun->CFRunLoopRunSpecific-> __CFRunLoopRun
Int32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 通知 Observers:即将进入 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// RunLoop 具体要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers:即将退出 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
/**
* __CFRunLoopRun
*
* @param rl 运行的 RunLoop 对象
* @param rlm 运行的 mode
* @param seconds loop 超时时间
* @param stopAfterHandle true: RunLoop 处理完事件就退出 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 {
// 通知 Observers:即将处理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers:即将处理 Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 处理 Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判断有无 Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有 Source1,就跳转到 handle_msg
goto handle_msg;
}
// 通知 Observers:即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// ⚠️休眠,等待消息来唤醒线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers:刚从休眠中唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被 Timer 唤醒) {
// 处理 Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (被 GCD 唤醒) {
// 处理 GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被 Source1 唤醒
// 处理 Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
/* 设置返回值 */
// 进入 loop 时参数为处理完事件就返回
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;
// mode 中没有任何的 Source0/Source1/Timer/Observer
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
RunLoop主要就做以下几件事情:
- __CFRunLoopDoObservers:通知Observers接下来要做什么
- __CFRunLoopDoBlocks:处理Blocks
- __CFRunLoopDoSources0:处理Sources0
- __CFRunLoopDoSources1:处理Sources1
- __CFRunLoopDoTimers:处理Timers
- 处理 GCD 相关:dispatch_async(dispatch_get_main_queue(), ^{ });
- __CFRunLoopSetSleeping/__CFRunLoopUnsetSleeping:休眠等待/结束休眠
- __CFRunLoopServiceMachPort -> mach-msg():转移当前线程的控制权
1.6 RunLoop 休眠的实现原理:__CFRunLoopServiceMachPort 函数实现:内核态/用户态切换
在__CFRunLoopRun函数中,会调用__CFRunLoopServiceMachPort函数,该函数中调用了mach_msg()函数来转移当前线程的控制权给内核态/用户态。
- 没有消息需要处理时,休眠线程以避免资源占用。调用mach_msg()从用户态切换到内核态,等待消息;
- 有消息需要处理时,立刻唤醒线程,调用mach_msg()回到用户态处理消息。
这就是RunLoop休眠的实现原理,也是RunLoop与简单的do...while循环区别:
- RunLoop:休眠的时候,当前线程不会做任何事,CPU 不会再分配资源;
- 简单的do...while循环:当前线程并没有休息,一直占用 CPU 资源。
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, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
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(); }
// ⚠️⚠️⚠️
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|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);
// Take care of all voucher-related work right after mach_msg.
// If we don't release the previous voucher we're going to leak it.
voucher_mach_msg_revert(*voucherState);
// Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
*voucherState = voucher_mach_msg_adopt(msg);
if (voucherCopy) {
if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) {
// Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy.
// CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged.
*voucherCopy = voucher_copy();
} else {
*voucherCopy = NULL;
}
}
CFRUNLOOP_WAKEUP(ret);
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
if (MACH_RCV_TOO_LARGE != ret) break;
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
1.7 RunLoop 与线程的关系
苹果官方文档中,RunLoop的相关介绍写在线程编程指南中,可见RunLoop和线程的关系不一般。Threading Programming Guide(苹果官方文档)
-
RunLoop对象和线程是一一对应关系;保存在一个全局的Dictionary里,线程作为key,RunLoop作为value;
-
如果没有RunLoop,线程执行完任务就会退出;如果没有RunLoop,主线程执行完main()函数就会退出,程序就不能处于运行状态;
-
RunLoop创建时机:线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;TODO_--
-
RunLoop销毁时机:RunLoop会在线程结束时销毁;
-
主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop;
-
主线程的RunLoop对象是在UIApplicationMain中通过[NSRunLoop currentRunLoop]获取,一旦发现它不存在,就会创建RunLoop对象。
运行循环:juejin.cn/post/684490…. 实践 TODO
运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。
int main(int argc, char \* argv\[]) {
@autoreleasepool {
NSLog(@"开始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass(\[AppDelegate class]));
NSLog(@"结束");
return re;
}
}
运行程序,我们发现只会打印开始,并不会打印结束,这**说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行**。 我们来看到RunLoop的源码
// 用DefaultMode启动
void CFRunLoopRun(void) { /\* DOES CALLOUT \*/
int32\_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK\_FOR\_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
1.7.1 未启动 RunLoop 的子线程
创建一个NSThread的子类HTThread并重写了dealloc方法来观察线程的状态。执行以下代码,发现子线程执行完一次test任务就退出销毁了,没有再执行test任务,原因就是没有启动该线程的RunLoop
- (void)testRunLoop {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
// 开启
}
- (void)test {
NSLog(@"testRunLoop on %@", [NSThread currentThread]);
}
1.7.2 开启子线程的 RunLoop-获取RunLoop对象/开启RunLoop
// 获取 RunLoop 对象
// Foundation
[NSRunLoop mainRunLoop]; // 获取主线程的 RunLoop 对象
[NSRunLoop currentRunLoop]; // 获取当前线程的 RunLoop 对象
// Core Foundation
CFRunLoopGetMain(); // 获取主线程的 RunLoop 对象
CFRunLoopGetCurrent(); // 获取当前线程的 RunLoop 对象
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
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);
__CFLock(&loopsLock);
}
// ️当前线程作为 Key,从 __CFRunLoops 字典中获取 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) { // ️如果字典中不存在
CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 创建当前线程的 RunLoop
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
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-Foundation
[[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// Core Foundation
CFRunLoopRun();
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false); // 第3个参数:设置为 true,代表执行完 Source/Port 后就会退出当前 loop
// 以CFRunLoopRun为例
void CFRunLoopRun(void) {
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) {
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
// 通过调用CFRunLoopRunSpecific()函数来启动RunLoop
1.7.3 实现一个常驻线程---TODO
好处:经常用到子线程的时候,不用一直创建销毁,提高性能; 条件:该任务需是串行的,而非并发; 步骤: ① 获取/创建当前线程的RunLoop;
② 向该RunLoop中添加一个Source/Port等来维持RunLoop的事件循环(如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop会立马退出);
③ 启动该RunLoop。
// ViewController.m
#import "ViewController.h"
#import "HTThread.h"
@interface ViewController ()
@property (nonatomic, strong) HTThread *thread;
@property (nonatomic, assign, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[HTThread alloc] initWithBlock:^{
NSLog(@"begin-----%@", [NSThread currentThread]);
// ① 获取/创建当前线程的 RunLoop
// ② 向该 RunLoop 中添加一个 Source/Port 等来维持 RunLoop 的事件循环
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
// ③ 启动该 RunLoop
/*
[[NSRunLoop currentRunLoop] run]
如果调用 RunLoop 的 run 方法,则会开启一个永不销毁的线程
因为 run 方法会通过反复调用 runMode:beforeDate: 方法,以运行在 NSDefaultRunLoopMode 模式下
换句话说,该方法有效地开启了一个无限的循环,处理来自 RunLoop 的输入源 Sources 和 Timers 的数据
*/
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"end-----%@", [NSThread currentThread]);
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (!self.thread) return;
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
}
// 停止子线程的 RunLoop
- (void)stopThread
{
// 设置标记为 YES
self.stopped = YES;
// 停止 RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
// 清空线程
self.thread = nil;
}
- (void)dealloc
{
NSLog(@"%s", __func__);
if (!self.thread) return;
// 在子线程调用(waitUntilDone设置为YES,代表子线程的代码执行完毕后,当前方法才会继续往下执行)
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
@end
// HTThread.h
#import <Foundation/Foundation.h>
@interface HTThread : NSThread
@end
// HTThread.m
#import "HTThread.h"
@implementation HTThread
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
1.8 RunLoop 与 NSTimer
NSTimer是由RunLoop来管理的,NSTimer其实就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的,可以相互转换; 如果我们在子线程上使用NSTimer,就必须开启子线程的RunLoop,否则定时器无法生效。
🌟解决 tableview 滑动时 NSTimer 失效的问题--应用点:滑动的时候用NSTimer
- 问题:因为RunLoop同一时间只能运行在一种模式下,当我们滑动tableview/scrollview的时候RunLoop会切换到UITrackingRunLoopMode界面追踪模式下。如果我们的NSTimer是添加到RunLoop的。KCFRunLoopDefaultMode/NSDefaultRunLoopMode默认模式下的话,此时是会失效的。
- 解决:我们可以将NSTimer添加到RunLoop的KCFRunLoopCommonModes/NSRunLoopCommonModes通用模式下,来保证无论在默认模式还是界面追踪模式下NSTimer都可以执行。
- NSTimer的创建方式--通过timerxxx开头方法创建的NSTimer是不会自动添加到RunLoop中的,所以一定要记得手动添加,否则NSTimer不生效。
// 自动添加到RunLoop的默认模式下的
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
// 自定义添加到RunLoop的某种模式下
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 如果是通过timerxxx开头方法创建的NSTimer是不会自动添加到RunLoop中的,所以一定要记得手动添加,否则NSTimer不生效。
CFRunLoopAddTimer 函数实现
CFRunLoopAddTimer()函数中会判断传入的modeName模式名称是不是 kCFRunLoopCommonModes通用模式,是的话就会将timer添加到RunLoop的 _commonModeItems 集合中,并同步该timer到 _commonModes 里的所有模式中,这样无论在默认模式还是界面追踪模式下NSTimer都可以执行。
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);
if (modeName == kCFRunLoopCommonModes) { // 判断 modeName 是不是 kCFRunLoopCommonModes
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) { // 懒加载,判断 _commonModeItems 是否为空,是的话创建
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlt); // 将 timer 添加到 _commonModeItems 中
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt}; // 将 timer 和 RunLoop 封装到 context 中
/* add new item to all common-modes */
// 遍历 commonModes,将 timer 添加到 commonModes 的所有模式下
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
......
}
}
static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
CFStringRef modeName = (CFStringRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}
NSTimer 和 CADisplayLink 存在的问题
不准时:NSTime和CADisplayLink底层都是基于RunLoop的CFRunLoopTimerRef的实现的,也就是说它们都依赖于RunLoop。如果RunLoop的任务过于繁重,会导致它们不准时。比如NSTimer每1.0秒就会执行一次任务,Runloop每进行一次循环,就会看一下NSTimer的时间是否达到1.0秒,是的话就执行任务。但是由于Runloop每一次循环的任务不一样,所花费的时间就不固定。假设第一次循环所花时间为 0.2s,第二次 0.3s,第三次 0.3s,则再过 0.2s 就会执行NSTimer的任务,这时候可能Runloop的任务过于繁重,第四次花了0.5s,那加起来时间就是 1.3s,导致NSTimer不准时。
解决方法:使用 GCD 的定时器。GCD 的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时。 示例如下:
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
//创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置时间(start:几s后开始执行; interval:时间间隔)
uint64_t start = 2.0; //2s后开始执行
uint64_t interval = 1.0; //每隔1s执行
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
//设置回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%@",[NSThread currentThread]);
});
//启动定时器
dispatch_resume(timer);
NSLog(@"%@",[NSThread currentThread]);
self.timer = timer;
/*
2020-02-01 21:34:23.036474+0800 多线程[7309:1327653] <NSThread: 0x600001a5cfc0>{number = 1, name = main}
2020-02-01 21:34:25.036832+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:26.036977+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:27.036609+0800 多线程[7309:1327707] <NSThread: 0x600001a1e5c0>{number = 4, name = (null)}
*/
1.9 RunLoop和自动释放池的关系,自动释放池在runloop的什么时机去创建、pop、push、销毁对象和释放的?🌲
RunLoop和自动释放池(Autorelease Pool)之间存在着密切的关系。在Cocoa和Cocoa Touch框架中,自动释放池是与RunLoop周期性地交互以管理内存的一种机制。下面是它们之间的关系以及自动释放池在RunLoop的各个时机中如何创建、pop、push和销毁对象的详细说明:
RunLoop和自动释放池的关系
RunLoop是一个事件处理循环,用于管理应用程序的事件和消息。它负责接收和分发各种输入事件(如触摸事件、定时器事件和IO事件)到应用程序的正确部分。 RunLoop为了提高效率和节省资源,不会在应用程序的整个生命周期内一直运行,而是在有事件处理时运行,没有事件处理时休眠。
自动释放池是用于存放那些标记为自动释放的对象。当自动释放池被销毁或“pop”时,池中的所有对象都会收到一条release
消息。这样可以延迟释放对象的时间,直到池被销毁,从而避免了频繁的创建和销毁,提高了内存管理的效率。
自动释放池在RunLoop的什么时机去创建、pop、push、销毁对象和释放
在RunLoop的每个周期开始时,会自动创建一个新的自动释放池。在RunLoop的事件处理阶段,如果有对象被标记为自动释放,这些对象会被加入到当前的自动释放池中。当RunLoop的周期结束时,当前的自动释放池会被销毁(pop),池中的所有对象都会收到一条release
消息。
具体来说,自动释放池的操作通常发生在RunLoop的以下时机:
-
入口(Entry):RunLoop开始时,会创建一个新的自动释放池。
-
在事件处理之前:在处理定时器、输入源等事件之前,可能会创建和销毁临时的自动释放池,以确保在事件处理过程中创建的自动释放对象能够被及时释放。
-
退出(Exit):RunLoop周期结束时,当前的自动释放池会被销毁,池中的对象会被释放。
这种设计确保了在一个RunLoop周期内创建的自动释放对象能够在周期结束时得到释放,从而管理内存使用,避免内存泄漏。同时,它也允许开发者不必过于担心在短时间内创建大量临时对象的内存管理问题。
总结来说,RunLoop和自动释放池之间的关系体现在RunLoop周期性地管理自动释放池,以此来高效地管理内存。自动释放池的创建和销毁与RunLoop的周期紧密相关,确保了对象的生命周期与事件处理循环的同步。
1.10 一个线程或者一个runloop会代表一个自动释放池么
不完全是。在Objective-C中,自动释放池(Autorelease Pool)和线程(Thread)或运行循环(Run Loop)的关系更加灵活和复杂。理解这些概念之间的关系对于管理内存和优化应用性能非常重要。
自动释放池与线程
每个线程可以有多个自动释放池,它们通常以嵌套的形式存在。自动释放池是用来存放那些在当前作用域结束时需要被释放的对象的。在Objective-C中,你可以使用@autoreleasepool
块来创建一个自动释放池。当执行离开这个块的作用域时,块中的所有对象都会接收到release
消息。
在主线程中,应用的主运行循环会在每次事件循环开始时创建一个新的自动释放池,并在事件循环结束时将其排空(drain)。这意味着主线程上的自动释放池会随着事件循环的每次迭代而创建和销毁。
对于后台线程,如果你是通过GCD(Grand Central Dispatch)或者NSOperation
来创建的,那么系统会自动为每个任务创建和销毁自动释放池。但如果你直接创建一个线程,那么你需要手动管理自动释放池。通常,你会在线程的入口点创建一个自动释放池,并在线程的任务完成时将其排空。
自动释放池与运行循环
运行循环(Run Loop)是用于管理事件输入的循环,主线程和其他需要处理事件的线程都会有运行循环。在主线程的运行循环中,每次循环迭代开始时都会自动创建一个新的自动释放池,并在迭代结束时将其排空。这确保了主线程上的对象不会在不需要时占用内存。对于手动创建的线程,如果你启动了一个运行循环,那么你需要在适当的位置手动管理自动释放池,就像管理线程一样。
总结
-
一个线程可以有多个自动释放池,它们通常以嵌套的形式存在。
-
在主线程中,主运行循环会自动管理自动释放池。
-
在手动创建的后台线程中,需要手动创建和销毁自动释放池。
-
运行循环和自动释放池的关系主要体现在主线程中,后台线程需要手动管理。
正确管理自动释放池对于内存管理和应用性能优化至关重要。