38. 对比分析 source0 和 source1。
首先我们从代码层面对 source0 和 source1 版本的 CFRunLoopSourceRef 进行区分,struct __CFRunLoopSource 通过其内部的 _context 来进行区分 source0 和 source1。
struct __CFRunLoopSource {
...
union {
CFRunLoopSourceContext version0;
CFRunLoopSourceContext1 version1;
} _context;
};
其中 version0、version1 分别对应 source0 和 source1,下面我们再看一下 CFRunLoopSourceContext 和 CFRunLoopSourceContext1 的定义:
typedef struct {
...
void * info; // 作为 perform 函数的参数
...
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode); // 当 source0 加入到 run loop 时触发的回调函数(在 CFRunLoopAddSource 函数中可看到其被调用)
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode); // 当 source0 从 run loop 中移除时触发的回调函数
// source0 要执行的任务块,当 source0 事件被触发时的回调, 调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 函数来执行 perform(info)
void (*perform)(void *info);
} CFRunLoopSourceContext;
typedef struct {
...
void * info; // 作为 perform 函数的参数
...
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
// getPort 函数指针,用于当 source1 被添加到 run loop 中的时候,从该函数中获取具体的 mach_port_t 对象,用来唤醒 run loop
mach_port_t (*getPort)(void *info);
// perform 函数指针即指向 run loop 被唤醒后 source1 要执行的回调函数,调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ 函数来执行
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
// 其它平台
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
source0 仅包含一个回调函数(perform),它并不能主动唤醒 run loop(进入休眠的 run loop 仅能通过 mach port 和 mach_msg 来唤醒)。使用时,你需要先调用 CFRunLoopSourceSignal(rls) 将这个 source 标记为待处理,然后手动调用 CFRunLoopWakeUp(rl) 来唤醒 run loop(CFRunLoopWakeUp 函数内部是通过 run loop 实例的 _wakeUpPort 成员变量来唤醒 run loop 的),唤醒后的 run loop 继续执行 __CFRunLoopRun 函数内部的外层 do while 循环来执行 timers(执行到达执行时间点的 timer 以及更新下次最近的时间点) 和 sources 以及 observer 回调 run loop 状态,其中通过调用 __CFRunLoopDoSources0 函数来执行 source0 事件,执行过后的 source0 会被 __CFRunLoopSourceUnsetSignaled(rls) 标记为已处理,后续 run loop 循环中不会再执行标记为已处理的 source0。source0 不同于不重复执行的 timer 和 run loop 的 block 链表中的 block 节点,source0 执行过后不会自己主动移除,不重复执行的 timer 和 block 执行过后会自己主动移除,执行过后的 source0 可手动调用 CFRunLoopRemoveSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode) 来移除。
source0 具体执行时的函数如下,info 做参数执行 perform 函数。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info); // perform(info)
下面是我们手动创建 source0 的示例代码,创建好的 CFRunLoopSourceRef 必须调用 CFRunLoopSourceSignal 函数把其标记为待处理,否则即使 run loop 正常循环,这里的 rls 也得不到执行,由于 thread 线程中的计时器存在所以这里也可以不用手动调用 CFRunLoopWakeUp 唤醒 run loop,run loop 已是唤醒状态,rls 能在 run loop 的一个循环中正常得到执行,然后是其中的三个断点,当执行到断点时我们在控制台打印 po [NSRunLoop currentRunLoop] 可在 kCFRunLoopDefaultMode 的 sources0 哈希表中看到 rls,以及它的 signalled 标记的值,通过源码可知在 rls 的 perform 待执行之前就会先调用 __CFRunLoopSourceUnsetSignaled(rls) 把其标记为已经处理,且处理过的 rls 并不会主动移除,它依然被保存在 kCFRunLoopDefaultMode 的 sources0 哈希表中,我们可以使用 CFRunLoopRemoveSource 函数手动移除。source0 不同于不重复执行的 timer 和 run loop 的 block 链表中的 block 节点,source0 执行过后不会自己主动移除,不重复执行的 timer 和 block 执行过后自己会主动移除。
话说是执行 source0 时需要手动调用 CFRunLoopWakeUp 来唤醒 run loop,实际觉得好像大部分场景下其它事件都会导致 run loop 正常进行着循环,只要 run loop 进行循环则标记为待处理的 source0 就能得到执行,好像并不需要我们刻意的手动调用 CFRunLoopWakeUp 来唤醒当前的 run loop。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"🧗♀️🧗♀️ ....");
// 构建下下文,这里只有三个参数有值,0 是 version 值代表是 source0,info 则直接传的 self 即当前的 vc,schedule 和 cancel 偷懒了传的 NULL,它们分别是
// 执行 CFRunLoopAddSource 添加 rls 和 CFRunLoopRemoveSource 移除 rls 时调用的,大家可以自己试试,
// 然后最后是执行函数 perform 传了 runLoopSourcePerformRoutine。
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, runLoopSourcePerformRoutine};
CFRunLoopSourceRef rls = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode);
// 创建好的 rls 必须手动标记为待处理,否则即使 run loop 正常循环也不会执行此 rls
CFRunLoopSourceSignal(rls); // ⬅️ 断点 1
// 由于计时器一直在循环执行,所以这里可不需要我们手动唤醒 run loop
CFRunLoopWakeUp(CFRunLoopGetCurrent()); // ⬅️ 断点 2
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"⏰⏰⏰ timer 回调...");
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode); // ⬅️ 断点 4(这里执行一次计时器回调再打断点)
}];
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
}
void runLoopSourcePerformRoutine (void *info) {
NSLog(@"👘👘 %@", [NSThread currentThread]); // ⬅️ 断点 3
}
初始创建完成的 rls 的 signalled 值为 NO,如果接下来不执行 CFRunLoopSourceSignal(rls) 的话,rls 是不会被 run loop 执行的。
// ⬅️ 断点 1
...
sources0 = <CFBasicHash 0x282aa55f0 [0x20e729430]>{type = mutable set, count = 1,
entries =>
1 : <CFRunLoopSource 0x2811f6580 [0x20e729430]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x139d1c2e0, callout = runLoopSourcePerformRoutine (0x100e929ec)}}
}
...
CFRunLoopSourceSignal(rls) 执行后,看到 rls 的 signalled 置为 Yes,在 run loop 循环中调用 __CFRunLoopDoSources0 函数时 rls 会得到执行。
// ⬅️ 断点 2
...
sources0 = <CFBasicHash 0x282aa55f0 [0x20e729430]>{type = mutable set, count = 1,
entries =>
1 : <CFRunLoopSource 0x2811f6580 [0x20e729430]>{signalled = Yes, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x139d1c2e0, callout = runLoopSourcePerformRoutine (0x100e929ec)}}
}
...
通过 __CFRunLoopDoSources0 函数的源码可知在 rls 的 perform 函数执行之前 __CFRunLoopSourceUnsetSignaled(rls) 已经把 rls 标记为已处理。
// ⬅️ 断点 3
...
sources0 = <CFBasicHash 0x282aa55f0 [0x20e729430]>{type = mutable set, count = 1,
entries =>
1 : <CFRunLoopSource 0x2811f6580 [0x20e729430]>{signalled = No, valid = Yes, order = 0, context = <CFRunLoopSource context>{version = 0, info = 0x139d1c2e0, callout = runLoopSourcePerformRoutine (0x100e929ec)}}
}
...
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode) 执行过后看到 rls 已经被移除,这里 source0 不同于不重复执行的 timer 和 run loop 的 block 链表中的 block 节点,source0 执行过后不会自己主动移除,timer 和 block 执行过后自己会主动移除。
// ⬅️ 断点 4
...
sources0 = <CFBasicHash 0x282aa55f0 [0x20e729430]>{type = mutable set, count = 0,
entries =>
}
...
针对 timers/sources(0/1) 的执行流程(暂时忽略 run loop 休眠和 main run loop 执行,其实极极极大部分情况我们都是在使用主线程的 run loop,这里为了分析 timers/sources 暂时假装是在子线程的 run loop 中)我们这里再回顾一下 __CFRunLoopRun 函数,从 __CFRunLoopRun 函数的外层 do while 循环开始,首先进来会连着回调 kCFRunLoopBeforeTimers 和 kCFRunLoopBeforeSources 两个 run loop 的活动变化,然后接下来就是调用 __CFRunLoopDoSources0(rl, rlm, stopAfterHandle) 来执行 source0,如果有 source0 被执行了,则 sourceHandledThisLoop 为 True,就不会回调 kCFRunLoopBeforeWaiting 和 kCFRunLoopAfterWaiting 两个活动变化。接着是根据当前 run loop 的本次循环被某个 mach port 唤醒的(__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy) 唤醒本次 run loop 的 mach port 会被赋值到 livePort 中)来处理具体的内容,假如是 rlm->_timerPort(或 modeQueuePort 它两等同只是针对不同的平台不同的 timer 使用方式)唤醒的则调用 __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()) 来执行 timer 的回调,如果还有其它 timer 或者 timer 重复执行的话会调用 __CFArmNextTimerInMode(rlm, rl) 来更新注册下次最近的 timer 的触发时间。 最后的话就是 source1 的端口了,首先通过 CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort)(内部是 CFRunLoopSourceRef found = rlm->_portToV1SourceMap ? (CFRunLoopSourceRef)CFDictionaryGetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)port) : NULL;,即从 rlm 的 _portToV1SourceMap 字典中以 livePort 为 Key 找到对应的 CFRunLoopSourceRef)来找到 livePort 所对应的具体的 rls(source1),然后是调用 __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) 来执行 rls 的回调,内部具体的执行是 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(rls->_context.version1.perform, msg, size, reply, rls->_context.version1.info) 即同样是 info 做参数执行 perform 函数,且在临近执行前同样会调用 __CFRunLoopSourceUnsetSignaled(rls) 把 source1 标记为已处理,且同 soure0 一样也不会主动从 rlm 的 sources1 哈希表中主动移除。(source1 系统会自动 signaled)
Source1 包含了一个 mach port(由 CFRunLoopSourceRef 创建时的 CFRunLoopSourceContext1 传入) 和一个回调(CFRunLoopSourceContext1 的 perform 函数指针),被用于通过内核和其它线程相互发送消息(mach_msg),这种 Source 能主动唤醒 run loop 的线程。
Source1 包含的 mach port 来自于创建 source1 时 CFRunLoopSourceContext1 的 info 成员变量,CFRunLoopSourceRef 通过 _context 的 info 持有 mach port,同时以 CFRunLoopSourceRef 为 Key,以 mach port 为 Value 保存在 rlm 的 _portToV1SourceMap 中,并且会把该 mach port 插入到 rlm 的 _portSet 中。如下代码摘录自 CFRunLoopAddSource 函数中:
...
} else if (1 == rls->_context.version0.version) {
// 把 rls 添加到 rlm 的 _sources1 集合中
CFSetAddValue(rlm->_sources1, rls);
// 以 info 为参,调用 rls->_context.version1.getPort 函数读出 mach port
// 基于 CFMachPort 创建的 CFRunLoopSourceRef 其 context 的 getPort 指针被赋值为 __CFMachPortGetPort 函数(iOS 下仅能使用 CFMachPort,不能使用 CFMessagePort)
// 基于 CFMessagePort 创建的 CFRunLoopSourceRef 其 context 的 getPort 指针被赋值为 __CFMessagePortGetPort 函数(macOS 下可用 CFMessagePort)
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) {
// 把 rls 和 src_port 保存在 rlm 的 _portToV1SourceMap 字典中
CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
// 把 src_port 插入到 rlm 的 _portSet 中
__CFPortSetInsert(src_port, rlm->_portSet);
}
}
...
可看到 source0 中仅有一些回调函数(perform 函数指针)会在 run loop 的本次循环中执行,而 source1 中有 mach port 可用来主动唤醒 run loop 后执行 source1 中的回调函数(perform 函数指针),即 source1 创建时会有 mach port 传入,然后当通过 mach_msg 函数向这个 mach port 发消息时,当前的 run loop 就会被唤醒来执行这个 source1 事件,但是 source0 则是依赖于由 “别人” 来唤醒 run loop,例如由开发者手动调用 CFRunLoopWakeUp 函数来唤醒 run loop,或者由 source1 唤醒 run loop 后,在当前 run loop 的本次循环中被标记为待处理的 source0 也趁机得到执行。
source1 由 run loop 和内核管理,mach port 驱动。 source0 则偏向应用层一些,如 Cocoa 里面的 UIEvent 处理,会以 source0 的形式发送给 main run loop。
翻看了几篇博客后发现手动唤醒 run loop 适用的场景可以是在主线程中唤醒休眠中的子线程。只要能拿到子线程的 run loop 对象就能通过调用 CFRunLoopWakeUp 函数来唤醒指定的子线程,唤醒的方式是调用 mach_msg 函数向子线程的 run loop 对象的 _weakUpPort 发送消息即可。下面我们看一下挺简短的源码。
CFRunLoopWakeUp 函数定义如下,只需要一个我们想要唤醒的线程的 run loop 对象。
void CFRunLoopWakeUp(CFRunLoopRef rl) {
CHECK_FOR_FORK();
// This lock is crucial to ignorable wakeups, do not remove it.
// CFRunLoopRef 加锁
__CFRunLoopLock(rl);
// 如果 rl 已经被标记为 "忽略唤醒",则直接解锁 return,
// 其实当 rl 有这个 "忽略唤醒" 的标记时代表的是 rl 此时已经是唤醒状态了,所以本次唤醒操作可以忽略。
// 全局搜索 __CFRunLoopSetIgnoreWakeUps 设置 "忽略唤醒" 标记的函数,
// 可发现其调用都是在 __CFRunLoopRun 函数中 run loop 唤醒之前,用来标记 run loop 此时是唤醒状态。
if (__CFRunLoopIsIgnoringWakeUps(rl)) {
__CFRunLoopUnlock(rl);
return;
}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
kern_return_t ret;
/* We unconditionally try to send the message, since we don't want to lose a wakeup,
but the send may fail if there is already a wakeup pending, since the queue length is 1. */
// __CFSendTrivialMachMessage 函数内部正是调用 mach_msg 向 rl->_wakeUpPort 端口发送消息
ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
// 发送不成功且不是超时,则 crash
if (ret != MACH_MSG_SUCCESS && ret != MACH_SEND_TIMED_OUT) CRASH("*** Unable to send message to wake up port. (%d) ***", ret);
#elif DEPLOYMENT_TARGET_WINDOWS
SetEvent(rl->_wakeUpPort);
#endif
// CFRunLoopRef 解锁
__CFRunLoopUnlock(rl);
}
如此,主线程通过调用 CFRunLoopWakeUp(rl) 来唤醒子线程的 run loop,那么添加到子线程中的标记为待处理的 source0 就能得到执行了。
Cocoa Foundation 和 Core Foundation 为使用与端口相关的对象和函数创建基于端口的输入源(source1)提供内置支持。例如,在 Cocoa Foundation 中,我们根本不需要直接创建 source1,只需创建一个端口对象,并使用 NSRunLoop 的实例方法将该端口添加到 run loop 中。port 对象会处理所需 source1 的创建和配置。如下代码在子线程中:
NSPort *port = [NSPort port];
[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
即可在当前 run loop 的 NSDefaultRunLoopMode 模式的 sources1 集合中添加一个 source1,此时只要在主线程中能拿到 port 我们就可以实现主线和子线的通信(唤醒子线程)。
在上面示例代码中打一个断点,然后在控制台执行 po [NSRunLoop currentRunLoop],可看到 kCFRunLoopDefaultMode 模式的 sources1 哈希表中多了一个 source1:
...
sources1 = <CFBasicHash 0x28148ebe0 [0x20e729430]>{type = mutable set, count = 1,
entries =>
2 : <CFRunLoopSource 0x282fd9980 [0x20e729430]>{signalled = No, valid = Yes, order = 200, context = <CFMachPort 0x282ddca50 [0x20e729430]>{valid = Yes, port = a20b, source = 0x282fd9980, callout = __NSFireMachPort (0x1df1ee1f0), context = <CFMachPort context 0x28148ec70>}}
}
...
在 Core Foundation 中则必须手动创建端口及其 source1。在这两种情况下,都使用与端口不透明类型(CFMachPortRef、CFMessagePortRef 或 CFSocketRef)相关联的函数来创建适当的对象。
39. run loop 与事件处理。
用户触发事件, IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收,SpringBoard 会利用 mach port,产生 source1,来唤醒目标 APP 的 com.apple.uikit.eventfetch-thread 的 RunLoop。Eventfetch thread 会将 main runloop 中 __handleEventQueue 所对应的 source0 设置为 signalled == Yes 状态,同时唤醒 main RunLoop。mainRunLoop 则调用 __handleEventQueue 进行事件队列处理。
接下来我们顺着刚刚的事件响应的过程再细化一个分支。我们当前的 App 进程接收到事件以后(SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程),会调用 __eventFetcherSourceCallback 和 __eventQueueSourceCallback 进行应用内部分发,此时会对事件做一个细化,会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
40. run loop 与手势识别。
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop 即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
41. run loop 与界面刷新。
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出 Loop) 事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
这个函数内部的调用栈大概是这样的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
在控制台打印 main run loop,在其 kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes 模式下都有同一个回调函数是 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 的 CFRunLoopObserver,其 activities 值为 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit),表示只监听 main run loop 的休眠前和退出状态。其 order = 2000000 比上面的手势识别的 order = 0 的 CFRunLoopObserver 的优先级要低。
当我们需要界面刷新,如 UIView/CALayer 调用了 setNeedsLayout/setNeedsDisplay,或更新了 UIView 的 frame,或 UI 层次。 其实,系统并不会立刻就开始刷新界面,而是先提交 UI 刷新请求,再等到下一次 main run loop 循环时,集中处理(集中处理的好处在于可以合并一些重复或矛盾的 UI 刷新)。而这个实现方式,则是通过监听 main run loop 的 before waitting 和 Exit 通知实现的。
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 内部会调用 CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) 在该函数中,会将所有的界面刷新请求提交,刷新界面,以及调用相关回调。
42. run loop 与卡顿监测。
卡顿的呈现方式大概可以理解为我们触摸屏幕时系统回馈不及时或者连续滑动屏幕时肉眼可见的掉帧,回归到程序层面的话可知这些感知的来源都是主线程,而分析有没有卡顿发生则可以从主线程的 run loop 入手,可以通过监听 main run loop 的活动变化,从而发现主线程的调用方法堆栈中是否某些方法执行时间过长而导致了 run loop 循环周期被拉长继而发生了卡顿,所以监测卡顿的方案是:通过监控 main run loop 从 kCFRunLoopBeforeSources(或者 kCFRunLoopBeforeTimers) 到 kCFRunLoopAfterWaiting 的活动变化所用时间是否超过了我们预定的阈值进而判断是否出现了卡顿,当出现卡顿时可以读出当前函数调用堆栈帮助我们来分析代码问题。
首先给 main run loop 添加一个 CFRunLoopObserverRef runLoopObserver 来帮助我们监听主线程的活动状态变化,然后创建一条子线程在子线程里面用一个死循环 while(YES) 来等待着主线程的状态变化,等待的方式是在子线程的 while 循环内部用 long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
函数,它的 timeout 参数刚好可以设置一个我们想要观察的 main run loop 的不同的活动状态变化之间的时间长度,当 dispatch_semaphore_wait 函数返回非 0 值时表示等待的时间超过了 timeout,所以我们只需要关注 dispatch_semaphore_wait 函数返回非 0 值的情况。我们使用 HCCMonitor 的单例对象在 runLoopObserver 的回调函数和子线程之间进行 "传值",当 runLoopObserver 的回调函数执行时我们调用 dispatch_semaphore_signal 函数结束子线程 while 循环中的 dispatch_semaphore_wait 等待,同时使用单例对象的 runLoopActivity 成员变量记录 main run loop 本次变化的活动状态值,然后如果子线程的 while 循环中连续三次出现 kCFRunLoopBeforeSources 或者 kCFRunLoopAfterWaiting 状态变化等待超时了,那么就可认为是主线程卡顿了。
监测 kCFRunLoopBeforeSources 或者 kCFRunLoopAfterWaiting 两个活动状态变化,即一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿。
在 run loop 的本次循环中,从 kCFRunLoopBeforeSources 到 kCFRunLoopBeforeWaiting 处理了 source/timer/block 的事情,如果时间花的太长必然导致主线程卡顿。从 kCFRunLoopBeforeWaiting 到 kCFRunLoopAfterWaiting 状态,如果本次唤醒花了太多时间也会必然造成卡顿。
43. run loop 与 GCD。
在 Run Loop 和 GCD 的底层双方各自都会相互用到对方。首先我们先看一下读 run loop 源码的过程中用到 GCD 的地方,前面我们学习 GCD 的时候已知使用 dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue)
可以构建计时器,且它比 NSTimer 的精度更高。
在 run loop 中有两个地方用到了 dispatch_source,一是在 run loop mode 中有一个与 _timerPort(mk_timer_create)对应的 _timerSource,它们两个的作用是相同的,都是用来当到达 run loop mode 的 _timers 数组中最近的某个计时器的触发时间时用来唤醒当前 run loop 的,然后还有一个地方是在 __CFRunLoopRun 函数中直接使用 dispatch_source 构建一个计时器用来为入参 run loop 的运行时间计时的,当入参 run loop 运行超时时此计时器便会触发。
在 __CFRunLoopRun 函数中我们看到所有平台下都是使用 dispatch_source 来构建计时器为 run loop 的运行时间而计时的。
(一个题外话:看到这里我们似乎可以得到一些理解和启发,CFRunLoopTimerRef 虽一直被我们称为计时器,但其实它的触发执行是完全依赖 run loop mode 中的 _timerPort 或者 _timerSource 来唤醒当前 run loop,然后在当前 run loop 的本次循环中判断本次 run loop 被唤醒的来源,如果是因为 timer ,则执行某个 CFRunLoopTimerRef 的回调事件并更新最近的下次执行时间,所以这里 CFRunLoopTimerRef 虽被称为计时器其实它的计时部分是依靠别人来做的,它本身并不具备计时功能,只是有一个值记录自己的下次触发时间而已。)
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 run loop 发送消息,run loop 会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。为什么子线程没有这个和 GCD 交互的逻辑?原因有二:
- 主线程 run loop 是主线程的事件管理者。run loop 负责何时让 run loop 处理何种事件。所有分发给主线程的任务必须统一交给主线程 run loop 排队处理。举例:UI 操作只能在主线程,不在主线程操作 UI 会带来很多 UI 错乱问题以及 UI 更新延迟问题。
- 子线程不接受 GCD 的交互。因为子线程不一定开启了 run loop。
上面一段结论我们在梳理 __CFRunLoopRun 函数流程时已经看的一清二楚了。如函数开始时判断当前是否是主线程来获取主队列的 port 并赋值给 dispatchPort,然后在 run loop 本次循环中判断唤醒来源是 dispatchPort 时,执行添加到主队列中的任务(_dispatch_main_queue_drain)。
...
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
// TSD 给 __CFTSDKeyIsInGCDMainQ 置为 6 和 下面的置 0 对应,可以理解为一个加锁行为!
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
void *msg = 0;
#endif
// 内部是调用 static void _dispatch_main_queue_drain(dispatch_queue_main_t dq) 函数,即处理主队列中的任务
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
}
...
44. run loop 与 FPS。
FPS(Frames Per Second)是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。FPS 是测量用于保存、显示动态视频的信息数量,每秒钟帧数越多,所显示的动作就会越流畅,iPhone 屏幕最大帧率是每秒 60 帧,一般我们的 APP 的 FPS 恒定的保持在 50-60 之间,用户滑动体验都是比较流畅的。关于屏幕卡顿的一些原因可以参考:iOS 保持界面流畅的技巧
YYKit 下的 YYFPSLabel 提供了一种监测 FPS 的方案,实现原理是把一个 CADisplayLink 对象添加到主线程的 run loop 的 NSRunLoopCommonModes 模式下,然后在 CADisplayLink 对象的回调函数中统计每秒钟屏幕的刷新次数。
45. 介绍 CADisplayLink。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动 TableView 时,即使一帧的卡顿也会让用户有所察觉。
CADisplayLink 表示一个绑定到显示 vsync 的计时器的类。(其中 CA 表示的是 Core Animation(核心动画) 首字母缩写,CoreAnimation.h 是 QuartzCore 框架中的一个包含 QuartzCore 框架所有头文件的文件)
/** Class representing a timer bound to the display vsync. **/
API_AVAILABLE(ios(3.1), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos)
@interface CADisplayLink : NSObject {
@private
void *_impl;
}
我们创建一个 CADisplayLink 对象并添加到当前线程的主线程中。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)displayLinkAction:(CADisplayLink *)displayLink {
NSLog(@"📧📧 %@", displayLink);
NSLog(@"duration: %lf timestamp: %lf targetTimestamp: %lf frameInterval: %d preferredFramesPerSecond: %d maximumFramesPerSecond: %d", displayLink.duration, displayLink.timestamp, displayLink.targetTimestamp, displayLink.frameInterval, displayLink.preferredFramesPerSecond, UIScreen.mainScreen.maximumFramesPerSecond);
}
// 控制台打印:
📧📧 <CADisplayLink: 0x6000008ec2c0>
duration: 0.016667 timestamp: 366093.060335 targetTimestamp: 366093.077002 frameInterval: 1 preferredFramesPerSecond: 0 maximumFramesPerSecond: 60
直接打印 CADisplayLink 对象的各个属性,可看到 duration 是我们熟悉的 0.016667 秒(16.7 毫秒),targetTimestamp - timestamp 约等于 16.7 毫秒,preferredFramesPerSecond 的值是 0,实际是屏幕的最大刷新率每秒 60 帧,iPhone 下 maximumFramesPerSecond 是 60。
在上面的 displayLinkAction 函数内打一个断点,进入断点后打印当前函数调用堆栈:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001007b3b1e Simple_iOS`-[ViewController displayLinkAction:](self=0x00007fc4ab601df0, _cmd="displayLinkAction:", displayLink=0x00006000013cc090) at ViewController.m:382:27
frame #1: 0x00007fff2afeb266 QuartzCore`CA::Display::DisplayLink::dispatch_items(unsigned long long, unsigned long long, unsigned long long) + 640
frame #2: 0x00007fff2b0c3e03 QuartzCore`display_timer_callback(__CFMachPort*, void*, long, void*) + 299
frame #3: 0x00007fff23b9503d CoreFoundation`__CFMachPortPerform + 157
frame #4: 0x00007fff23bd4bc9 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 41 // ⬅️ 看到是执行 source1 的回调函数
frame #5: 0x00007fff23bd4228 CoreFoundation`__CFRunLoopDoSource1 + 472
frame #6: 0x00007fff23bced64 CoreFoundation`__CFRunLoopRun + 2516
frame #7: 0x00007fff23bce066 CoreFoundation`CFRunLoopRunSpecific + 438
frame #8: 0x00007fff384c0bb0 GraphicsServices`GSEventRunModal + 65
frame #9: 0x00007fff48092d4d UIKitCore`UIApplicationMain + 1621
frame #10: 0x00000001007b486d Simple_iOS`main(argc=1, argv=0x00007ffeef44bd60) at main.m:76:12
frame #11: 0x00007fff5227ec25 libdyld.dylib`start + 1
(lldb)
看到 CADisplayLink 的回调函数是通过 source1 的回调来执行的。然后打印当前线程的 run loop 可看到创建了一个回调函数是 _ZL22display_timer_callbackP12__CFMachPortPvlS1_ 的 source1。
...
0 : <CFRunLoopSource 0x600003b11140 [0x7fff80617cb0]>{signalled = No, valid = Yes, order = -1, context = <CFMachPort 0x6000039146e0 [0x7fff80617cb0]>{valid = Yes, port = 6507, source = 0x600003b11140, callout = _ZL22display_timer_callbackP12__CFMachPortPvlS1_ (0x7fff2b0c3cd8), context = <CFMachPort context 0x6000035200d0>}}
...
通过以上可知 CADisplayLink 的内部是 source1 来驱动的。
46. YYFPSLabel 是如何监测帧率的。
#import "YYFPSLabel.h"
//#import <YYKit/YYKit.h>
#import "YYText.h"
#import "YYWeakProxy.h"
#define kSize CGSizeMake(55, 20)
@implementation YYFPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
UIFont *_font;
UIFont *_subFont;
NSTimeInterval _llll;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (frame.size.width == 0 && frame.size.height == 0) {
frame.size = kSize;
}
self = [super initWithFrame:frame];
self.layer.cornerRadius = 5;
self.clipsToBounds = YES;
self.textAlignment = NSTextAlignmentCenter;
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
_font = [UIFont fontWithName:@"Menlo" size:14];
if (_font) {
_subFont = [UIFont fontWithName:@"Menlo" size:4];
} else {
_font = [UIFont fontWithName:@"Courier" size:14];
_subFont = [UIFont fontWithName:@"Courier" size:4];
}
// 创建一个 CADisplayLink 对象添加到 main run loop 的 NSRunLoopCommonModes 模式下。
// 因为 CADisplayLink 对象会 retain target,所以这里用了一个 [YYWeakProxy proxyWithTarget:self] 做中间的桥梁,
// self 赋值给 YYWeakProxy 对象的 weak 属性 _target,即 self 被 YYWeakProxy 对象弱引用,
// 并重写 YYWeakProxy 的 forwardingTargetForSelector: 函数,直接返回 _target 对象来接收处理发送给 YYWeakProxy 的消息,
// 即把 CADisplayLink 的回调函数 tick: 转移到 YYFPSLabel 类来处理。
//(self 持有 _link、_link 持有 YYWeakProxy、YYWeakProxy 弱引用 self,这样就破开了原有的引用循环)
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
return self;
}
- (void)dealloc {
// 销毁时一定要调用 CADisplayLink 的 invalidate 函数
[_link invalidate];
}
- (CGSize)sizeThatFits:(CGSize)size {
return kSize;
}
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) {
// 初次调用 tick 函数时,_lastTime 记录第一帧的时间戳
_lastTime = link.timestamp;
return;
}
// 统计 tick 被调用的次数
_count++;
// link.timestamp 是当前帧的时间戳,减去上一次统计帧率的时间戳,当时间间隔大于等于 1 秒时才进行帧率统计,
// 即 1 秒钟统计一次帧率(也没必要过于频繁的统计帧率)
NSTimeInterval delta = link.timestamp - _lastTime;
// 时间大于等于 1 秒钟计算一次帧率,刷新一次 YYFPSLabel 显示的帧率值
if (delta < 1) return;
// 更新 _lastTime 为当前帧的时间戳
_lastTime = link.timestamp;
// tick 被调用的次数除以时间间隔,即为当前的帧率
float fps = _count / delta;
// tick 被调用的次数清 0(开始下一轮帧率统计)
_count = 0;
CGFloat progress = fps / 60.0;
UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
[text yy_setColor:color range:NSMakeRange(0, text.length - 3)];
[text yy_setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
text.yy_font = _font;
[text yy_setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
self.attributedText = text;
}
@end
tick: 函数内部借助 CADisplayLink 对象中记录的每一帧的时间戳来统计出每秒钟的帧率,足够我们日常开发中监测滑动帧率。
47. 属性修饰符的作用。
属性(@property)的本质是编译器自动帮我们生成: _Ivar
+ setter
+ getter
,而属性修饰符则正作用于各个属性的 setter
和 getter
函数。
我们创建如下 Person 类,并声明不同修饰符的属性。
// Person.h 如下声明,Person.m 文件什么也不写
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, strong) NSObject *objc_nonatomic_strong;
@property (nonatomic, retain) NSObject *objc_nonatomic_retain;
@property (nonatomic, copy) NSObject *objc_nonatomic_copy;
@property (nonatomic, weak) NSObject *objc_nonatomic_weak;
@property (nonatomic, unsafe_unretained) NSObject *objc_nonatomic_unsafe_unretained;
@property (nonatomic, assign) NSObject *objc_nonatomic_assign;
// readonly 修饰的属性,编译器仅自动生成 getter 函数
@property (nonatomic, strong, readonly) NSObject *objc_nonatomic_strong_readonly;
@property (atomic, strong) NSObject *objc_atomic_strong;
@property (atomic, retain) NSObject *objc_atomic_retain;
@property (atomic, copy) NSObject *objc_atomic_copy;
@property (atomic, weak) NSObject *objc_atomic_weak;
@property (atomic, unsafe_unretained) NSObject *objc_atomic_unsafe_unretained;
@property (atomic, assign) NSObject *objc_atomic_assign;
@end
NS_ASSUME_NONNULL_END
选择真机运行模式,保证编译出的是 ARM
下的汇编指令,(x86
的看不太懂)然后在 xcode
左侧用鼠标选中 Person.m
文件,通过 xcode
菜单栏 Product -> Perform Action -> Assemble "Person.m"
生成汇编指令,可以看到我们的所有属性所对应的 setter getter
方法的汇编实现。
atomic
仅仅是对读写属性时加锁,在多线程下对属性进行复合运算的话还是需要我们自行加锁保证线程安全。加锁的过程如下 objc_getProperty
函数中从全局的属性锁列表(PropertyLocks
)内取得一把锁,进行加锁和解锁。
objc_nonatomic_strong
属性的 getter
函数内部没有调用任何函数,只是地址偏移取值。
objc_nonatomic_strong
属性的 setter
函数内部看到 bl
指令跳转到 objc_storeStrong
函数,它实现的事情就是 retain
新值,release
旧值。
void
objc_storeStrong(id *location, id obj)
{
// 1. 取出属性原本指向的旧值
id prev = *location;
// 2. 如果旧值和入参传入的新值相同,就没有赋值的必要了,直接 return
if (obj == prev) {
return;
}
// 3. 先 retain 新值 obj,obj 的引用计数 +1
objc_retain(obj);
// 4. 把属性指向新值
*location = obj;
// 5. 释放旧值
objc_release(prev);
}
objc_nonatomic_retain
属性的 setter
和 getter
函数和 objc_nonatomic_strong
一致。
objc_nonatomic_copy
属性的 getter
函数内部看到最后 b
指令跳转到了 objc_getProperty
函数。
// ptrdiff_t offset
// ptrdiff_t 是 C/C++ 标准库中定义的一个与机器相关的数据类型。
// ptrdiff_t 类型变量通常用来保存两个指针减法操作的结果。
// ptrdiff_t 类型则应保证足以存放同一数组中两个指针之间的差距,它有可能是负数。
// offset 是成员变量距离对象起始地址的偏移量。
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// 如果 offset 为 0,则返回该对象的所属的类对象的地址
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
// self 指针偏移找到成员变量
id *slot = (id*) ((char*)self + offset);
// 如果 atomic 为 false 则直接返回成员变量
if (!atomic) return *slot;
// Atomic retain release world
// 从全局的属性锁列表内取得锁
spinlock_t& slotlock = PropertyLocks[slot];
// 加锁
slotlock.lock();
// retain
id value = objc_retain(*slot);
// 解锁
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
// 把 value 放进自动释放池,是为了它的引用计数与上面的 retain 操作保持平衡
return objc_autoreleaseReturnValue(value);
}
如果属性不是 atomic
修饰的话不需要对读取过程加锁,objc_getProperty
函数的前半部分就已经 return
成员变量了,成员变量依然是通过 self
指针偏移找到并返回。如果属性是 atomic
修饰的话,会通过 PropertyLocks[slot]
取得一把锁,而加锁的内容是 id value = objc_retain(*slot)
对成员变量执行一次 retain
操作引用计数 +1
(保证 getter 函数执行过程中对象不会被释放),然后为了性能,在解锁后才调用 objc_autoreleaseReturnValue(value)
把成员变量放进自动释放池,保证和刚刚的 retain
操作抵消,保证最终成员变量能正常释放销毁。
objc_nonatomic_copy
属性的 setter
函数内部看到 bl
指令跳转到了 objc_setProperty_nonatomic_copy
函数,内部是对入参新值进行 copy
操作。下面我们来看一下 objc_setProperty_nonatomic_copy
函数实现。
void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
// 直接调用 reallySetProperty 函数
// 且后面三个实参分别表示:
// atomic: false
// copy: true
// mutableCopy: false
reallySetProperty(self, _cmd, newValue, offset, false, true, false);
}
// atomic: false
// copy: true
// mutableCopy: false
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
// 如果 offset 为 0,则调用 changeIsa 修改对象的 isa
if (offset == 0) {
object_setClass(self, newValue);
return;
}
// 用于记录旧值的临时变量,主要是在最后对旧值进行 release 操作,释放旧值
id oldValue;
// 根据 offset 取得对象当前要进行 setter 的成员变量(旧值)
id *slot = (id*) ((char*)self + offset);
if (copy) {
// 如果是 copy 的话,对 newValue 执行一次 copy 操作,这里直接把 copy 结果赋值给 newValue
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
// 如果是 mutableCopy 的话,对 newValue 执行一次 mutableCopy 操作,这里直接把 mutableCopy 结果赋值给 newValue
newValue = [newValue mutableCopyWithZone:nil];
} else {
// 如果旧值和新值相同的话,则直接 return
if (*slot == newValue) return;
// retain 新值
newValue = objc_retain(newValue);
}
if (!atomic) {
// 如果不是 atomic 的话,不需要加锁,把 *slot 赋值给 oldValue,会在函数末尾释放 oldValue
oldValue = *slot;
// 把新值赋值给对象的成员变量
*slot = newValue;
} else {
// 如果是 atomic 的话,则对赋值的过程进行加锁,看到 atomic 修饰的属性只是对新值旧值赋值的过程进行了加锁,和 nonatomic 不加锁相比,这个加锁的操作就是性能损耗的来源。
// 看到这里我们也发现了,atomic 只是对 setter 和 getter 加锁,只能保证 setter 和 getter 是线程安全的,但是我们日常开发中几乎都是复合操作,
// 如 self.a = self.a + 1;
// 此操作包含 getter、setter、加操作,atomic 只是给单个 getter、setter 操作加锁了,无法保证这种复合操作的线程安全,如果要实现线程安全需要额外加锁。
// 如下伪代码:
// lock.lock();
// self.a = self.a + 1;
// lock.unlock();
// 从全局的属性锁列表内取得锁
spinlock_t& slotlock = PropertyLocks[slot];
// 加锁
slotlock.lock();
// 把 *slot 赋值给 oldValue,会在函数末尾释放 oldValue
oldValue = *slot;
// 把新值赋值给对象的成员变量
*slot = newValue;
// 解锁
slotlock.unlock();
}
// 释放旧值
objc_release(oldValue);
}
objc_nonatomic_weak
属性的 getter
函数内部看到 bl
指令跳转到 objc_loadWeakRetained
函数,在结尾处 b
指令跳转到 objc_autoreleaseReturnValue
,即我们熟悉的 objc_loadWeak
函数。retain
和 autorelease
的配对使用,防止读值过程中对象释放,同时自动释放池的延迟释放也能保证对象的正常销毁。
id
objc_loadWeak(id *location)
{
if (!*location) return nil;
return objc_autorelease(objc_loadWeakRetained(location));
}
objc_nonatomic_weak
属性的 setter
函数内部看到 bl
指令跳转到了 objc_storeWeak
函数。即 weak
修饰对属性读取调用 objc_loadWeak
赋值调用 objc_storeWeak
函数。
objc_nonatomic_unsafe_unretained
属性的 getter
函数和 objc_nonatomic_strong
属性的 getter
函数 一样,内部没有调用任何函数,只是地址偏移取值。
objc_nonatomic_unsafe_unretained
属性的 setter
函数看到内部没有调用任何其它函数,就是纯粹的入参、地址偏移、存储入参到成员变量的位置。这里也验证了 unsafe_unretained
的 setter
的本质,即不 retain
新值也不 release
旧值。setter
和 getter
函数都是简单的根据地址存入值和读取值。所以这里也引出另一个问题,赋值给 unsafe_unretained
属性的对象并不会被 unsafe_unretained
属性所持有,那么当此对象正常释放销毁以后,也并没有把 unsafe_unretained
属性置为 nil
,此时我们如果再用 unsafe_unretained
属性根据地址读取对象,会直接引发野指针访问导致 crash
。
objc_nonatomic_assign
属性的 setter
和 getter
函数和 objc_nonatomic_unsafe_unretained
属性如出一辙。
objc_nonatomic_strong_readonly
属性只生成了 getter
函数,也符合我们的预期。
下面是 atomic
修饰对属性。
// getter
...
...
// 零寄存器的值和 0x1 做或操作,并把结果存入 w3,表示 w3 = 1,同时表示下面调用 objc_getProperty 函数是第 4 个参数 BOOL atomic 是 true
// x0 - x7 寄存器保存函数参数
orr w3, wzr, #0x1
...
b _objc_getProperty
...
// setter
...
bl _objc_setProperty_atomic
...
void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
// atomic 参数使用的是 true
reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
objc_atomic_strong
属性在 setter
和 getter
函数中都加了锁。
objc_atomic_retain
属性 和 objc_atomic_strong
属性的 setter
和 getter
函数如出一辙。
objc_atomic_copy
属性的 setter
和 getter
同样也是进行加锁。
// getter
...
orr w3, wzr, #0x1 // 第 4 个参数 BOOL atomic 是 true
...
b _objc_getProperty
...
// setter
bl _objc_setProperty_atomic_copy
void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
// atomic 值使用的是 true
reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}
objc_atomic_weak
、objc_atomic_unsafe_unretained
、objc_atomic_assign
和对应的 nonatomic
修饰的属性的 setter
getter
函数相同。