博主前期通读了 Apple 的五份源码 objc4-781、libdispatch-1173.40.5、CF-1151.16、libmalloc-283.100.6、libclosure-74 基本对 iOS 的大部分底层原理都有了一个基础的认知,然后算法部分的话是专注刷了两遍 《剑指 Offer》(在 IDE 里可以完成默写,完全手写的话可能还需要一些练习)。那么既然是面试肯定免不了要刷题,题目的话就从网络搜集各位大佬面试时的题目以及本人面试时被问到的题目,然后试着从自己的理解上给题目作出解答,如有错误的地方还望大家进行指正。
21. dispatch_semaphore 的实现原理。
dispatch_semaphore 是 GCD 中提供的一个很常用的操作,通常用于保证资源的多线程安全性和控制任务的并发数量。其本质实际上是基于 mach 内核的信号量接口来实现的。
dispatch_semaphore_t
是指向 dispatch_semaphore_s
结构体的指针。首先看一下基础的数据结构。
struct dispatch_queue_s;
DISPATCH_CLASS_DECL(semaphore, OBJECT);
struct dispatch_semaphore_s {
DISPATCH_OBJECT_HEADER(semaphore);
// 可看到上半部分的宏定义和其它的 GCD 类是相同的,毕竟大家都是继承自 dispatch_object_s,重点是下面两个新的成员变量,
// dsema_value 和 dsema_orig 是信号量执行任务的关键,执行一次 dispatch_semaphore_wait 操作,dsema_value 的值就做一次减操作。
long volatile dsema_value;
long dsema_orig;
_dispatch_sema4_t dsema_sema;
};
dispatch_semaphore_s
结构体中:dsema_orig
是信号量的初始值,dsema_value
是信号量的当前值,信号量的相关 API 正是通过操作 dsema_value
来实现其功能的。
dispatch_semaphore_create
用初始值(long value
)创建新的计数信号量。当两个线程需要协调特定事件的完成时,将值传递为零非常有用。传递大于零的值对于管理有限的资源池非常有用,该资源池的大小等于该值(例如我们有多个文件要从服务器下载下来,然后用 dispatch_semaphore 限制只能并发五条线程(dispatch_semaphore_create(5)
)进行下载)。
参数 value
:信号量的起始值,传递小于零的值将导致返回 NULL
。返回值 result
:新创建的信号量,失败时为 NULL
。
dispatch_semaphore_wait
等待(减少)信号量。
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
{
// 原子操作 dsema 的成员变量 dsema_value 的值减 1
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
// 如果减 1 后仍然大于等于 0,则直接 return
if (likely(value >= 0)) {
return 0;
}
// 如果小于 0,则调用 _dispatch_semaphore_wait_slow 函数进行阻塞等待
return _dispatch_semaphore_wait_slow(dsema, timeout);
}
减少计数信号量,如果结果值小于零,此函数将等待信号出现,然后返回。(可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。)dsema
:信号量,在此参数中传递 NULL
的结果是未定义的。timeout
:何时超时(dispatch_time),为方便起见,有 DISPATCH_TIME_NOW
和 DISPATCH_TIME_FOREVER
常量。函数返回值 result
,成功返回零,如果发生超时则返回非零(_DSEMA4_TIMEOUT
)。
当 timeout
是 DISPATCH_TIME_FOREVER
时,do while 循环一直等下去,直到 sema
的值被修改为不等于 KERN_ABORTED
。
void
_dispatch_sema4_wait(_dispatch_sema4_t *sema)
{
kern_return_t kr;
do {
kr = semaphore_wait(*sema);
} while (kr == KERN_ABORTED);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
}
其中调用了 mach 内核的信号量接口 semaphore_wait
和 semaphore_timedwait
进行 wait 操作。所以,GCD 的信号量实际上是基于 mach 内核的信号量接口来实现。semaphore_timedwait
函数即可以指定超时时间。
dispatch_semaphore_signal
发信号(增加)信号量。如果先前的值小于零,则此函数在返回之前唤醒等待的线程。如果线程被唤醒,此函数将返回非零值。否则,返回零。
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
// 原子操作 dsema 的成员变量 dsema_value 的值加 1
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
// 如果 value 大于 0 表示目前没有线程需要唤醒,直接 return 0
return 0;
}
// 如果过度释放,导致 value 的值一直增加到 LONG_MIN(溢出),则 crash
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value, "Unbalanced call to dispatch_semaphore_signal()");
}
// value 小于等于 0 时,表示目前有线程需要唤醒
return _dispatch_semaphore_signal_slow(dsema);
}
_dispatch_semaphore_signal_slow
内部调用 _dispatch_sema4_signal(&dsema->dsema_sema, 1)
唤醒一条线程。
DISPATCH_NOINLINE
long _dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema) {
_dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
// count 传 1,唤醒一条线程
_dispatch_sema4_signal(&dsema->dsema_sema, 1);
return 1;
}
semaphore_signal
能够唤醒一个在 semaphore_wait
中等待的线程。如果有多个等待线程,则根据线程优先级来唤醒。
void
_dispatch_sema4_signal(_dispatch_sema4_t *sema, long count)
{
do {
// semaphore_signal 唤醒线程
kern_return_t kr = semaphore_signal(*sema);
DISPATCH_SEMAPHORE_VERIFY_KR(kr);
} while (--count);
}
22. dispatch_group 的实现原理。
dispatch_group 可以将一组 GCD 任务关联到一起,可以监听这一组所有任务的执行情况,当所有任务异步执行完毕后我们可以得到一个或多个回调通知(使用 dispatch_group_notify
添加几个就能执行几个回调通知)。(dispatch_group 不持有与它相关的任务 block,但是会通过链表的形式持有 dispatch_group_notify
函数添加的回调通知 block)
dispatch_group_s
定义和 dispatch_semaphore_s
定义都是放在 semaphore_internal.h 文件中,而且该文件中仅包含它俩的内容,其实文件这样布局也是有用意的,因为它俩的内部实现有一些相似性,dispatch_group 在内部也会维护一个值,当调用 dispatch_group_enter
函数进行进组操作时(dg_bits
- 0x0000000000000004ULL
),当调用 dispatch_group_leave
函数进行出组操作时(dg_state
+ 0x0000000000000004ULL
)时对该值进行操作(这里可以把 dg_bits
和 dg_state
理解为一个值),当该值达到临界值 0 时会做一些后续操作(_dispatch_group_wake
唤醒异步执行 dispatch_group_notify
函数添加的所有回调通知),且在使用过程中一定要谨记进组(enter)和出组(leave)必须保持平衡。
假设与 dispatch_group 关联的 GCD 任务是一个 block,dispatch_group 并不持有此 block,甚至 dispatch_group 与此 block 没有任何关系,dispatch_group 内部的那个值只是与 enter/leave 操作有关,GCD 任务只是借用了此值,例如在创建多个 GCD 异步任务之前调用多次 enter 操作,然后在每个 GCD 任务结束时调用 leave 操作,当这多个 GCD 异步任务都执行完毕,那么如果 dispatch_group 添加了回调通知,此时自会收到回调通知。即使我们使用 dispatch_group_async 创建多个 GCD 异步任务 block, 这些 GCD 任务 block 其实与 dispatch_group 也没有任何直接的关系。
那么与这些 GCD 异步任务相比的话,我们使用 dispatch_group_notify
函数添加的多个回调通知的 block 则是被 dispatch_group 所完全拥有的,这些回调通知 block 会链接成一个链表,而 dispatch_group 实例则直接拥有此链表的头节点和尾节点。
dispatch_group_t
是指向 dispatch_group_s
结构体的指针,dispatch_group_s
结构体的定义如下。
DISPATCH_CLASS_DECL(group, OBJECT);
struct dispatch_group_s {
DISPATCH_OBJECT_HEADER(group);
// 可看到上半部分和其它 GCD 对象都是相同的,毕竟大家都是继承自 dispatch_object_s,重点是下面的新内容
union {
uint64_t volatile dg_state; // leave 时加 DISPATCH_GROUP_VALUE_INTERVAL
struct {
uint32_t dg_bits; // enter 时减 DISPATCH_GROUP_VALUE_INTERVAL
// 主要用于 dispatch_group_wait 函数被调用后,
// 当 dispath_group 处于 wait 状态时,结束等待的条件有两条:
// 1): 当 dispatch_group 关联的 block 都执行完毕后,wait 状态结束
// 2): 当到达了指定的等待时间后,即使关联的 block 没有执行完成,也结束 wait 状态
// 而当 dg_gen 不为 0 时,说明 dg_state 发生了进位,可表示 dispatch_group 关联的 block 都执行完毕了,
// 如果 dispatch_group 此时处于 wait 状态的话就可以结束了,此时正对应上面结束 wait 状态的条件 1 中。
uint32_t dg_gen;
};
} __attribute__((aligned(8)));
// 下面两个成员变量比较特殊,它们分别是一个链表的头节点指针和尾节点指针
// 调用 dispatch_group_notify 函数可添加当 dispatch_group 关联的 block 异步执行完成后的回调通知,
// 多次调用 dispatch_group_notify 函数可添加多个回调事件(我们日常开发一般就用了一个回调事件,可能会忽略这个细节),
// 而这些多个回调事件则会构成一个 dispatch_continuation_s 作为节点的链表,当 dispatch_group 中关联的 block 全部执行完成后,
// 此链表中的 dispatch_continuation_s 都会得到异步执行。
//(注意是异步,具体在哪个队列则根据 dispatch_group_notify 函数的入参决定,以及执行的优先级则根据队列的优先级决定)。
struct dispatch_continuation_s *volatile dg_notify_head; // dispatch_continuation_s 链表的头部节点
struct dispatch_continuation_s *volatile dg_notify_tail; // dispatch_continuation_s 链表的尾部节点
};
dg_bits
和 dg_state
是联合体共享同一块内存空间的不同名的成员变量,进组和出组时减少和增加 DISPATCH_GROUP_VALUE_INTERVAL
操作的其实是同一个值,再详细一点的话是联合体共占用 64 bit 空间,其中 uint64_t 类型的 dg_state 可占完整 64 bit,然后 uint32_t 类型的 dg_bits
和 uint32_t 类型的 dg_gen
组成结构体共占用这 64 bit,其中 dg_bits
在 低 32 bit,dg_gen
在高 32 bit。
GCD 任务 block 与 dispatch_group 关联的方式:
- 调用
dispatch_group_enter
表示一个 block 与 dispatch_group 关联,同时 block 执行完后要调用dispatch_group_leave
表示解除关联,否则dispatch_group_s
会永远等下去。 - 调用
dispatch_group_async
函数与 block 关联,其实它是在内部封装了一对 enter 和 leave 操作。
在 dispatch_group 进行进组出组操作每次是用加减 4 (DISPATCH_GROUP_VALUE_INTERVAL
)来记录的,并不是常见的加 1 减 1,然后起始值是从 uint32_t 的最小值 0 开始的,这里用了一个无符号数和有符号数的转换的小技巧,例如 dispatch_group 起始状态时 uint32_t 类型的 dg_bits
值为 0,然后第一个 enter 操作进来以后,把 uint32_t 类型的 dg_bits
从 0 减去 4,然后 -4 转换为 uint32_t 类型后值为 4294967292,然后 leave 操作时 dg_bits
加 4,即 4294967292 加 4,这样会使 uint32_t 类型值溢出然后 dg_bits
值就变回 0 了(uint32_t 类型的最小值),对应到 dispatch_group 中的逻辑原理即表示 dg_bits
达到临界值了,表示与组关联的 block 都执行完成了,可以执行后续的唤醒操作了。
还有一点,dg_bits
使用 32 bit 空间对应使用 uint32_t 类型,然后 DISPATCH_GROUP_VALUE_INTERVAL
(间隔)用 4 是因为 uint32_t 类型表示的数字个数刚好是 4 的整数倍吗,不过只要是 2 的幂都是整数倍,且 uint32_t 类型的数字即使以 4 为间隔表示的数字个数也完全足够使用了, 这里的还包括了掩码的使用,4 的二进制表示时后两位是 0,正好可以用来表示两个掩码位,仅后两位是 1 时分别对应 DISPATCH_GROUP_HAS_NOTIFS
(表示 dispatch_group 是否有 notify 回调通知的掩码)和 DISPATCH_GROUP_HAS_WAITERS
(对应 dispatch_group_wait 函数的使用,表示 dispatch_group 是否处于等待状态的掩码)。
dispatch_group_async
将一个 block 提交到指定的调度队列并进行异步调用,并将该 block 与给定的 dispatch_group 关联(其内部自动插入了 dispatch_group_enter
和 dispatch_group_leave
操作,相当于 dispatch_async
和 dispatch_group_enter
、dispatch_group_leave
三个函数的一个封装)。和我们自己手动调用 enter、leave、dispatch_async 相比 dispatch_group_async 轻松了不少,可以让我们更专注于 GCD 任务的编写。
还有一个点这里要注意一下,把入参 block db
封装成 dispatch_continuation_t
dc
的过程中,会把 dc_flags
设置为 DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC
,这里的 DC_FLAG_GROUP_ASYNC
标志关系到 dc
执行的时候调用的具体函数(这里的提交的任务的 block 和 dispatch_group 关联的点就在这里,dc
执行时会调用 _dispatch_continuation_with_group_invoke(dc)
,而我们日常使用的 dispatch_async
函数提交的异步任务的 block 执行的时候调用的是 _dispatch_client_callout(dc->dc_ctxt, dc->dc_func)
函数,它们正是根据 dc_flags
中的 DC_FLAG_GROUP_ASYNC
标识来区分的。
dispatch_group_notify
函数,当与 dispatch_group 相关联的所有 block 都已完成时,计划将 db
提交到队列 dq
(即当与 dispatch_group 相关联的所有 block 都已完成时,notify 添加的回调通知将得到执行)。如果没有 block 与 dispatch_group 相关联,则通知块 db
将立即提交。如下代码中通知块 db
将立即被调用。
dispatch_group_t group = dispatch_group_create();
// dispatch_group_notify 提交的回调 block 立即得到执行
dispatch_group_notify(group, globalQueue, ^{
NSLog(@"🏃♀️ %@", [NSThread currentThread]);
});
// 控制台打印:
🏃♀️ <NSThread: 0x600000fcbe00>{number = 5, name = (null)}
通知块 db
提交到目标队列 dq
时,该 dispatch_group 关联的 block 将为空,或者说只有该 dispatch_group 关联的 block 为空时,通知块 db
才会提交到目标队列 dq
。此时可以通过 dispatch_release
释放 dispatch_group,也可以重新用于其他操作。
dispatch_group_notify
函数不会阻塞当前线程,此函数会立即返回,如果我们想阻塞当前线程,想要等 dispatch_group 中关联的 block 全部执行完成后才执行接下来的操作时,可以使用 dispatch_group_wait
函数并指定具体的等待时间(DISPATCH_TIME_FOREVER
)。
os_atomic_rmw_loop2o
是一个宏定义,内部包裹了一个 do while 循环,直到 old_state == 0 时跳出循环执行 _dispatch_group_wake
函数唤醒执行 notify 链表中的回调通知,即对应我们上文中的 dispatch_group_leave
函数中 dg_bits
的值回到 0 表示 dispatch_group
中关联的 block 都执行完了。
dispatch_group_wait
函数同步等待直到与 dispatch_group 关联的所有 block 都异步执行完成或者直到指定的超时时间过去为止,才会返回。如果没有与 dispatch_group 关联的 block,则此函数将立即返回。从多个线程同时使用同一 dispatch_group 调用此函数的结果是不确定的。成功返回此函数后,dispatch_group 关联的 block 为空,可以使用 dispatch_release
释放 dispatch_group,也可以将其重新用于其它 block。
dispatch_group_wait
函数内部使用同上面的 os_atomic_rmw_loop2o 宏定义,内部是一个 do while 循环,每次循环都从本地原子取值,判断 dispatch_group 所处的状态,是否关联的 block 都异步执行完毕了。
_dispatch_group_wake
把 notify 回调函数链表中的所有的函数提交到指定的队列中异步执行,needs_release
表示是否需要释放所有关联 block 异步执行完成、所有的 notify 回调函数执行完成的 dispatch_group 对象。dg_state
则是 dispatch_group 的状态,包含目前的关联的 block 数量等信息。
23. dispatch_barrier_async 的实现原理。
dispatch_barrier_async
提交 barrier block 以在指定的调度队列上异步执行,同 dispatch_async
函数一样不会阻塞当前线程,此函数会直接返回并执行接下来的函数语句。dispatch_barrier_async
的作用是对添加到同一并发队列中的异步任务作出 “排序”。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"🔞 START: %@", [NSThread currentThread]);
dispatch_async(concurrentQueue, ^{ sleep(3); NSLog(@"🏃♀️ %@", [NSThread currentThread]);}); // ⬅️ 任务一
dispatch_async(concurrentQueue, ^{ sleep(4); NSLog(@"🏃♀️🏃♀️ %@", [NSThread currentThread]);});// ⬅️ 任务二
dispatch_barrier_async(concurrentQueue, ^{ sleep(3); NSLog(@"🚥🚥 %@", [NSThread currentThread]);}); // ⬅️ Barrie 任务
dispatch_async(concurrentQueue, ^{ sleep(3); NSLog(@"🏃♀️🏃♀️🏃♀️ %@", [NSThread currentThread]);}); // ⬅️ 任务三
dispatch_async(concurrentQueue, ^{ sleep(2); NSLog(@"🏃♀️🏃♀️🏃♀️🏃♀️ %@", [NSThread currentThread]);}); // ⬅️ 任务四
NSLog(@"🔞 END: %@", [NSThread currentThread]);
}
首先四个任务都不会阻塞主线程,两条 🔞 的打印会首先执行完毕,然后是任务一和任务二异步并发执行,当它们全部都执行完毕以后,开始异步执行 Barrie 任务,当 Barrie 任务 执行完毕以后,才开始异步并发执行任务三和任务四,这样就像在前两个任务和后两个任务之间插了一道无形的墙,使在 Barrie 任务之前添加的任务和之后添加的任务有了执行顺序,这就是 dispatch_barrier_async
函数的作用,可以在多个异步并发任务之间添加执行顺序。
dq
参数是 Barrier block 提交到的目标调度队列,这里要注意把需要控制异步并发执行顺序的任务都添加到同一个自定义的并发队列 dq
中,同时注意不能使用 dispatch_get_global_queue
API 获取的全局并发队列中(会导致 Barrier 失效,因为全局并发队列是系统创建的,苹果有时候会在全局并发队列中处理它自有任务,使用 barrier 函数阻塞全局并发队列无效),系统将在目标队列上保留引用,直到该 block 执行完成为止。
work
参数是提交到目标调度队列的 block(该函数内部会代表调用者执行 Block_copy
和 Block_release
)。
#ifdef __BLOCKS__
void
dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work)
{
// 取得一个 dispatch_continuation_s 结构体实例,用于封装 work
dispatch_continuation_t dc = _dispatch_continuation_alloc();
// continuation resources are freed on run this is set on async or for non event_handler source handlers
// #define DC_FLAG_CONSUME 0x004ul
// continuation acts as a barrier
// #define DC_FLAG_BARRIER 0x002ul
// DC_FLAG_CONSUME | DC_FLAG_BARRIER = 0x006ul
// dc_flags 中添加 DC_FLAG_BARRIER 标记,标记此 work 是一个屏障 block,然后剩下的内容都和 dispatch_async 完全相同
uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
dispatch_qos_t qos;
// 封装 work block 的内容以及任务执行时所处的队列等内容到 dc 中
qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
// 把封装好的 dispatch_continuation_s 进行异步调用
_dispatch_continuation_async(dq, dc, qos, dc_flags);
}
#endif
看到 dispatch_barrier_async
函数内部和 dispatch_async
相比在 dc_flags
赋值时添加了 DC_FLAG_BARRIER
标记,而此标记正是告知 dispatch_continuation_s
结构体中封装的 block 是一个 barrier block,其它的内容则和 dispatch_async
如出一辙。
一个 dispatch barrier
允许你在一个并行队列中创建一个同步点。当在队列中遇到这个 barrier block
时,这个 barrier block
便会延迟执行(同时所有在其后的 block 都会延迟),直至所有在 barrier 之前的 block 执行完成。这时,这个 barrier block
便会执行,之后队列便恢复正常执行。
调用这个函数总是会在这个 block 被提交后立刻返回,并且不会等到 block 被触发。当这个 barrier block
到达私有并行 队列最前端时,它不是立即执行。恰恰相反,这个队列会一直等待当前正在执行的队列执行完成。此时 barrier block
才会执行。所有 barrier block
之后提交的 block 会等到 barrier block
执行结束后才会执行。
这里你指定的并行队列应该是自己通过 dispatch_queue_cretate
创建的。如果你传的是一个串行或是一个全局的并行队列,那这个函数便等同于 dispatch_async
函数效果了。
24. 介绍 run loop 的概念(run loop 与线程的关系)。
Run loop 是与 thread 关联的基本基础结构的一部分。Run loop 是一个 event processing loop (事件处理循环),可用于计划工作并协调收到的事件的接收。Run loop 的目的是让 thread 在有工作要做时保持忙碌,而在没有工作时让 thread 进入睡眠状态。
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,这种模型通常被称作 Event Loop。实现这种模型的关键点在于基于消息机制:管理事件/消息,让线程在没有消息时休眠以避免资源占用、在有消息到来时立刻被唤醒执行任务。
那什么是 run loop?顾名思义,run loop 就是在 “跑圈”,run loop 运行的核心代码是一个有状态的 do while 循环,每循环一次就相当于跑了一圈,线程就会对当前这一圈里面产生的事件进行处理,并且只要不是超时或者故意退出状态下 run loop 就不会退出,所以可以保证线程不退出,并且可以让我们根据自己需要向线程中添加任务。
那么为什么线程要有 run loop 呢?其实我们的 APP 可以理解为是靠 event 驱动的(包括 iOS 和 Android 应用)。我们触摸屏幕、网络回调等都是一个个的 event,也就是事件。这些事件产生之后会分发给我们的 APP,APP 接收到事件之后分发给对应的线程。通常情况下,如果线程没有 run loop,那么一个线程一次只能执行一个任务,执行完成后线程就会退出。要想 APP 的线程一直能够处理事件或者等待事件(比如异步事件),就要保活线程,也就是不能让线程早早的退出,此时 run loop 就派上用场了,其实也不是必须要给线程指定一个 run loop,如果需要我们的线程能够持续的处理事件,那么就需要给线程绑定一个 run loop,也就是说,run loop 能够保证线程一直可以处理事件。
run loop 与线程的关系:一个线程对应一个 run loop,程序运行是主线程的 main run loop 默认启动了,所以我们的程序才不会退出,子线程的 run loop 按需启动(调用 run 方法)。run loop 是线程的事件管理者,或者说是线程的事件管家,它会按照顺序管理线程要处理的事件,决定哪些事件在什么时候提交给线程处理。(run loop 内部是基于内核基于 mach port 进行工作的)
在开发者文档中查看 UIApplicationMain
函数,摘要告诉我们 UIApplicationMain
函数完成:创建应用程序对象和应用程序代理并设置 event cycle,看到 Return Value 一项 Apple 已经明确告诉我们 UIApplicationMain
函数是不会返回的,并且在 Discussion 中也告诉我们 UIApplicationMain
函数启动了 main run loop 并开始着手为我们处理事件。
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = 0;
do {
// 在睡眠中等待消息
int message = sleep_and_wait();
// 处理消息
retVal = process_message(message);
} while (retVal == 0);
return 0;
}
}
还有一个极隐秘的点。当我们使用 block 时会在 block 外面使用 __weak
修饰符取得一个的 self
的弱引用变量,然后在 block 内部又会使用 __strong
修饰符取得一个的 self 弱引用变量的强引用,首先这里是在 block 内部,当 block 执行完毕后会进行自动释放强引用的 self,这里的目的只是为了保证在 block 执行期间 self 不会被释放,这就默认延长了 self 的生命周期到 block 执行结束,这在我们的日常开发中没有任何问题,但是,但是,但是,放在 run loop 这里是不行的,当我们直接 push 进入 ViewController
然后直接 pop 回上一个页面时,我们要借用 ViewController 的 dealloc 函数来 stop self.commonThread
线程的 run loop 的,如果我们还用 __strong
修饰符取得 self 强引用的话,那么由于 self.commonThread
线程创建时的 block 内部的 run loop 的 runMode:beforeDate:
启动函数是没有返回的,它会一直潜在的延长 self 的生命周期,会直接导致 ViewController
无法释放,dealloc
函数得不到调用(描述的不够清晰,看下面的实例代码应该会一眼看明白的)。
这里是 __weak
和 __strong
配对使用的一些解释,如果对 block 不清晰的话可以参考前面的文章进行学习。
// 下面在并行队列里面要执行的 block 没有 retain self
__weak typeof(self) _self = self;
dispatch_async(globalQueue_DEFAULT, ^{
// 保证在下面的执行过程中 self 不会被释放,执行结束后 self 会执行一次 release。
// 在 ARC 下,这里看似前面的 __wek 和这里的 __strong 相互抵消了,
// 这里的 self 是被 block 截获的 self,
// 这里 __strong 的 self,在出了下面的右边花括号时,会执行一次 release 操作。
// 且只有此 block 执行的时候 _self 有值那么此处的 __strong self 才会有值,
// 否则下面的 if 判断就直接 return 了。
__strong typeof(_self) self = _self;
if (!self) return;
// do something
// ...
dispatch_async(dispatch_get_main_queue(), ^{
// 此时如果能进来,表示此时 self 是存在的
self.view.backgroundColor = [UIColor redColor];
});
});
NSRunLoop 对象处理来自 window system 的鼠标和键盘事件、NSPort 对象和 NSConnection 对象等 sources 的输入。NSRunLoop 对象还处理 NSTimer 事件。
你的应用程序既不创建也不显式管理 NSRunLoop 对象。每个 NSThread 对象(包括应用程序的主线程)都有一个根据需要自动为其创建的 NSRunLoop 对象。如果你需要访问当前线程的 run loop,应使用类方法 currentRunLoop 进行访问。请注意,从 NSRunLoop 的角度来看,NSTimer 对象不是 "input"—而是一种特殊类型,这意味着它们在触发后不会导致 run loop 返回。(如前面的示例代码中,没有被 while 循环包裹的 runMode:beforeDate: 函数,在我们点击一次屏幕后,使其接到一个事件后,run loop 就会退出,而如果只是添加 timer 的话,run loop 则可以一直接收 timer 的回调,并不会退出。)
NSRunLoop 类通常不被认为是线程安全的,其方法只能在当前线程的上下文中调用。永远不要尝试调用在其他线程中运行的 NSRunLoop 对象的方法,因为这样做可能会导致意外结果。
currentRunLoop
返回当前线程的 run loop,返回值是当前线程的 NSRunLoop 对象。如果该线程还没有 run loop,则会为其创建并返回一个 run loop。
首先 NSRunLoop 是对 Core Foundation 框架中的 __CFRunLoop 结构的封装,所以文章上面所有提到的 run loop 在代码层面都可以理解为 NSRunLoop 对象或者是 __CFRunLoop 结构体实例。NSRunLoop 的封装使我们可以以更加面向对象的思想来学习和使用 run loop,并且是继承自 NSObject 的可以使用 ARC 来自动处理内存申请和释放,同时每个 NSRunLoop 对象是和一个 __CFRunLoop 结构体实例所对应的,getCFRunLoop 函数使我们可以由一个 NSRunLoop 对象得到其对应的 CFRunLoopRef(__CFRunLoop 结构体指针)然后由 CFRunLoopRef 我们可以对 run loop 执行更多的操作,因为这里虽是封装,但是还是有很多 __CFRunLoop 的函数在 NSRunLoop 并没有实现,例如上面的提到的设置 run loop observer:void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode)
函数。
提示:timer 不被视为 input source,在等待此方法返回时可能会触发多次。(即我们使用 addTimer:forMode: 向 NSRunLoop 对象添加一个 timer,而且此 NSRunLoop 对象 仅此一个 timer,然后不使用 while 循环仅使用 runMode:beforeDate: 启动该 run loop,则 timer 的 selector 将一直执行,timer 的执行并不能像 input source 一样仅输入一次就能导致 runMode:beforeDate: 函数返回)
参考链接🔗🔗:
25. run loop 官方文档。
在另一个线程上执行 selector 时,目标线程必须具有活动的 run loop。对于你创建的子线程,这意味着等待直到你的代码显式启动当前线程的 run loop。但是,由于主线程启动了自己的 run loop,因此你可以在应用程序调用应用程序委托的 applicationDidFinishLaunching:
函数后立即开始在主线程上发出调用(添加 selector 在主线程执行)。每次循环时,run loop 都会处理队列中所有的执行 selector 的调用,而不是在每次循环迭代时仅处理一个。
Timer sources 在将来的预设时间将事件同步传递到你的线程。Timers 是线程通知自己执行某事的一种方式。
尽管 timer 生成基于时间的通知(time-based notifications),但它不是一种实时机制(real-time mechanism)。与 input sources 一样,timer 与 run loop 的特定 mode 相关联。如果 timer 未处于 run loop 当前监视的模式,则在以 timer 支持的 mode 之一运行 run loop 之前,timer 不会触发。类似地,如果在 run loop 正在执行处理程序例程(handler routine)的过程中触发 timer ,则 timer 将等到下一次通过 run loop 调用其处理程序例程(handler routine)。如果 run loop 根本没有运行,计时器就不会触发。
你可以将 timer 配置为仅生成一次事件或重复生成事件。重复 timer 根据预定的触发时间而不是实际触发时间自动重新调度自己。例如,如果一个 timer 被安排在某个特定时间触发,并且此后每隔 5 秒触发一次,那么即使实际触发时间被延迟,计划的触发时间也将始终落在原来的 5 秒时间间隔上。如果触发时间延迟太久,以致错过了一个或多个预定的触发时间,则对于错过的时间段,timer 只触发一次。在为错过的时间段触发后,timer 将重新安排为下一个预定的触发时间。
与 timers 类似, run loop observers 可以使用一次或重复使用。一次性 observer 在激发后从 run loop 中移除自己,而重复 observer 保持连接。你可以指定在创建 observer 时是运行一次还是重复运行。
每次运行它时,线程的 run loop 都会处理待办事件(pending events),并为所有附加的 observers 生成通知。它执行此操作的顺序非常 具体/明确,如下所示:
-
通知 observers 即将进入 run loop。
-
通知 observers 任何准备就绪的 timers 即将触发。
-
通知 observers 任何不基于端口(not port based)的 input sources(source0)都将被触发。
-
触发所有准备触发的非基于端口的输入源(non-port-based input sources)(source0)。
-
如果基于端口的输入源(port-based input source)已准备好并等待启动,请立即处理事件。转到步骤 9。
-
通知 observers,线程即将进入休眠状态。
-
使线程进入休眠状态并等待唤醒,直到发生以下事件之一时会被唤醒:
- 基于端口的输入源(port-based input source)(source1)的事件到达。
- timers 触发。
- 为 run loop 设置的超时时间过期。
- run loop 被显式唤醒。
- 通知 observer,线程刚刚唤醒。
- 处理唤醒时收到的待办事件。
- 如果触发了用户定义的 timer(处理 timer 事件并重新启动循环)。转到步骤2。
- 如果触发了 input source,传递事件。(source1)
- 如果 run loop 被明确唤醒但尚未超时,重新启动循环。转到步骤2。
- 通知 observer,run loop 已退出。
例如,如果计划执行以下任一操作,则需要启动 run loop:
- 使用端口(ports)或自定义输入源(custom input sources)与其他线程通信。
- 在线程上使用 timer。
- 在 Cocoa 应用程序中使用任何
performSelector…
方法。 - 保持线程执行定期任务(periodic tasks)。
如果选择使用 run loop,则配置和设置很简单。不过,与所有线程编程一样,你应该有一个在适当情况下退出子线程的计划。通过让线程退出而干净地结束它总是比强制它终止要好。
Run loop 对象提供主接口,用于向 run loop 添加 input sources、timers 和 run loop observers,然后运行它。每个线程都有一个与之关联的 run loop 对象。在 Cocoa 中,此对象是 NSRunLoop 类的实例。在低级应用程序中(low-level application),它是指向 CFRunLoopRef 不透明类型的指针。
要获取当前线程的 run loop,请使用以下方法之一:
- 在 Cocoa 应用程序中,使用 NSRunLoop 的
currentRunLoop
类方法检索 NSRunLoop 对象。 - 使用
CFRunLoopGetCurrent
函数。
在子线程上运行 run loop 之前,必须至少向其添加一个 input source 或 timer。如果 run loop 没有任何要监视的 source,则当你尝试运行它时,它会立即退出。
在为长生存期线程(long-lived thread)配置运行循环时,最好至少添加一个 input source 来接收消息。虽然你可以在只附加 timer 的情况下进入运行 run loop,但一旦 timer 触发,它通常会失效,这将导致 run loop 退出。附加一个重复 timer 可以使 run loop 在较长的时间内运行,但需要定期启动 timer 以唤醒线程,这实际上是另一种轮询形式。相比之下,input source 等待事件发生,让线程一直处于休眠状态。
在处理事件之前,有两种方法可以使 run loop 退出:
- 配置 run loop 以使用超时值运行。
- 告诉运行循环停止。
如果可以管理的话,使用超时值当然是首选。指定一个超时值可以让 run loop 在退出之前完成其所有的正常处理,包括向 run loop observers 发送通知。
使用 CFRunLoopStop
函数显式停止 run loop 会产生类似超时的结果。run loop 发送任何剩余的 run loop 通知,然后退出。不同之处在于,你可以在无条件启动的运行循环中使用此技术。
要与 NSMachPort 对象建立本地连接,需要创建端口对象并将其添加到主线程的运行循环中。启动子线程时,将同一对象传递给线程的入口点函数。子线程可以使用相同的对象将消息发送回主线程。
CFRunLoop 对象监视任务的输入源(sources of input),并在准备好进行处理时调度控制。输入源(input sources)的示例可能包括用户输入设备、网络连接、周期性或延时事件以及异步回调。
run loop 可以监视三种类型的对象:sources(CFRunLoopSource)、timers(CFRunLoopTimer)和 observers(CFRunLoopObserver)。要在这些对象需要处理时接收回调,必须首先使用 CFRunLoopAddSource
、CFRunLoopAddTimer
或 CFRunLoopAddObserver
将这些对象放入 run loop 中,以后也可以从 run loop 中删除它们(或使其 invalidate)以停止接收其回调。
添加到 run loop 的每个 source、timer 和 observer 必须与一个或多个 run loop modes 相关联。Modes 决定 run loop 在给定迭代期间处理哪些事件。每次 run loop 执行时,它都以特定 mode 执行。在该 mode 下,run loop 只处理与该 mode 关联的 sources、timers 和 observers 关联的事件。你可以将大多数 sources 分配给默认的 run loop mode(由 kCFRunLoopDefaultMode 常量指定),该 mode 用于在应用程序(或线程)空闲时处理事件。然而,系统定义了其它 modes,并且可以在其它 modes 下执行 run loop,以限制处理哪些 sources、timers 和 observers。因为 run loop modes 被简单地指定为字符串,所以你还可以定义自己的自定义 mode 来限制事件的处理。
26. 在当前线程获取 run loop 的过程。
CFRunLoopGetMain/CFRunLoopGetCurrent
函数可分别用于获取主线程的 run loop 和获取当前线程(子线程)的 run loop。main run loop 使用一个静态变量 __main 存储,子线程的 run loop 会保存在当前线程的 TSD 中。两者在第一次获取 run loop 时都会调用 _CFRunLoopGet0 函数根据线程的 pthread_t 对象从静态全局变量 __CFRunLoops(static CFMutableDictionaryRef)中获取,如果获取不到的话则新建 run loop 对象,并根据线程的 pthread_t 保存在静态全局变量 __CFRunLoops(static CFMutableDictionaryRef)中,方便后续读取。
每个线程只有一个 run loop。既不能创建(系统帮创建,不需要开发者自己手动创建)也不能销毁线程的 run loop(线程销毁时同时也会通过 TSD 使其对应的 run loop 销毁)。Core Foundation 会根据需要自动为你创建它。使用 CFRunLoopGetCurrent
获取当前线程的 run loop。调用 CFRunLoopRun
以默认模式运行当前线程的 run loop,直到 run loop 被 CFRunLoopStop
停止。也可以调用 CFRunLoopRunInMode
以指定的 mode 运行当前线程的 run loop 一段时间(或直到 run loop 停止)。只有在请求的模式至少有一个要监视的 source 或 timer 时,才能运行 run loop。
Core Foundation 中的 CFRunLoop 都是 C API,提供了 run loop 相当丰富的接口,且都是线程安全的,NSRunLoop 是对 CFRunLoopRef 的封装,提供了面向对象的 API,非线程安全的。使用 NSRunLoop 的 getCFRunLoop
方法即可获取相应的 CFRunLoopRef
类型。
下面看一下Objective-C 指针与 Core Foundation 指针之间的转换规则:
ARC 仅管理 Objective-C 指针(retain、release、autorelease),不管理 Core Foundation 指针,CF 指针需要我们手动的 CFRetain 和 CFRelease 来管理(对应 MRC 时的 retain/release),CF 中没有 autorelease。
Cocoa Foundation 指针与 Core Foundation指针转换,需要考虑的是所指向对象所有权的归属。ARC 提供了 3 个修饰符来管理。
1. __bridge,什么也不做,仅仅是转换。此种情况下:
1.1:从 Cocoa 转换到 Core,需要手动 CFRetain,否则,Cocoa 指针释放后,传出去的指针则无效。
1.2:从 Core 转换到 Cocoa,需要手动 CFRelease,否则,Cocoa 指针释放后,对象引用计数仍为 1,不会被销毁。
2. __bridge_retained,转换后自动调用 CFRetain,即帮助自动解决上述 1.1 的情形。
(__bridge_retained or CFBridgingRetain,ARC 把对象所有权转出,需 Core Foundation 处理。)
3. __bridge_transfer,转换后自动调用 CFRelease,即帮助自动解决上述 1.2 的情形。
(__bridge_transfer or CFBridgingRelease,Core Foundation 把对象所有权交给 ARC,由 ARC 自动处理。)
在 Core Foundation 下 __CFRunLoop 结构是 Run Loop 对应的数据结构,对应 Cocoa 中的 NSRunLoop 类。CFRunLoopRef 则是指向 __CFRunLoop 结构体的指针。
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
CFRuntimeBase _base; // 所有 CF "instances" 都是从这个结构开始的
pthread_mutex_t _lock; // 锁定以访问 mode list(CFMutableSetRef _modes)
// typedef mach_port_t __CFPort;
// 唤醒 run loop 的端口,这个是 run loop 原理的关键所在,可通过 port 来触发 CFRunLoopWakeUp 函数
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 手动唤醒 run loop 的端口。初始化 run loop 时设置,仅用于 CFRunLoopWakeUp,CFRunLoopWakeUp 函数会向 _wakeUpPort 发送一条消息
Boolean _unused; // 标记是否使用过
//_perRunData 是 run loop 每次运行都会重置的一个数据结构,这里的重置是指再创建一个 _per_run_data 实例赋值给 rl->_perRunData,
// 并不是简单的把 _perRunData 的每个成员变量重新赋值。(volatile 防止编译器自行优化,每次读值都去寄存器里面读取)
// 包括 uint32_t stopped; 是 run loop 是否停止的标记,uint32_t ignoreWakeUps; run loop 是否忽略唤醒的标记(它们的实际用意是表示当前 run loop 是否已经停止、是否已经被唤醒)
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; // run loop 所对应的线程,二者是一一对应的。(之前在学习线程时并没有在线程的结构体中看到有 run loop 相关的字段,其实线程的 run loop 是存在了 TSD 中,当然如果是线程有获取 run loop 的话)
uint32_t _winthread; // Windows 下记录 run loop 对象创建时所处的线程的 ID
CFMutableSetRef _commonModes; // 存储字符串(而非 runloopMode 对象)的容器,对应着所有标记为 common 的 mode。
CFMutableSetRef _commonModeItems; // 存储 modeItem 对象的容器,对应于所有标记为 common 的 mode 下的 Item(即 source、timer、observer)
CFRunLoopModeRef _currentMode; // 当前运行的 Run Loop Mode 实例的指针(typedef struct __CFRunLoopMode *CFRunLoopModeRef)
CFMutableSetRef _modes; // 集合,存储的是 CFRunLoopModeRef
// 链表头指针,该链表保存了所有需要被 run loop 执行的 block。
// 外部通过调用 CFRunLoopPerformBlock 函数来向链表中添加一个 block 节点。
// run loop 会在 CFRunLoopDoBlock 时遍历该链表,逐一执行 block。
struct _block_item *_blocks_head;
// 链表尾指针,之所以有尾指针,是为了降低增加 block 节点时的时间复杂度。
//(例如有新节点插入时,只有头节点的话,要从头节点遍历才能找到尾节点,现在已经有尾节点不需要遍历了,则时间复杂度从 O(n) 降到了 O(1)。)
struct _block_item *_blocks_tail;
// 绝对时间是自参考日期到参考日期(纪元)为 2001 年 1 月 1 日 00:00:00 的时间间隔。
// typedef double CFTimeInterval;
// typedef CFTimeInterval CFAbsoluteTime;
CFAbsoluteTime _runTime; // 运行时间点
CFAbsoluteTime _sleepTime; // 休眠时间点
// 所有 "CF objects" 的基础 "type" 及其上的多态函数(polymorphic functions)
// typedef const void * CFTypeRef;
CFTypeRef _counterpart;
};
需要被 run loop 执行的 block 链表中的节点数据结构。
struct _block_item {
struct _block_item *_next; // 指向下一个节点
// typedef const void * CFTypeRef;
// CFString 或 CFSet 类型,也就是说一个 block 可能对应单个或多个 mode
CFTypeRef _mode; // CFString or CFSet
void (^_block)(void); // 真正要执行的 block 本体
};
一个 run loop 对应多个 mode,一个 mode 下可以包含多个 modeItem。既然 run loop 包含多个 mode 那么它定可以在不同的 mode 下运行,run loop 一次只能在一个 mode 下运行,如果想要切换 mode,只能退出 run loop,然后再根据指定的 mode 运行 run loop,这样可以是使不同的 mode 下的 modeItem 相互隔离,不会相互影响。
27. 创建 run loop 的过程。
static CFRunLoopRef __CFRunLoopCreate(pthread_t t);
函数入参是一个线程,返回值是一个 run loop 指针,正如其名,完成的功能就是为线程创建 run loop。它首先调用 _CFRuntimeCreateInstance
函数,根据入参 CFRunLoopGetTypeID()
创建一个 CFRunLoop 实例变量并返回其地址,然后对该 run loop 的实例变量的一些成员变量进行初始化。
- 初始化 loop 的
_perRunData
。 - 初始化 loop 的
pthread_mutex_t _lock
,_lock
的初始时属性用的PTHREAD_MUTEX_RECURSIVE
,即_lock
为一个互斥递归锁。 - 给 loop 的
_wakeUpPort
(唤醒端口)赋初值(__CFPortAllocate()
)。 - 设置 loop 的
_perRunData->ignoreWakeUps
为0x57414B45
,前面__CFRunLoopPushPerRunData
初始化时_perRunData->ignoreWakeUps
的值是0x00000000
。 - 初始化 loop 的
_commonModes
并把默认 mode 的字符串("kCFRunLoopDefaultMode")添加到_commonModes
中,即把默认 mode 标记为 common。 - 初始化 loop 的
_modes
,并把构建好的CFRunLoopModeRef rlm
添加到_modes
中。 - 把
pthread_t t
赋值给 loop 的_pthread
成员变量。(Windows
下会获取当前线程的 ID 赋值给 loop 的_winthread
)
28. CFRunLoopMode 定义及其创建过程。
每次 run loop 开始 run 的时候,都必须指定一个 mode,称为 run loop mode(运行循环模式)。mode 指定了在这次 run 中,run loop 可以处理的任务,对于不属于当前 mode 的任务,则需要切换 run loop 至对应 mode 下,再重新调用 run 方法,才能够被处理,这样也保证了不同 mode 的 source/timer/observer 互不影响,使不同 mode 下的数据做到相互隔离的。
__CFRunLoopMode
定义。
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base; // 所有 CF "instances" 都是从这个结构开始的
pthread_mutex_t _lock; // 必须在锁定之前将 run loop 锁定,即加锁前需要 run loop 对象先加锁
CFStringRef _name; // mode 的一个字符串名称
Boolean _stopped; // 标记了 run loop 的运行状态,实际上并非如此简单,还有前面的 _per_run_data。
char _padding[3];
// _sources0、_sources1、_observers、_timers 都是集合类型,里面都是 mode item,即一个 mode 包含多个 mode item
CFMutableSetRef _sources0; // sources0 事件集合(之所以用集合是为了保证每个元素唯一)
CFMutableSetRef _sources1; // sources1 事件集合
CFMutableArrayRef _observers; // run loop observer 观察者数组
CFMutableArrayRef _timers; // 计时器数组
CFMutableDictionaryRef _portToV1SourceMap; // 存储了 Source1 的 port 与 source 的对应关系,key 是 mach_port_t,value 是 CFRunLoopSourceRef
__CFPortSet _portSet; // 保存所有需要监听的 port,比如 _wakeUpPort,_timerPort,queuePort 都保存在这个集合中
CFIndex _observerMask; // 添加 obsever 时设置 _observerMask 为 observer 的 _activities(CFRunLoopActivity 状态)
// DEPLOYMENT_TARGET_MACOSX 表示部署在 maxOS 下
// #if DEPLOYMENT_TARGET_MACOSX
// #define USE_DISPATCH_SOURCE_FOR_TIMERS 1
// #define USE_MK_TIMER_TOO 1
// #else
// #define USE_DISPATCH_SOURCE_FOR_TIMERS 0
// #define USE_MK_TIMER_TOO 1
// #endif
// 在 maxOS 下 USE_DISPATCH_SOURCE_FOR_TIMERS 和 USE_MK_TIMER_TOO 都为真。
#if USE_DISPATCH_SOURCE_FOR_TIMERS
// 使用 dispatch_source 表示 timer
dispatch_source_t _timerSource; // GCD 计时器
dispatch_queue_t _queue; // 队列
Boolean _timerFired; // set to true by the source when a timer has fired 计时器触发时由 source 设置为 true,在 _timerSource 的回调事件中值会置为 true,即标记为 timer 被触发。
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
// 使用 MK 表示 timer
mach_port_t _timerPort; // MK_TIMER 的 port
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
// timer 软临界点
uint64_t _timerSoftDeadline; /* TSR */
// timer 硬临界点
uint64_t _timerHardDeadline; /* TSR */
};
__CFRunLoopFindMode
函数根据 modeName 从 rl 的 _modes 中找到其对应的 CFRunLoopModeRef,如果找到的话则加锁并返回。如果未找到,并且 create 为真的话,则新建 __CFRunLoopMode 加锁并返回,如果 create 为假的话,则返回 NULL。
static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) {
// 用于检查给定的进程是否被分叉
CHECK_FOR_FORK();
// struct __CFRunLoopMode 结构体指针
CFRunLoopModeRef rlm;
// 创建一个 struct __CFRunLoopMode 结构体实例,
// 并调用 memset 把 srlm 内存空间全部置为 0。
struct __CFRunLoopMode srlm;
memset(&srlm, 0, sizeof(srlm));
// __kCFRunLoopModeTypeID 现在正是表示 CFRunLoopMode 类,实际值是 run loop mode 类在全局类表 __CFRuntimeClassTable 中的索引。
// 前面 __CFRunLoopCreate 函数内部会调用 CFRunLoopGetTypeID() 函数,
// 其内部是全局执行一次在 CF 运行时中注册两个新类 run loop(CFRunLoop)和 run loop mode(CFRunLoopMode),
// 其中 __kCFRunLoopModeTypeID = _CFRuntimeRegisterClass(&__CFRunLoopModeClass),那么 __kCFRunLoopModeTypeID 此时便是 run loop mode 类在全局类表中的索引。
//(__CFRunLoopModeClass 可以理解为一个静态全局的 "类对象"(实际值是一个),_CFRuntimeRegisterClass 函数正是把它放进一个全局的 __CFRuntimeClassTable 类表中。)
// 本身 srlm 是一片空白内存,现在相当于把 srlm 设置为一个 run loop mode 类的对象。
//(实际就是设置 CFRuntimeBase 的 _cfinfo 成员变量,srlm 里面目前包含的内容就是 run loop mode 的类信息。)
_CFRuntimeSetInstanceTypeIDAndIsa(&srlm, __kCFRunLoopModeTypeID);
// 把 srlm 的 mode 名称设置为入参 modeName
srlm._name = modeName;
// 从 rl->_modes 哈希表中找 &srlm 对应的 CFRunLoopModeRef
rlm = (CFRunLoopModeRef)CFSetGetValue(rl->_modes, &srlm);
// 如果找到了则加锁,然后返回 rlm。
if (NULL != rlm) {
__CFRunLoopModeLock(rlm);
return rlm;
}
// 如果没有找到,并且 create 值为 false,则表示不进行创建,直接返回 NULL。
if (!create) {
return NULL;
}
// 创建一个 CFRunLoopMode 对并返回其地址
rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL);
// 如果 rlm 创建失败,则返回 NULL
if (NULL == rlm) {
return NULL;
}
// 初始化 rlm 的 pthread_mutex_t _lock 为一个互斥递归锁。
//(__CFRunLoopLockInit 内部使用的 PTHREAD_MUTEX_RECURSIVE 表示递归锁,允许同一个线程对同一锁加锁多次,且需要对应次数的解锁操作)
__CFRunLoopLockInit(&rlm->_lock);
// 初始化 _name
rlm->_name = CFStringCreateCopy(kCFAllocatorSystemDefault, modeName);
// 下面是一组成员变量的初始赋值
rlm->_stopped = false;
rlm->_portToV1SourceMap = NULL;
// _sources0、_sources1、_observers、_timers 初始状态都是空的
rlm->_sources0 = NULL;
rlm->_sources1 = NULL;
rlm->_observers = NULL;
rlm->_timers = NULL;
rlm->_observerMask = 0;
rlm->_portSet = __CFPortSetAllocate(); // CFSet 申请空间初始化
rlm->_timerSoftDeadline = UINT64_MAX;
rlm->_timerHardDeadline = UINT64_MAX;
// ret 是一个临时变量初始值是 KERN_SUCCESS,用来表示向 rlm->_portSet 中添加 port 时的结果,
// 如果添加失败的话,会直接 CRASH
kern_return_t ret = KERN_SUCCESS;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
// macOS 下,使用 dispatch_source 构造 timer
// _timerFired 首先赋值为 false,然后在 timer 的回调函数执行的时候会赋值为 true
rlm->_timerFired = false;
// 队列
rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0);
// 构建 queuePort,入参是 mode 的 _queue
mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
// 如果 queuePort 为 NULL,则 crash。(无法创建运行循环模式队列端口。)
if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1);
// 构建 dispatch_source 类型使用的是 DISPATCH_SOURCE_TYPE_TIMER,表示是一个 timer
rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue);
// 这里为了在下面的 block 内部修改 _timerFired 的值,用了一个 __block 指针变量。(觉的如果这里只是改值,感觉用指针就够了可以不用 __block 修饰)
// 当 _timerSource(计时器)回调时会执行这个 block。
__block Boolean *timerFiredPointer = &(rlm->_timerFired);
dispatch_source_set_event_handler(rlm->_timerSource, ^{
*timerFiredPointer = true;
});
// Set timer to far out there. The unique leeway makes this timer easy to spot in debug output.
// 将计时器设置在远处。独特的回旋余地使该计时器易于发现调试输出。(从 DISPATCH_TIME_FOREVER 启动,DISPATCH_TIME_FOREVER 为时间间隔)
_dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321);
// 启动
dispatch_resume(rlm->_timerSource);
// 把运行循环模式队列端口 queuePort 添加到 rlm 的 _portSet(端口集合)中。
ret = __CFPortSetInsert(queuePort, rlm->_portSet);
// 如果添加失败则 crash。(无法将计时器端口插入端口集中。)
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
#if USE_MK_TIMER_TOO
// mk 构造 timer
// 构建 timer 端口
rlm->_timerPort = mk_timer_create();
// 同样把 rlm 的 _timerPort 添加到 rlm 的 _portSet(端口集合)中。
ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet);
// 如果添加失败则 crash。(无法将计时器端口插入端口集中。)
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret);
#endif
// 然后这里把 rl 的 _wakeUpPort 也添加到 rlm 的 _portSet(端口集合)中。
//(这里要特别注意一下,run loop 的 _wakeUpPort 会被插入到所有 mode 的 _portSet 中。)
ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet);
// 如果添加失败则 crash。(无法将唤醒端口插入端口集中。)
if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret);
#if DEPLOYMENT_TARGET_WINDOWS
rlm->_msgQMask = 0;
rlm->_msgPump = NULL;
#endif
// 这里把 rlm 添加到 rl 的 _modes 中,
//(本质是把 rlm 添加到 _modes 哈希表中)
CFSetAddValue(rl->_modes, rlm);
// 释放,rlm 已被 rl->_modes 持有,并不会被销毁只是减少引用计数
CFRelease(rlm);
// 加锁,然后返回 rlm
__CFRunLoopModeLock(rlm); /* return mode locked */
return rlm;
}
其中 ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet)
会把 run loop 对象的 _wakeUpPort
添加到每个 run loop mode 对象的 _portSet
端口集合里。即当一个 run loop 有多个 run loop mode 时,那么每个 run loop mode 都会有 run loop 的 _wakeUpPort
。
在 macOS 下 run loop mode 的 _timerSource
的计时器的回调事件内部会把 run loop mode 的 _timerFired
字段置为 true,表示计时器被触发。
run loop mode 创建好了,看到 source/timer/observer 三者对应的 _sources0
、_sources1
、_observers
、_timers
四个字段初始状态都是空,需要我们自己添加 run loop mode item,