0%

dispatch_async异步函数的调用

我们继续上一章的内容继续研究一下block的调用,在NSlog这里打一个断点,看看调用的函数栈。

1
2
3
dispatch_async(conque, ^{
NSLog(@"12334");
});

我们运行代码,通过bt命令看一下调用栈:

1
2
3
4
5
6
7
8
9
10
11
(lldb) bt
* thread #8, queue = 'conque', stop reason = breakpoint 2.1
* frame #0: 0x0000000103abd0d7 __29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x0000000103ac00e8) at ViewController.m:48:9
frame #1: 0x0000000103d2e7ec libdispatch.dylib`_dispatch_call_block_and_release + 12
frame #2: 0x0000000103d2f9c8 libdispatch.dylib`_dispatch_client_callout + 8
frame #3: 0x0000000103d32316 libdispatch.dylib`_dispatch_continuation_pop + 557
frame #4: 0x0000000103d3171c libdispatch.dylib`_dispatch_async_redirect_invoke + 779
frame #5: 0x0000000103d41508 libdispatch.dylib`_dispatch_root_queue_drain + 351
frame #6: 0x0000000103d41e6d libdispatch.dylib`_dispatch_worker_thread2 + 135
frame #7: 0x00007fff60c8e453 libsystem_pthread.dylib`_pthread_wqthread + 244
frame #8: 0x00007fff60c8d467 libsystem_pthread.dylib`start_wqthread + 15

从下往上看哈~函数的调用竟然是通过与pthread交互之后发生的。然后到了_dispatch_worker_thread2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void
_dispatch_worker_thread2(pthread_priority_t pp)
{
bool overcommit = pp & _PTHREAD_PRIORITY_OVERCOMMIT_FLAG;
dispatch_queue_global_t dq;

pp &= _PTHREAD_PRIORITY_OVERCOMMIT_FLAG | ~_PTHREAD_PRIORITY_FLAGS_MASK;
_dispatch_thread_setspecific(dispatch_priority_key, (void *)(uintptr_t)pp);
dq = _dispatch_get_root_queue(_dispatch_qos_from_pp(pp), overcommit);

_dispatch_introspection_thread_add();
_dispatch_trace_runtime_event(worker_unpark, dq, 0);

int pending = os_atomic_dec2o(dq, dgq_pending, relaxed);
dispatch_assert(pending >= 0);
_dispatch_root_queue_drain(dq, dq->dq_priority,
DISPATCH_INVOKE_WORKER_DRAIN | DISPATCH_INVOKE_REDIRECTING_DRAIN);
_dispatch_voucher_debug("root queue clear", NULL);
_dispatch_reset_voucher(NULL, DISPATCH_THREAD_PARK);
_dispatch_trace_runtime_event(worker_park, NULL, 0);
}

根据函数调用栈,来到_dispatch_root_queue_drain这个函数。函数内容做了删减。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void
_dispatch_root_queue_drain(dispatch_queue_global_t dq,
dispatch_priority_t pri, dispatch_invoke_flags_t flags)
{
...
// 设置当前queue
_dispatch_queue_set_current(dq);
_dispatch_init_basepri(pri);
_dispatch_adopt_wlh_anon();

struct dispatch_object_s *item;
bool reset = false;
dispatch_invoke_context_s dic = { };
#if DISPATCH_COCOA_COMPAT
_dispatch_last_resort_autorelease_pool_push(&dic);
#endif // DISPATCH_COCOA_COMPAT
_dispatch_queue_drain_init_narrowing_check_deadline(&dic, pri);
_dispatch_perfmon_start();
while (likely(item = _dispatch_root_queue_drain_one(dq))) {
if (reset) _dispatch_wqthread_override_reset();
// 函数重点
_dispatch_continuation_pop_inline(item, &dic, flags, dq);
reset = _dispatch_reset_basepri_override();
if (unlikely(_dispatch_queue_drain_should_narrow(&dic))) {
break;
}
}

...
_dispatch_reset_wlh();
_dispatch_clear_basepri();
// 设置当前queue为NULL
_dispatch_queue_set_current(NULL);
}

这个函数内一开始需要将当前队列调回来,然后执行block中的内容,完成之后,在把对列置空。block内部怎么调用,就在_dispatch_continuation_pop_inline里头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline void
_dispatch_continuation_pop_inline(dispatch_object_t dou,
dispatch_invoke_context_t dic, dispatch_invoke_flags_t flags,
dispatch_queue_class_t dqu)
{
dispatch_pthread_root_queue_observer_hooks_t observer_hooks =
_dispatch_get_pthread_root_queue_observer_hooks();
if (observer_hooks) observer_hooks->queue_will_execute(dqu._dq);
flags &= _DISPATCH_INVOKE_PROPAGATE_MASK;
if (_dispatch_object_has_vtable(dou)) {
dx_invoke(dou._dq, dic, flags);
} else {
_dispatch_continuation_invoke_inline(dou, flags, dqu);
}
if (observer_hooks) observer_hooks->queue_did_execute(dqu._dq);
}

我们猜测有可能执行的是_dispatch_continuation_invoke_inline,因为其他的看着也不咋像那回事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static inline void
_dispatch_continuation_invoke_inline(dispatch_object_t dou,
dispatch_invoke_flags_t flags, dispatch_queue_class_t dqu)
{
dispatch_continuation_t dc = dou._dc, dc1;
dispatch_invoke_with_autoreleasepool(flags, {
uintptr_t dc_flags = dc->dc_flags;
_dispatch_continuation_voucher_adopt(dc, dc_flags);
if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
_dispatch_trace_item_pop(dqu, dou);
}
if (dc_flags & DC_FLAG_CONSUME) {
dc1 = _dispatch_continuation_free_cacheonly(dc);
} else {
dc1 = NULL;
}
if (unlikely(dc_flags & DC_FLAG_GROUP_ASYNC)) {
// 这个可能跟group有关
_dispatch_continuation_with_group_invoke(dc);
} else {
// 应该会执行这个
_dispatch_client_callout(dc->dc_ctxt, dc->dc_func);
_dispatch_trace_item_complete(dc);
}
if (unlikely(dc1)) {
_dispatch_continuation_free_to_cache_limit(dc1);
}
});
_dispatch_perfmon_workitem_inc();
}

dispatch_invoke_with_autoreleasepool这里有一个autoreleasepool,源码内部对自动释放池的操作还是很严谨的。

这里先看看_dispatch_client_callout

1
2
3
4
5
6
7
8
9
10
11
12
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
_dispatch_get_tsd_base();
void *u = _dispatch_get_unwind_tsd();
// 执行这里
if (likely(!u)) return f(ctxt);
_dispatch_set_unwind_tsd(NULL);
f(ctxt);
_dispatch_free_unwind_tsd();
_dispatch_set_unwind_tsd(u);
}

看到了木有啊,f还记得是啥吗?回到上一章的这个_dispatch_continuation_init的函数中,有解释哦,f就等于_dispatch_call_block_and_release

我们再回过头看看打印的函数调用栈,最后执行的不就是_dispatch_call_block_and_release吗?前面的dispatch_asyn内部实现对block进行保存,这里进行调用。

到此为止整个异步函数的调用就结束了。

结合上一章的dispatch_async的内容,我们可以通过汇编添加symbolic breakpoint进行判断,我们所分析的函数执行步奏是否正确,在不知道执行流程的情况下,添加断点可以让我们比较清楚的知道其内部是怎么执行的。这里就不去操作了哈~

dispatch_once

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"once ...");
});

我们使用dispatch_once都是这么写,经常用于创建单利或者只执行一次的代码。接下来看看其内部实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define dispatch_once _dispatch_once

void
_dispatch_once(dispatch_once_t *predicate,
DISPATCH_NOESCAPE dispatch_block_t block)
{
if (DISPATCH_EXPECT(*predicate, ~0l) != ~0l) {
// 调用这个
dispatch_once(predicate, block);
} else {
// 栅栏函数
dispatch_compiler_barrier();
}
DISPATCH_COMPILER_CAN_ASSUME(*predicate == ~0l);
}

在这个函数内部会通过条件判断执行栅栏函数还是,调用dispatch_once;

1
2
3
4
5
6
7
8
9
10
11
@param val
A pointer to a dispatch_once_t that is used to test whether the block has completed or not.

@param block
The block to execute once.

void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}

这里有两个相关参数的介绍,

  1. val:是一个指针,用来判断block执行完成与否。
  2. block:只执行一次的block块

之后就到了dispatch_once_f方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
// 1. dispatch_once_gate_t结构体
dispatch_once_gate_t l = (dispatch_once_gate_t)val;

// 2.真机的情况下,是arm64,DISPATCH_ONCE_INLINE_FASTPATH = 0,
// DISPATCH_ONCE_USE_QUIESCENT_COUNTER = 0
#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER

// 3. 所以执行这里,进行原子类型的加载,就是判断是否执行过
uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
if (likely(v == DLOCK_ONCE_DONE)) {
return;
}
// 4. 不会执行
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
if (likely(DISPATCH_ONCE_IS_GEN(v))) {
return _dispatch_once_mark_done_if_quiesced(l, v);
}
#endif
#endif
// 5. 条件判断是否执行,
if (_dispatch_once_gate_tryenter(l)) {
// 6.
return _dispatch_once_callout(l, ctxt, func);
}
// 7. 等待
return _dispatch_once_wait(l);
}

我们分析一下这个代码:

  1. dispatch_once_gate_t是一个结构体,内部有两个变量dispatch_gate_s dgo_gate是一个锁,uintptr_t dgo_once是否执行过。

  2. arm64判断。

  3. 原子类型的加载,判断当前block块是否执行过,已经执行则return。

  4. arm64下DISPATCH_ONCE_USE_QUIESCENT_COUNTER=0

  5. 条件判断,是否执行过,其内部实现如下:

    1
    2
    3
    4
    5
    6
    static inline bool
    _dispatch_once_gate_tryenter(dispatch_once_gate_t l)
    {
    return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,
    (uintptr_t)_dispatch_lock_value_for_self(), relaxed);
    }

    通过os_atomic_cmpxchg函数比较,在这个锁_dispatch_lock_value_for_self下判断&l->dgo_once, DLOCK_ONCE_UNLOCKED是否相同。不相同执行6._dispatch_once_callout,否则执行7.

  6. _dispatch_once_callout这个函数内部调用的是_dispatch_client_callout之前已经讲过,就是执行block的内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    static void
    _dispatch_once_callout(dispatch_once_gate_t l, void *ctxt,
    dispatch_function_t func)
    {
    // 执行block
    _dispatch_client_callout(ctxt, func);
    // &l->dgo_once赋值,标记已执行
    _dispatch_once_gate_broadcast(l);
    }

    接下来看一下是如何标记的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    static inline void
    _dispatch_once_gate_broadcast(dispatch_once_gate_t l)
    {
    // 首先获取self的一个锁
    dispatch_lock value_self = _dispatch_lock_value_for_self();
    uintptr_t v;
    // arm64下不会执行这个
    #if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
    v = _dispatch_once_mark_quiescing(l);
    #else
    // 执行这里
    v = _dispatch_once_mark_done(l);
    #endif
    // 判断锁是不是自己,是就return
    if (likely((dispatch_lock)v == value_self)) return;
    _dispatch_gate_broadcast_slow(&l->dgo_gate, (dispatch_lock)v);
    }

    我们在看一下_dispatch_once_mark_done内部实现:

    1
    2
    3
    4
    5
    static inline uintptr_t
    _dispatch_once_mark_done(dispatch_once_gate_t dgo)
    {
    return os_atomic_xchg(&dgo->dgo_once, DLOCK_ONCE_DONE, release);
    }

    用这个函数os_atomic_xchg去改变dgo_once的值。

  7. _dispatch_once_wait函数内部是一个for (;;)的死循环,会一直等待dispatch_once的执行,执行完成之后os_atomic_rmw_loop_give_up(return),不在阻塞线程。

这就是dispatch_once的整个流程。使用了一个dgo_once变量来标记是否执行过:

  1. 没有执行则去执行block,并标记dgo_once
  2. 执行过,直接返回
  3. 正在执行,则等待block执行完成。

栅栏函数

先看栅栏函数的代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)barrierDemo {
// 创建一个并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);

// dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);

/* 1. 异步函数 */
dispatch_async(concurrentQueue, ^{
NSLog(@"1");
//sleep(3); // ①
});
/* 2. 栅栏函数 */ // - ②dispatch_barrier_sync
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"2--%@--",[NSThread currentThread]);
});
/* 3. 异步函数 */
dispatch_async(concurrentQueue, ^{
NSLog(@"3");
});
NSLog(@"4");
}
  1. 看一下这个的打印顺序,4-1-2-3也有可能1-4-2-3,打算肯定是先2之后才3。
  2. 这时候把①的代码放开,执行sleep,看看打印顺序。4-1-2-3。2在1之后3s才打印
  3. 把②的代码进行替换,异步的栅栏函数,换成同步函数,看一下执行顺序:1-2-4-3

所以栅栏函数是拦截队列用的,会等待栅栏函数之前的任务执行完成。

  1. 如果把创建的队列换成全局并发队列,会怎么样?可以试一下,这里会发生crash。因为全局队列是系统生成的,系统可能在别的地方也有调用,使用栅栏函数相当于拦截了系统函数,会出现不可控的问题。
  2. 如果是同步函数呢?同步函数本身就是按照队列中任务添加的顺序执行的。如果再加上栅栏函数,完全没有意义,反而会更耗性能。

所以:栅栏函数只能用于自定义的并发队列。

同步函数 dispatch_sync

1
2
3
4
5
6
7
8
9
10
void
dispatch_sync(dispatch_queue_t dq, dispatch_block_t work)
{
uintptr_t dc_flags = DC_FLAG_BLOCK;
if (unlikely(_dispatch_block_has_private_data(work))) {
// 对私有数据的处理,最后还是会回到_dispatch_sync_f函数
return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
}
_dispatch_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
}

这个的重点就是_dispatch_sync_f函数。

1
2
3
4
5
6
static void
_dispatch_sync_f(dispatch_queue_t dq, void *ctxt, dispatch_function_t func,
uintptr_t dc_flags)
{
_dispatch_sync_f_inline(dq, ctxt, func, dc_flags);
}

内部没有做任何处理,只是调用了_dispatch_sync_f_inline函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static inline void
_dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt,
dispatch_function_t func, uintptr_t dc_flags)
{
// 串行队列的宽度等于1,
if (likely(dq->dq_width == 1)) {
// 所以就会执行这里。直接return
return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags);
}

if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
}

dispatch_lane_t dl = upcast(dq)._dl;
// Global concurrent queues and queues bound to non-dispatch threads
// always fall into the slow case, see DISPATCH_ROOT_QUEUE_STATE_INIT_VALUE
if (unlikely(!_dispatch_queue_try_reserve_sync_width(dl))) {
// 发生死锁的原因。
return _dispatch_sync_f_slow(dl, ctxt, func, 0, dl, dc_flags);
}

if (unlikely(dq->do_targetq->do_targetq)) {
return _dispatch_sync_recurse(dl, ctxt, func, dc_flags);
}
_dispatch_introspection_sync_begin(dl);
_dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG(
_dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags)));
}

看到这个_dispatch_barrier_sync_f函数,是不是感觉很眼熟,跟上面说的栅栏函数一样啊,我们接着看一下栅栏函数的内部实现。

1
2
3
4
5
6
7
8
9
void
dispatch_barrier_sync(dispatch_queue_t dq, dispatch_block_t work)
{
uintptr_t dc_flags = DC_FLAG_BARRIER | DC_FLAG_BLOCK;
if (unlikely(_dispatch_block_has_private_data(work))) {
return _dispatch_sync_block_with_privdata(dq, work, dc_flags);
}
_dispatch_barrier_sync_f(dq, work, _dispatch_Block_invoke(work), dc_flags);
}

还真的是一模一样啊,同步函数内部竟然是一个同步栅栏函数。

我们再一步步的探索,_dispatch_barrier_sync_f内部调用的是_dispatch_barrier_sync_f_inline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static inline void
_dispatch_barrier_sync_f_inline(dispatch_queue_t dq, void *ctxt,
dispatch_function_t func, uintptr_t dc_flags)
{
// tid是一个mach_port类型,获取当前的mach_port,一般情况下,mach_port是和线程同时存在的,用来保活。
dispatch_tid tid = _dispatch_tid_self();

if (unlikely(dx_metatype(dq) != _DISPATCH_LANE_TYPE)) {
DISPATCH_CLIENT_CRASH(0, "Queue type doesn't support dispatch_sync");
}

dispatch_lane_t dl = upcast(dq)._dl;
if (unlikely(!_dispatch_queue_try_acquire_barrier_sync(dl, tid))) {
// 这里也会发生死锁
return _dispatch_sync_f_slow(dl, ctxt, func, DC_FLAG_BARRIER, dl,
DC_FLAG_BARRIER | dc_flags);
}

if (unlikely(dl->do_targetq->do_targetq)) {
return _dispatch_sync_recurse(dl, ctxt, func,
DC_FLAG_BARRIER | dc_flags);
}
// 对列内部进行排序
_dispatch_introspection_sync_begin(dl);
//
_dispatch_lane_barrier_sync_invoke_and_complete(dl, ctxt, func
DISPATCH_TRACE_ARG(_dispatch_trace_item_sync_push_pop(
dq, ctxt, func, dc_flags | DC_FLAG_BARRIER)));
}

接着就到了block调用和调用完成的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static void
_dispatch_lane_barrier_sync_invoke_and_complete(dispatch_lane_t dq,
void *ctxt, dispatch_function_t func DISPATCH_TRACE_ARG(void *dc))
{
// 这里执行block的内容,
_dispatch_sync_function_invoke_inline(dq, ctxt, func);
_dispatch_trace_item_complete(dc);
if (unlikely(dq->dq_items_tail || dq->dq_width > 1)) {
return _dispatch_lane_barrier_complete(dq, 0, 0);
}

// block执行完成之后,对当前线程操作,因为同步操作会占用当前线程
// 执行完之后,需要后续的任务继续执行。
const uint64_t fail_unlock_mask = DISPATCH_QUEUE_SUSPEND_BITS_MASK |
DISPATCH_QUEUE_ENQUEUED | DISPATCH_QUEUE_DIRTY |
DISPATCH_QUEUE_RECEIVED_OVERRIDE |
DISPATCH_QUEUE_RECEIVED_SYNC_WAIT;
uint64_t old_state, new_state;
dispatch_wakeup_flags_t flags = 0;

// loop寻找当前线程,根据线程的状态释放当前任务的堵塞。不在阻塞当前线程
os_atomic_rmw_loop2o(dq, dq_state, old_state, new_state, release, {
new_state = old_state - DISPATCH_QUEUE_SERIAL_DRAIN_OWNED;
new_state &= ~DISPATCH_QUEUE_DRAIN_UNLOCK_MASK;
new_state &= ~DISPATCH_QUEUE_MAX_QOS_MASK;
if (unlikely(old_state & fail_unlock_mask)) {
os_atomic_rmw_loop_give_up({
return _dispatch_lane_barrier_complete(dq, 0, flags);
});
}
});
if (_dq_state_is_base_wlh(old_state)) {
_dispatch_event_loop_assert_not_owned((dispatch_wlh_t)dq);
}
}

我们先看一下_dispatch_sync_function_invoke_inline函数的内容:

1
2
3
4
5
6
7
8
9
10
static inline void
_dispatch_sync_function_invoke_inline(dispatch_queue_class_t dq, void *ctxt,
dispatch_function_t func)
{
dispatch_thread_frame_s dtf;
_dispatch_thread_frame_push(&dtf, dq);
_dispatch_client_callout(ctxt, func);
_dispatch_perfmon_workitem_inc();
_dispatch_thread_frame_pop(&dtf);
}

其主要目的是把当前任务添加(push)到线程中,然后执行_dispatch_client_callout(这个就不细说了),执行完成之后pop出去。

这个就是同步函数为啥会阻塞当前线程的内部原理。接下来我们再看看死锁。

同步死锁

我们再回到_dispatch_sync_f_inline函数,看看发生死锁的原因_dispatch_sync_f_slow。在同步函数内部和栅栏函数内部都会发生死锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static void
_dispatch_sync_f_slow(dispatch_queue_class_t top_dqu, void *ctxt,
dispatch_function_t func, uintptr_t top_dc_flags,
dispatch_queue_class_t dqu, uintptr_t dc_flags)
{
dispatch_queue_t top_dq = top_dqu._dq;
dispatch_queue_t dq = dqu._dq;
if (unlikely(!dq->do_targetq)) {
// 没有找到target执行invoke
return _dispatch_sync_function_invoke(dq, ctxt, func);
}
// 设置默认值
pthread_priority_t pp = _dispatch_get_priority();
struct dispatch_sync_context_s dsc = {
.dc_flags = DC_FLAG_SYNC_WAITER | dc_flags,
.dc_func = _dispatch_async_and_wait_invoke,
.dc_ctxt = &dsc,
.dc_other = top_dq,
.dc_priority = pp | _PTHREAD_PRIORITY_ENFORCE_FLAG,
.dc_voucher = _voucher_get(),
.dsc_func = func,
.dsc_ctxt = ctxt,
.dsc_waiter = _dispatch_tid_self(),
};

// push到队列中
_dispatch_trace_item_push(top_dq, &dsc);
// 等待当前线程,这里会一直等
__DISPATCH_WAIT_FOR_QUEUE__(&dsc, dq);

if (dsc.dsc_func == NULL) {
// dsc_func being cleared means that the block ran on another thread ie.
// case (2) as listed in _dispatch_async_and_wait_f_slow.
dispatch_queue_t stop_dq = dsc.dc_other;
return _dispatch_sync_complete_recurse(top_dq, stop_dq, top_dc_flags);
}

_dispatch_introspection_sync_begin(top_dq);
_dispatch_trace_item_pop(top_dq, &dsc);
_dispatch_sync_invoke_and_complete_recurse(top_dq, ctxt, func,top_dc_flags
DISPATCH_TRACE_ARG(&dsc));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
__DISPATCH_WAIT_FOR_QUEUE__(dispatch_sync_context_t dsc, dispatch_queue_t dq)
{
// 获取当前的状态
uint64_t dq_state = _dispatch_wait_prepare(dq);
// 判断状态是否是waiter
if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
// 发生crash
DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
"dispatch_sync called on queue "
"already owned by current thread");
}

...

}

dsc->dsc_waiter的值是在上一层函数通过_dispatch_tid_self()获取到的。然后判断是否是在等待,是的话则触发crash。

我们还用之前的例子来看一下死锁的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)textDemo4 {
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

运行一下,发现就会crash,如果

可以看出最后的调用栈就是我们上面分析的_dispatch_sync_f_slow -> __DISPATCH_WAIT_FOR_QUEUE__

还用这张图来分析它的原因,虽然我们已经看到了其内部的实现原理。

总结

  1. 异步函数的调用逻辑原理
  2. dispatch_once的原理:使用dgo_once标记是否执行过。
  3. 栅栏函数
    1. 只用于自定义的并发函数
  4. dispatch_sync 同步函数
    1. 内部是一个栅栏函数
    2. 死锁的原因:互相等待

GCD简介

全称是 Grand Central Dispatch。底层为C语言,将任务添加到队列,并且指定执行任务的函数。GCD提供了非常强大的函数。

GCD的优势

  • 是苹果公司为多核的并行运算提出的解决方案
  • 会自动利用更多的CPU内核(比如双核、四核)
  • 会自动管理线程的生命周期(创建线程、调度任务、销毁线程) 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

同步和异步函数

GCD使用block封装任务,任务的block没有参数也没有返回值。

任务的调度有同步和异步之分。

同步dispatch_sync

  1. 必须等待当前语句执行完毕,才会执行下一条语句。
  2. 同步不会开启线程。
  3. 在当前线程执行block任务。

异步 dispatch_async

  1. 会开启新线程执行block任务
  2. 异步是多线程的代名词

队列

队列是一种数据结构,遵循先进先出的原则(FIFO)。分为串行队列和并发队列(并行)。

不管是串行还是并发队列,谁在队列的最前头谁先开始执行。但是执行的快慢与当前所需资源有关。

串行等待上一个任务执行完成
并发不会等待上一个任务执行完成

函数与队列

同步函数 + 串行队列

  • 不会开启线程,在当前线程执行任务
  • 执行完一个执行下一个,会产生堵塞
1
2
3
4
5
6
7
8
9
10
11
12
// 创建串行队列
- (void)serialSyncTest{
dispatch_queue_t queue = dispatch_queue_create("queue1", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i < 20; i++) {
// a
dispatch_sync(queue, ^{
// b
NSLog(@"i = %d, thread = %@",i,[NSThread currentThread]);
});
// c
}
}

会从0到19,按照a-b-c的顺序输出所有数据。根据打印的线程,发现就是主线程,并不会开启新线程。

同步函数 + 并发队列

  • 不会开启线程,在当前线程执行任务
  • 任务一个接着一个执行
1
2
3
4
5
6
7
8
9
10
11
- (void)concurrentSyncTest{

//1:创建并发队列
dispatch_queue_t queue = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<20; i++) {
dispatch_sync(queue, ^{
NSLog(@"i = %d, thread = %@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
}

同步并发队列,会按照顺序执行,最后打印hello queue

同步函数的情况下,不管是串行还是并发,都不会开启新线程,任务按步执行。

异步函数 + 串行队列

  • 开启新线程
  • 任务一个接一个执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 异步串行
- (void)serialAsyncTest{
//1:创建串行队列
dispatch_queue_t queue = dispatch_queue_create("queue3", DISPATCH_QUEUE_SERIAL);
for (int i = 0; i<20; i++) {
// a
dispatch_async(queue, ^{
// b
NSLog(@"i = %d,thread = %@",i,[NSThread currentThread]);
});
// c
}
// d
NSLog(@"hello queue");
}

由于是异步串行队列,线程的创建会有耗时操作,在for循环中执行的顺序是a-c-b(a),执行了a之后是c,在之后不一定是a还是b。

而d语句可能先执行,也可能后执行。

根据线程的打印情况,发现会开启新线程。

异步函数 + 并发队列

  • 开启新线程,并开始执行
  • 任务异步执行,没有顺序,与CPU调度有关
1
2
3
4
5
6
7
8
9
10
- (void)concurrentAsyncTest{
//1:创建并发队列
dispatch_queue_t queue = dispatch_queue_create("queue4", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<20; i++) {
dispatch_async(queue, ^{
NSLog(@"%d-%@",i,[NSThread currentThread]);
});
}
NSLog(@"hello queue");
}

会开启新的线程,执行没有顺序。

函数队列的面试题

异步并发队列

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)textDemo {
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
// 耗时
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

这是一个并行队列,内部是异步执行。当block内部有任务需要执行时,会产生耗时,所以就会先执行完成block外部的简单调用。而在block内部,是按照正常的流程执行的。

打印的结果是1 5 2 4 3

异步串行队列

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)textDemo1{
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

只要是block内部需要执行,则一定是耗时操作,所以先执行1,然后5,在异步串行队列内部,是与外部一样的道理,

结果是:1 5 2 4 3

并发队列 异步同步嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)textDemo3 {
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

结果为 1 5 2 3 4
同步并发队列也不会开启新线程,一个一个执行。

串行 异步同步嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)textDemo4 {
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
NSLog(@"2");
// 同步
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}

上面已经有过异步串行、异步并行的例子了,只要异步函数内部有任务要执行,就属于耗时操作,会优先执行完毕外部的简单操作。所以先执行 1 5

在异步函数内部,继续串行执行。这时候会执行2,然后碰到了同步串行队列。而同步串行队列是需要等待外部执行完成之后才会执行,但是4也在等待同步函数的执行,造成了互相等待,发生了死锁。

这里即使把4注释掉,也同样会发生死锁。

所以结果为: 1 5 2 – 死锁

总结

我们一定要清楚,不管是异步还是同步,都是对与block块和其下一行代码来说的。在block内部不管当前是异步还是同步,串行还是并行,都是从上往下执行的。

另外并发和串行的区别:

  • 并发不会等待一个任务执行完成才执行。
  • 串行会等待一个任务执行完毕才执行。

同步和异步:
同步和异步是对当前线程而言的。

  • 异步函数下,不管是串行队列还是并行队列,都不影响block块之外的内存执。因为block内部是在新开启的线程中执行的。
  • 同步函数下,并行队列不受影响,因为并行不需要等待上一个任务执行完成。如果是串行队列,那在当前线程下会发生死锁。

主队列 & 全局队列

1
2
3
4
5
6
dispatch_queue_t serial = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t conque = dispatch_queue_create("conque", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);

NSLog(@"%@\n%@\n%@\n%@",serial,conque,mainQueue,globQueue);

打印一些这4个队列:

1
2
3
4
<OS_dispatch_queue_serial: serial>
<OS_dispatch_queue_concurrent: conque>
<OS_dispatch_queue_main: com.apple.main-thread>
<OS_dispatch_queue_global: com.apple.root.default-qos>

这里打印了4个队列,但是其实一共只有两个队列,就是串行队列和并发队列。

通过汇编手法,我们发现GCD的源码存在与libdispatch.dylib库中,我们就从这个库里看GCD的底层实现。

主队列

dispatch_get_main_queue()主队列专门用来在主线程上调度任务的串行队列,并不会开启新线程。

如果当前主线程正在执行任务,那么无论主队列中被添加了什么任务,都不会被调度执行。

1
dispatch_queue_t mainQueue = dispatch_get_main_queue();

我们通过源码查看

1
2
3
4
5
6
//
dispatch_queue_main_t
dispatch_get_main_queue(void)
{
return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}

dispatch_get_main_queue的解释中,我们发现:主队列依赖于主线程dispatch_main()runloop,并且主线程是在main()函数之前自动创建的(dyld的流程)。

先看看啥是dispatch_queue_main_t

A dispatch queue that is bound to the app’s main thread and executes tasks serially on that thread.

1
typedef struct dispatch_queue_static_s *dispatch_queue_main_t;

可以看出来OS_dispatch_queue_main是一个类。

那我们找一找DISPATCH_GLOBAL_OBJECT这个的实现:

1
#define DISPATCH_GLOBAL_OBJECT(type, object) ((OS_OBJECT_BRIDGE type)&(object))

这是一个宏定义,内部使用的是一个type类型强转之后与object进行二进制的”&“运算。

然后看看这两个参数:

dispatch_queue_main_t

The type of the default queue that is bound to the main thread
从字面意思就是把默认线程绑定到主线程。

_dispatch_main_q

Returns the default queue that is bound to the main thread.
返回一个绑定了主线程的默认线程。接下来我们通过源码看一下_dispatch_main_q

1
2
3
4
5
6
7
8
9
10
11
struct dispatch_queue_static_s _dispatch_main_q = {
DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
.do_targetq = _dispatch_get_default_queue(true),
#endif
.dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
DISPATCH_QUEUE_ROLE_BASE_ANON,
.dq_label = "com.apple.main-thread",
.dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
.dq_serialnum = 1,
};

_dispatch_main_q是一个结构体:

dq_label:使用的标签,上方代码中打印出来的东西。
dq_atomic_flags:是一个flag,DQF_WIDTH(1)表示宽度,1只能通过1个
dq_serialnum:串行数是1

知道了两个参数,我们直接使用”&“运算看是否能得到我们想要的主线程。

1
dispatch_queue_t mainQueue = (OS_OBJECT_BRIDGE dispatch_queue_main_t)&(_dispatch_main_q);

得到的这个mainQueue与上方的点结果是一直到。

接下来我们得验证一下,dispatch_get_main_queue是在main函数之前执行的。在dyld的流程中,我们知道他会执行一个libdispatch_init(void)的操作。在它的内部源码中有如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
libdispatch_init(void)
{
// ...
// line 7921
#if DISPATCH_USE_RESOLVERS // rdar://problem/8541707
_dispatch_main_q.do_targetq = _dispatch_get_default_queue(true);
#endif
// 设置当前线程
_dispatch_queue_set_current(&_dispatch_main_q);
// 绑定线程
_dispatch_queue_set_bound_thread(&_dispatch_main_q);
// ...
}

在第7758行代码,可以看到创建了 _dispatch_main_q静态结构体,之后设置当前线程为为主线程,然后进行绑定。

全局队列

dispatch_get_global_queue(0,0),为了方便使用,苹果创建了全局队列,全局队列是一个并发队列

在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列。

1
2
dispatch_queue_global_t
dispatch_get_global_queue(intptr_t identifier, uintptr_t flags);

这里有两个参数:
第一个identifier:表示优先级,与QOS的优先级一一对应。

1
2
3
4
DISPATCH_QUEUE_PRIORITY_HIGH        // 高
DISPATCH_QUEUE_PRIORITY_DEFAULT // 默认
DISPATCH_QUEUE_PRIORITY_LOW // 低
DISPATCH_QUEUE_PRIORITY_BACKGROUND // BACKGROUND

第二个参数是flag:
保留供将来使用的标志。始终将此参数指定为0。

接下来,查看一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_queue_global_t
dispatch_get_global_queue(long priority, unsigned long flags)
{
dispatch_assert(countof(_dispatch_root_queues) ==
DISPATCH_ROOT_QUEUE_COUNT);

if (flags & ~(unsigned long)DISPATCH_QUEUE_OVERCOMMIT) {
return DISPATCH_BAD_INPUT;
}
dispatch_qos_t qos = _dispatch_qos_from_queue_priority(priority);
#if !HAVE_PTHREAD_WORKQUEUE_QOS
if (qos == QOS_CLASS_MAINTENANCE) {
qos = DISPATCH_QOS_BACKGROUND;
} else if (qos == QOS_CLASS_USER_INTERACTIVE) {
qos = DISPATCH_QOS_USER_INITIATED;
}
#endif
if (qos == DISPATCH_QOS_UNSPECIFIED) {
return DISPATCH_BAD_INPUT;
}
return _dispatch_get_root_queue(qos, flags & DISPATCH_QUEUE_OVERCOMMIT);
}

这一坨东西其实都不用看,只需要看到最后return _dispatch_get_root_queue()是这么个东西。

1
2
3
4
5
6
7
8
static inline dispatch_queue_global_t
_dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
{
if (unlikely(qos < DISPATCH_QOS_MIN || qos > DISPATCH_QOS_MAX)) {
DISPATCH_CLIENT_CRASH(qos, "Corrupted priority");
}
return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
}

_dispatch_root_queues[]应该就是一个数组,通过传进来的参数获取对应的queue。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
struct dispatch_queue_global_s _dispatch_root_queues[] = {
// ...
.dq_atomic_flags = DQF_WIDTH(DISPATCH_QUEUE_WIDTH_POOL), \
_DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, 0,
.dq_label = "com.apple.root.maintenance-qos",
.dq_serialnum = 4,
),
_DISPATCH_ROOT_QUEUE_ENTRY(MAINTENANCE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.maintenance-qos.overcommit",
.dq_serialnum = 5,
),
_DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, 0,
.dq_label = "com.apple.root.background-qos",
.dq_serialnum = 6,
),
_DISPATCH_ROOT_QUEUE_ENTRY(BACKGROUND, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.background-qos.overcommit",
.dq_serialnum = 7,
),
_DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, 0,
.dq_label = "com.apple.root.utility-qos",
.dq_serialnum = 8,
),
_DISPATCH_ROOT_QUEUE_ENTRY(UTILITY, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.utility-qos.overcommit",
.dq_serialnum = 9,
),
_DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT, DISPATCH_PRIORITY_FLAG_FALLBACK,
.dq_label = "com.apple.root.default-qos",
.dq_serialnum = 10,
),
_DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
DISPATCH_PRIORITY_FLAG_FALLBACK | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.default-qos.overcommit",
.dq_serialnum = 11,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, 0,
.dq_label = "com.apple.root.user-initiated-qos",
.dq_serialnum = 12,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INITIATED, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.user-initiated-qos.overcommit",
.dq_serialnum = 13,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, 0,
.dq_label = "com.apple.root.user-interactive-qos",
.dq_serialnum = 14,
),
_DISPATCH_ROOT_QUEUE_ENTRY(USER_INTERACTIVE, DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
.dq_label = "com.apple.root.user-interactive-qos.overcommit",
.dq_serialnum = 15,
),
};

我们看到了lable的内容,有我们刚才打印的那个com.apple.root.default-qos。会根据我们设置的优先级返回不同的全局队列。

1
2
3
4
dq_atomic_flags = DQF_WIDTH(DISPATCH_QUEUE_WIDTH_POOL)

#define DISPATCH_QUEUE_WIDTH_FULL 0x1000ull
#define DISPATCH_QUEUE_WIDTH_POOL (DISPATCH_QUEUE_WIDTH_FULL - 1)

dq_atomic_flags的值也就是 (0x1000 - 1) = 4095

dispatch_queue_create 原理

直奔主题,在源码中查看dispatch_queue_create方法:

1
2
3
4
5
6
dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
return _dispatch_lane_create_with_target(label, attr,
DISPATCH_TARGET_QUEUE_DEFAULT, true);
}

这里传了两个参数,第一个是标签,表示创建的队列,第二个标识串行还是并发。

串行:DISPATCH_QUEUE_SERIAL

我们看一下源码:

1
#define DISPATCH_QUEUE_SERIAL NULL

所以,通常情况下,我们在创建串行队列时,也会使用NULL来替换。

并发:DISPATCH_QUEUE_CONCURRENT

我们看一下源码实现:

1
2
3
#define DISPATCH_QUEUE_CONCURRENT \
DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
_dispatch_queue_attr_concurrent)

这里有一个DISPATCH_GLOBAL_OBJECT()函数,在主队列中已经介绍过了(通过&运算)。

_dispatch_lane_create_with_target这个函数中,我们发现很长很难懂,那我们就通过多年的编程经验,看它返回的时候一个什么东西,然后看这个是怎么创建的。下面的代码是经过删减的,有需要的自行查看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
dispatch_queue_t tq, bool legacy)
{
// 1. 创建 dqai
dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
// ...
// 2. 创建vtable
const void *vtable;
dispatch_queue_flags_t dqf = legacy ? DQF_MUTABLE : 0;
if (dqai.dqai_concurrent) {
// OS_dispatch_queue_concurrent
vtable = DISPATCH_VTABLE(queue_concurrent);
} else {
vtable = DISPATCH_VTABLE(queue_serial);
}
// ...
// 3. label赋值
if (label) {
const char *tmp = _dispatch_strdup_if_mutable(label);
if (tmp != label) {
dqf |= DQF_LABEL_NEEDS_FREE;
label = tmp;
}
}
// 4. dq alloc分配内存空间
dispatch_lane_t dq = _dispatch_object_alloc(vtable,
sizeof(struct dispatch_lane_s));
// 5. dq init操作
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
(dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // init
// 6. 对dq进行赋值
dq->dq_label = label;
dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
dqai.dqai_relpri);
if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
}
if (!dqai.dqai_inactive) {
_dispatch_queue_priority_inherit_from_target(dq, tq);
_dispatch_lane_inherit_wlh_from_target(dq, tq);
}
_dispatch_retain(tq);
dq->do_targetq = tq;
_dispatch_object_debug(dq, "%s", __func__);
return _dispatch_trace_queue_create(dq)._dq;
}

创建dqai

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_queue_attr_info_t
_dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
{
dispatch_queue_attr_info_t dqai = { };

if (!dqa) return dqai;

#if DISPATCH_VARIANT_STATIC
if (dqa == &_dispatch_queue_attr_concurrent) { // null 默认
dqai.dqai_concurrent = true;
return dqai;
}
#endif
// ...
return dqai;
}

还记得我们在创建队列时传的参数吗?第一个是label,第二个是串行还是并发。

这个dqai会判断当前是串行还是并发,并对dqai.dqai_concurrent = true;进行赋值。

创建 vtable

vtable会根据当前是串行还是并发进行创建,我们一步一步的追寻vtable是什么。

1
2
3
4
5
6
7
8
#if OS_OBJECT_HAVE_OBJC2
#define DISPATCH_VTABLE(name) DISPATCH_OBJC_CLASS(name)
#define DISPATCH_OBJC_CLASS(name) (&DISPATCH_CLASS_SYMBOL(name))
#define DISPATCH_CLASS_SYMBOL(name) OS_dispatch_##name##_class
#elif
...
#end

1
2
3
4
5
6
if (dqai.dqai_concurrent) {
// OS_dispatch_queue_concurrent
vtable = DISPATCH_VTABLE(queue_concurrent);
} else {
vtable = DISPATCH_VTABLE(queue_serial);
}

通过源码发现,vtable就是一个类。最后生成的就是OS_dispatch_##name##_class

##name##就是创建vtable时的参数,就会生成对应的OS_dispatch_queue_serial_classOS_dispatch_queue_concurrent_class

label赋值

这个就是创建时传入的那个label标签的内容。

dq alloc分配内存空间

这里执行了alloc操作,开始分配内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_lane_t dq = _dispatch_object_alloc(vtable,
sizeof(struct dispatch_lane_s));


void *
_dispatch_object_alloc(const void *vtable, size_t size)
{
// 这个是在mac下执行
#if OS_OBJECT_HAVE_OBJC1
...
#else
// 这里分配内存,isa指向
return _os_object_alloc_realized(vtable, size);
#endif
}

真正的alloc操作是在这里执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline _os_object_t
_os_object_alloc_realized(const void *cls, size_t size)
{
_os_object_t obj;
dispatch_assert(size >= sizeof(struct _os_object_s));
// 开辟空间
while (unlikely(!(obj = calloc(1u, size)))) {
// 执行的都是likely的操作,所以不会走这里,这里也没有意义,内部是sleep操作
_dispatch_temporary_resource_shortage();
}
// isa指向
obj->os_obj_isa = cls;
return obj;
}

dq init操作

alloc之后,执行init操作。

初始化的时候会判断当前要生成并发还是串行队列,并发的话,个数是DISPATCH_QUEUE_WIDTH_MAX,串行是1,就是开辟的最大任务数。

对dq进行赋值

比如lable标签、overcommit,priority等赋值。同时绑定target。

最后return

1
return _dispatch_trace_queue_create(dq)._dq;

这里看源码都是最后返回的都是dq对应的数据。

dispatch_async 源码

我们接下来看一下异步并发队列函数的源码。

1
2
3
dispatch_async(conque, ^{
NSLog(@"12334");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
void
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

void
dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
{
dispatch_continuation_t dc = _dispatch_continuation_alloc();
uintptr_t dc_flags = DC_FLAG_CONSUME;
dispatch_qos_t qos;
// 任务包装器,只有这里有对work的操作
qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
_dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}

dispatch_async()函数内部会执行_dispatch_continuation_init,这个是函数中的重点。看一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static inline dispatch_qos_t
_dispatch_continuation_init(dispatch_continuation_t dc,
dispatch_queue_class_t dqu, dispatch_block_t work,
dispatch_block_flags_t flags, uintptr_t dc_flags)
{
// 1. work就是外部的block,这里ctxt是对block的一个copy操作
void *ctxt = _dispatch_Block_copy(work);

dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
if (unlikely(_dispatch_block_has_private_data(work))) {
// dc_flags赋值
dc->dc_flags = dc_flags;
// block赋值到dc_ctxt中
dc->dc_ctxt = ctxt;
// will initialize all fields but requires dc_flags & dc_ctxt to be set
return _dispatch_continuation_init_slow(dc, dqu, flags);
}
// 所以会走这里,func可以理解为work的方法名。
dispatch_function_t func = _dispatch_Block_invoke(work);
if (dc_flags & DC_FLAG_CONSUME) {
// 设置方法
func = _dispatch_call_block_and_release;
}
// 这里又是重点内容
return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}

我们进一步查看_dispatch_continuation_init_f源码,其内部主要是为了保存block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static inline dispatch_qos_t
_dispatch_continuation_init_f(dispatch_continuation_t dc,
dispatch_queue_class_t dqu, void *ctxt, dispatch_function_t f,
dispatch_block_flags_t flags, uintptr_t dc_flags)
{
// 默认优先级0
pthread_priority_t pp = 0;
// 设置dc_flags
dc->dc_flags = dc_flags | DC_FLAG_ALLOCATED;
// 设置方法
dc->dc_func = f;
// 方法实现。
dc->dc_ctxt = ctxt;
// 设置优先级
if (!(flags & DISPATCH_BLOCK_HAS_PRIORITY)) {
pp = _dispatch_priority_propagate();
}
_dispatch_continuation_voucher_set(dc, flags);
// 对block调用的优先级处理
return _dispatch_continuation_priority_set(dc, dqu, pp, flags);
}

以上内容呢,是qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);的内部实现,接下来我们再看下一句代码
_dispatch_continuation_async

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void
_dispatch_continuation_async(dispatch_queue_class_t dqu,
dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
{
#if DISPATCH_INTROSPECTION
if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
_dispatch_trace_item_push(dqu, dc);
}
#else
(void)dc_flags;
#endif
return dx_push(dqu._dq, dc, qos);
}

这里主要执行的就是dx_push。我们全局搜了一下,它是一个宏。

1
#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)

接下来,就有点迷茫了,dq_push是个什么鬼东西?全局搜一下。

我们发现,dq_push的内容是根据当前类型赋值的,比如是串行,那就是一个_dispatch_lane_push,我们这里使用的并发队列,所以,应该执行的是_dispatch_lane_concurrent_push

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
_dispatch_lane_concurrent_push(dispatch_lane_t dq, dispatch_object_t dou,
dispatch_qos_t qos)
{
// 一堆条件判断
if (dq->dq_items_tail == NULL &&
!_dispatch_object_is_waiter(dou) &&
!_dispatch_object_is_barrier(dou) &&
_dispatch_queue_try_acquire_async(dq)) {
// 我们先看看这个东西
return _dispatch_continuation_redirect_push(dq, dou, qos);
}

// 最后会执行到这里。
_dispatch_lane_push(dq, dou, qos);
}

经过一系列判断,执行到_dispatch_continuation_redirect_push.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void
_dispatch_continuation_redirect_push(dispatch_lane_t dl,
dispatch_object_t dou, dispatch_qos_t qos)
{
if (likely(!_dispatch_object_is_redirection(dou))) {
// 这里会生成_dc,内部就不细说了,主要是为了绑定block,target
dou._dc = _dispatch_async_redirect_wrap(dl, dou);
} else if (!dou._dc->dc_ctxt) {
// 如果没有实现,赋值一个
dou._dc->dc_ctxt = (void *)
(uintptr_t)_dispatch_queue_autorelease_frequency(dl);
}
// 这里指向target
dispatch_queue_t dq = dl->do_targetq;
if (!qos) qos = _dispatch_priority_qos(dq->dq_priority);
// 又来了一个dx_push
dx_push(dq, dou, qos);
}

看到这里就有疑惑了,上一步刚执行了`dx_push·,怎么这里有来了一个?

其实就好比Person继承自NSObject,比如实现init方法,会通过isa指向父类,调用父类的方法,这里也是一样的,通过do_targetq指向父类,执行父类的方法。父类就是_dispatch_root_queue_push

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
_dispatch_root_queue_push(dispatch_queue_global_t rq, dispatch_object_t dou,
dispatch_qos_t qos)
{
#if DISPATCH_USE_KEVENT_WORKQUEUE
dispatch_deferred_items_t ddi = _dispatch_deferred_items_get();
// 这里不是重点内容,不需要看
if (unlikely(ddi && ddi->ddi_can_stash)) {...}
#endif
#if HAVE_PTHREAD_WORKQUEUE_QOS
if (_dispatch_root_queue_push_needs_override(rq, qos)) {
return _dispatch_root_queue_push_override(rq, dou, qos);
}
#else
(void)qos;
#endif
_dispatch_root_queue_push_inline(rq, dou, dou, 1);
}

重点也就是在_dispatch_root_queue_push_inline

1
2
3
4
5
6
7
8
9
static inline void
_dispatch_root_queue_push_inline(dispatch_queue_global_t dq,
dispatch_object_t _head, dispatch_object_t _tail, int n)
{
struct dispatch_object_s *hd = _head._do, *tl = _tail._do;
if (unlikely(os_mpsc_push_list(os_mpsc(dq, dq_items), hd, tl, do_next))) {
return _dispatch_root_queue_poke(dq, n, 0);
}
}

在这个函数内部执行_dispatch_root_queue_poke,这个函数内部其实也就是一个_dispatch_root_queue_poke_slow方法。是整个dispatch中相当重要的一环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void
_dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor) {
...
// 这里执行跟类queue的初始化,内部是一个dispatch_once,只会初始化一次。单利,下一章介绍
_dispatch_root_queues_init();
...
// 如果是Global类型的函数,直接返回了。
if (dx_type(dq) == DISPATCH_QUEUE_GLOBAL_ROOT_TYPE)
#endif
{
_dispatch_root_queue_debug("requesting new worker thread for global "
"queue: %p", dq);
r = _pthread_workqueue_addthreads(remaining,
_dispatch_priority_to_pp_prefer_fallback(dq->dq_priority));
(void)dispatch_assume_zero(r);
return;
}
...
// 这中间省略的代码是判断remaining数,也就是需要创建的线程数。
do {
_dispatch_retain(dq); // released in _dispatch_worker_thread
// 循环创建线程
while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
if (r != EAGAIN) {
(void)dispatch_assume_zero(r);
}
_dispatch_temporary_resource_shortage();
}
} while (--remaining);
...
} while (!os_atomic_cmpxchgv2o(dq, dgq_thread_pool_size, t_count,
t_count - remaining, &t_count, acquire));

这个是GCD内部相当重点的一个点,首先进行root_queues的初始化,然后创建线程来执行任务。

1
2
3
4
5
6
7
_dispatch_root_queues_init();

_dispatch_root_queues_init(void)
{
dispatch_once_f(&_dispatch_root_queues_pred, NULL,
_dispatch_root_queues_init_once);
}

初始化函数内部调用的dispatch_once_f,只会执行一次,这一内容,下一章会有介绍。_dispatch_root_queues_init_once重点要看的是这个内部是个啥。

_dispatch_root_queues_init_once的内部实现代码就不放出来了,太长了,主要的作用就是创建与线程直接的依赖,同时关联线程的回调方法_dispatch_worker_thread2

root_queues初始化完成之后,再创建线程,但是内部是怎么调用block实现的,下一章有介绍。

接下来,我们返回到_dispatch_lane_concurrent_push这里,也就是连续的dq_push之后,最终会执行_dispatch_lane_push

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void
_dispatch_lane_push(dispatch_lane_t dq, dispatch_object_t dou,
dispatch_qos_t qos)
{
dispatch_wakeup_flags_t flags = 0;
struct dispatch_object_s *prev;

if (unlikely(_dispatch_object_is_waiter(dou))) {
return _dispatch_lane_push_waiter(dq, dou._dsc, qos);
}

dispatch_assert(!_dispatch_object_is_global(dq));
qos = _dispatch_queue_push_qos(dq, qos);

...

os_mpsc_push_update_prev(os_mpsc(dq, dq_items), prev, dou._do, do_next);
if (flags) {
// 这里的重点是wakeup
return dx_wakeup(dq, qos, flags);
}
}

而dx_wakeup与dx_push如出一辙,都是宏定义,根据当前队列进行赋值,这里就不细说了,直接选择root类型的方法。

由于代码巨大,这里直接放了截图。

最后执行的是wakeup,要保持线程是清醒的,其实就是为了保活。直到block执行完毕。没有target没有上一层之后,执行release操作。

这个也就是dispatch_async的实现。下一章会继续block是如何调用的。

总结

  1. GCD的介绍

  2. 同步、异步函数的介绍

  3. 串行队列、并发队列

  4. 函数与队列的4种组合,以及面试题

    1. 并发不会等待一个任务执行完成才执行。
    2. 串行会等待一个任务执行完毕才执行。
    3. 异步函数下,不管是串行队列还是并行队列,都不影响block块之外的任务执。因为block内部是在新开启的线程中执行的。
    4. 同步函数下,并行队列不受影响,因为并行不需要等待上一个任务执行完成。如果是串行队列,那在当前线程下会发生死锁。
  5. 主队列dispatch_get_main_queue,全局队列dispatch_get_global_queue内部实现

  6. dispatch_queue_create创建一个队列的原理

  7. dispatch_async内部实现,异步会创建线程,然后进行weakup保活操作,block执行完成之后进行释放。

引用

libdispatch源文件
这里是用的是libdispatch-1271.40.12.tar.gz文件。

base64 补充

Base64就是一种基于64个字符来表示二进制数据的方法。没6个比特为一个单元,

具体可以查看base64的解释

64个字符包括 A-Z a-z 0-9 + /,再加上=用来补位,加上【等号】就是65个。
64个字符分别对应 0 - 63 这64个数字,64个数字对应着4个6位二进制数。

下方代码是在iOS中的一种编码、解码方式:

1
2
3
4
5
6
7
8
9
10
//编码
- (NSString *)base64Encode:(NSString *)string{
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
return [data base64EncodedStringWithOptions: 0];
}
//解码
- (NSString *)base64Decode:(NSString *)string{
NSData *data = [[NSData alloc] initWithBase64EncodedString:string options:0];
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
  1. base64只适用于表示二进制文件。
  2. base64编码后,文件数量变多,不使用与大型数据。
  3. base64和数据一一对应,不安全。

Hash

Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。

这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。

hash不是加密算法。

Hash特点

  • 算法公开
  • 对相同数据运算,得到的结果是一样的
  • 对不同数据运算,得到的结果是定长的,如MD5得到的结果默认是128位,32个字符(16进制标识)。
  • 无法逆运算
  • 信息摘要,信息“指纹”,是用来做数据识别的

常见的散列算法

常见的就是MD5,SHA等等。

MD5

MD5消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。

MD5由美国密码学家罗纳德·李维斯特(Ronald Linn Rivest)设计,于1992年公开,用以取代MD4算法。

1996年后被证实存在弱点,可以被加以破解,对于需要高度安全性的资料,专家一般建议改用其他算法。

2004年,证实MD5算法无法防止碰撞攻击,因此不适用于安全性认证,如SSL公开密钥认证或是数字签名等用途。

md5现常用于文件校验。

SHA

安全散列算法(英语:Secure Hash Algorithm,缩写为SHA)是一个密码散列函数家族,是FIPS所认证的安全散列算法。能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的算法。且若输入的消息不同,它们对应到不同字符串的几率很高。

SHA家族的算法分别为:

  1. SHA-0:1993年发布,是SHA-1的前身。
  2. SHA-1:1995年发布,SHA-1在许多安全协议中广为使用,包括TLS、GnuPG、SSH、S/MIME和IPsec,是MD5的后继者。但SHA-1的安全性在2010年以后已经不被大多数的加密场景所接受。
  3. SHA-2:2001年发布,包括SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。SHA-2目前没有出现明显的弱点。虽然至今尚未出现对SHA-2有效的攻击,但它的算法跟SHA-1基本上仍然相似。
  4. SHA-3:2015年正式发布,由于对MD5出现成功的破解,以及对SHA-0和SHA-1出现理论上破解的方法,NIST感觉需要一个与之前算法不同的,可替换的加密散列算法,也就是现在的SHA-3。

MD5 - SHA对比

Hash 用途

  • 用户密码加密
  • 搜索引擎
  • 版权
  • 数字签名

密码加密逻辑:

客户端在注册账号密码时,是必须加密的,但是怎么能保证密码的安全,那最好就是所有人都不知道,前后端都不知道密码,所以之前说的RSA加密就不行了,只要知道公钥或者私钥就可以破解了。

常用的加密方式有以下4种:

  1. 直接使用MD5
  2. MD5加盐
  3. HMAC加密
  4. 在HAC加密方案上添加时间戳等方案

直接使用MD5

将密码等重要的文本内容直接使用md5进行加密,但是md5现在也不安全,大部分md5加密之后都可以被破解。md5在线破解

md5现在常用于文件校验。

所以有了第二种方式:

MD5加盐

直接使用md5加密不算安全,那么就在文本上直接拼接一串字符串(盐、salt值),这样就可以防止被破解,但是如果添加的字符串泄露了,也通用会造成数据泄露的风险。

所以通常情况下,这个salt值都是由服务端生成的,每一个用户过来就对应的生成一个salt值,这种方式已经比第一种安全很多了。

那如果用户更换了设备,就需要先拿到salt值,然后再次输入密码,还是有可能被暴利破解。

所以有了第三种方式:

HMAC加密

HMAC被称为:密钥散列消息认证码。英语:Keyed-hash message authentication code),又称散列消息认证码(Hash-based message authentication code,缩写为HMAC)。

是一种通过特别计算方式之后产生的消息认证码(MAC),使用密码散列函数,同时结合一个加密密钥。它可以用来保证资料的完整性,同时可以用来作某个消息的身份验证。

通俗来讲,类似于我们现在的授权认证,比如当我们在新设备上登录微信,就需要老设备点击确认或者扫码操作,这种就是获取授权的一个操作。认证流程如下:

  1. 先由客户端向服务器发出一个验证请求。
  2. 服务器接到此请求后生成一个随机数并通过网络传输给客户端(此为挑战)。
  3. 客户端将收到的随机数提供给ePass,由ePass使用该随机数与存储在ePass中的密钥进行HMAC-MD5运算并得到一个结果作为认证证据传给服务器(此为响应)。
  4. 与此同时,服务器也使用该随机数与存储在服务器数据库中的该客户密钥进行HMAC-MD5运算,如果服务器的运算结果与客户端传回的响应结果相同,则认为客户端是一个合法用户。

这个过程是通过Hash运算得到一个值进行服务器端的验证。这种方式已经基于完美了,但还不够完美。

HMAC+时间戳

如果非法分子使用这种授权,模拟用户登录,那就会有问题了。通常会加上时间戳验证,这个授权认证需要在某一个时间范围内进行,超过了时间就会失败。从而大大增加安全性。

搜索引擎

我们经常使用百度搜索、谷歌搜索也会,有时候搜索出来的东西都是一样的。比如:

搜索:iOS NSStringNSString iOS是一样的。首先对这两个进行md5加密,得到一个结果:

1
2
3
"iOS" = 1bdf605991920db11cbdf8508204c4eb

"NSString" = e4263c36f49e2d937749bb3c6c7bbadb

这两个字符通过md5加密之后,相加得到的一个结果,所以不管顺序如何,得到的结果都是一样的。

版权问题

比如图片类型的网站,上传的第一份图片,就会生成一份原始的hash值。之后其他人下载之后使用,但是他们下载的不会是源文件,而是平台在内部做了处理重新生成的。

如果有人说我这个是正版的图片,你这个是盗版的,那就用原始文件进行对比处理。

数字签名

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。它是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。

数字签名是非对称密钥加密技术与数字摘要技术的应用。

数字签名的实现

数字签名算法是依靠公钥加密技术来实现的。在公钥加密技术里,每一个使用者有一对密钥:一把公钥和一把私钥。公钥可以自由发布,但私钥则秘密保存;还有一个要求就是要让通过公钥推算出私钥的做法不可能实现。

普通的数字签名算法包括三种算法:

  1. 一种密码生成算法
  2. 标记算法
  3. 验证算法

通常情况下,使用Hash+RSA的方式实现数字签名。需要注意的是,私钥是保密的,公钥可以自由发布。

总结

  • base64部分的补充
  • RSA终端命令
  • RSA特点
    • RSA安全系数非常高(整个业务逻辑非常安全)
    • 加密效率非常低(不能做大数据加密)
    • 用来加密关键数据
  • HASH特点
    • 不可逆运算
    • 相同的数据,结果相同
    • 不同的数据,长度相同
    • 一般用于做数据的识别(密码、版权)
  • md5及SHA
  • hash的应用:
    • 密码加密(HMAC + 时间戳)
    • 数字签名
      • 算法:RSA+HASH
      • 目的:验证数据的完整性,不被篡改
      • 步奏:1.原始数据的hash值,2.使用rsa加密hash值,3.将原始数据+数字签名一起打包发送传递。

引用

散列函数
SHA家族
md5在线破解
密钥散列消息认证码
数字签名-维基百科
数字签名-百度百科

内存主要分为5大区:

  1. 栈 stack
  2. 对 heap
  3. 全局区/静态区
  4. 常量区
  5. 代码区

这张图详细的介绍了5大分区的分配情况。

栈是从高地址向低地址开始分配,了解汇编的同学应该知道栈顶和栈底,这两个寄存器,栈顶处与低地址区,栈底处于高地址区。

堆是从低地址向高地址开始分配。在堆中获取数据相对比较麻烦,所以都是在栈中开辟空间指向堆。

当栈和堆有一方不断开辟空间,导致两个处于临界点时,就会发生堆栈溢出。

定义

1. 什么是线程

官方文档:线程编程指南

线程是在应用程序内部实现多个执行路径的相对轻量的方法。在系统级别,程序并排运行,系统根据每个程序的需求和其他程序的需求为每个程序分配执行时间。但是,每个程序中都存在一个或多个执行线程,这些线程可用于同时或以几乎同时的方式执行不同的任务。系统本身实际上管理着这些执行线程,调度它们在可用内核上运行,并根据需要抢先中断它们以允许其他线程运行。

  • 多个线程可以提高应用程序的感知响应能力。
  • 多线程可以提高应用程序在多核系统上的实时性能。

2. 什么是进程

进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内。

3. 两者的关系

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
  • 进程要想执行任务,必须得有线程,进程至少要有一条线程。
  • 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程。
  • 同一进程的线程共享本进程的地址空间,而进程之间则时独立的。
  • 同一进程内的线程共享进程的资源,如:内存、I/O、CPU等,而进程之间是独立的。
  • 一个进程崩溃后,保护模式下,不会对其他进程产生影响。一个线程崩溃则整个进程死掉。进程比线程健壮。
  • 进程切换时,消耗的资源大,效率低。设计频繁切换时,是哦那个线程好于进程。
  • 线程不能独立执行,必须依存于进程(应用程序)。
  • 线程时处理器调度的基本单位,进程不是。
  • 线程没有地址空间,线程保护在进程地址空间中。

4. 多线程的优点

  • 能适当提高程序的执行效率
  • 能适当提高资源的利用率(CPU,内存)
  • 线程上的任务执行完成后,线程会自动销毁

5. 多线程的缺点

  • 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占512KB)
  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,CPU 在调用线程上的开销就越大
  • 程序设计更加复杂,比如线程间的通信、多线程的数据共享
  • 多线程操作增加代码复杂度

6. 时间片

CPU在多个任务之间进行快速的切换,这个时间间隔就是时间片。

  • 单核CPU同一时间,CPU只能处理1个线程(只能有一个线程执行)
  • 多线程同时执行,是CPU快速在多个线程直接的切换,因为CPU调度线程的时间足够快,就造成了多线程的”同时“执行的效果。
  • 如果线程数非常多,CPU会在N个线程之间切换,消耗大量的CPU资源,线程的执行效率会降低。

7. 线程的声明周期

8. 线程池的调度

4种饱和策略

  • AbortPolicy 直接抛出RejectedExecutionExeception异常来阻止系统正常运行
  • CallerRunsPolicy 将任务回退到调用者
  • DisOldestPolicy 丢掉等待最久的任务
  • DisCardPolicy 直接丢弃任务

那么这里有一个问题,是不是创建的线程优先级越高,执行的就越快呢?

答案是不一定,需要根据线程要使用的资源,已经线程池的饱和程度来判断。

  1. 如果这个线程需要很大的资源,比如处理几个G和处理几KB效率肯定是不一样的。
  2. 如果线程池处于饱和状态,并且都在执行状态,是没有办法把正在执行的线程取消掉的。

这就需要锁来解决,

多线程解决方案:

常见的多线程有pthread、NSThread、GCD、NSOperation:

两个人买票,同一时间相同的操作,A买的时候有100张,B买的时候也有100张,那AB都买完的时候应该只剩下998张才对,但是如果不处理的情况下,会出现资源抢占的问题。

这就需要锁来解决。

1. atomic与nonatomic

atomic:原子属性(线程安全),针对多线程设计的,使用属性时默认是atomic,保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)。atomic本身就有一把锁(自旋锁) 单写多读:单个线程写入,多个线程可以读取。
nonatomic:非原子属性,非线程安全,适合内存小的移动设备。

iOS开发建议:
所有属性都声明为 nonatomic
尽量避免多线程抢夺同一块资源,尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力

自旋锁 与 互斥锁的区别

两个都是进行同步操作而产生的。

自旋锁:发现其他线程在执行,当前线程会一直询问(忙等),直到当前线程开始执行。消耗性能比较高。适用于任务复杂度较低的。
互斥锁:发现其他线程在执行,当前线程即刻进入休眠(就绪状态),已知等待被唤醒执行。对于任务复杂度较高,资源较大使用互斥锁。

之后会有详细的描述,这里只是引出相关内容。

NSPort通信

1
2
3
4
5
6
7
//1. 创建主线程的port
// 子线程通过此端口发送消息给主线程
self.myPort = [NSMachPort port];
//2. 设置port的代理回调对象
self.myPort.delegate = self;
//3. 把port加入runloop,接收port消息
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];

创建了NSPort之后,一定要加入到NSRunLoop中包活,否则没有效果。

  1. runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心 的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局 的字典里。
  2. runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务就会被唤醒去执行任务。
  3. runloop在第一次获取时被创建,在线程结束时被销毁。
  4. 对于主线程来说,runloop在程序一启动就默认创建好了。
  5. 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线 程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。

KVO

官方文档:Key-Value Observing

Important: In order to understand key-value observing, you must first understand key-value coding.

在官方的文档中,有这么一句话,要理解KVO,必须先知道KVC。

KVO的基本使用

下面创建一个Person类,并添加几个属性。

1
2
3
4
5
6
7
8
9
10
11

static void *PersonNameContext = &PersonNameContext;

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, copy) NSString *fullName;
@property (nonatomic, strong) NSMutableArray *mArray;

@end

1. 简单使用

KVO对实例变量是不起作用的。可以试一下,即使添加了set方法、添加了didChangeValueForKey:方法也不行,即使使用了KVC也监听不到。正常使用来说,还是针对属性。

1.1 添加监听

1
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

被监听者进行调用,添加一个监听对象,监听某一个属性的变化。context是上下文,在官方文档中,推荐使用context,不使用这个,也可以使用NULL代替。例如:

1
2
3
4
// 当前对象监听person对象的name属性的变化
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

//[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext]

1.2 监听变化

1
2
3
4
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//if (context == PersonNameContext) {}
NSLog(@"%@",change);
}

这里可以通过keyPath来判断某一个属性发生变化,如果监听了多个对象,并且有相同的属性,则在这里会添加一堆判断条件,会使代码变得臃肿,所以还是推荐使用context来判断。

1.3 移除KVO

添加了监听之后,再dealloc时一定要移除。

1
2
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

2. KVO中属性依赖

比如我们要监听fullName属性的变化,当namenickName中的一个发生变化时,都需要改变fullName的值,需要怎么处理?如果同时监听两个属性也不是不行,但是肯定还有其他更简便的方法。这就需要添加依赖。

Person.m中实现如下方法:

1
2
3
4
5
6
7
8
9
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {

NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"name", @"nickName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}

这样就可以实现依赖监听了,也不用实现重复代码。

3. KVO监听数组

我们实现对person.mArray的监听,但是当我们执行添加和删除操作时,方法并不会触发监听事件。

这也就时开始的时候所说的,KVO是基于KVC的,这个时候,我们利用KVC的方式获取数组就可以实现了。

1
2
// [self.person.mArray addObject:@"1"];
[[self.person mutableArrayValueForKey:@"mArray"] addObject:@"1"];

4. 自动、手动实现监听

1
2
// 自动开关
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key

这是一个系统方法,只需要重写即可,默认是YES,如果针对某些key返回了NO,则需要手动实现set方法。没有实现则不会监听到。

1
2
3
4
5
- (void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}

KVO底层原理

我们通过监听一个中的name属性的变化来判断监听前后会出现什么不同,来查看KVO的底层实现原理。

1
2
3
self.person = [Person new];

[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

我们在这一行代码添加一个断点,分别通过lldb打印当前person的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(lldb) po self.person
<Person: 0x60000294a010>
(lldb) po self.person->isa
Person
(lldb) po [self.person class]
Person
(lldb) po [self.person superclass]
NSObject

(lldb) po self.person
<Person: 0x60000294a010>
(lldb) po self.person->isa
NSKVONotifying_Person
(lldb) po [self.person class]
Person
(lldb) po [self.person superclass]
NSObject

我们发现虽然两次po self.person输出的都是Person类,指向的内存地址也是一样的,两次输出class和superClass确都相同。但是isa的指向却是完全不同,竟然变成了NSKVONotifying_Person

NSKVONotifying_Person是什么呢?怎么会创建一个这个东西,难道是Person的子类?
猜测应该是Person的子类。

为什么两次输出class和superClass都是一样的?
我们猜测可能是改写了class方法。

带着疑问,我们输出一下监听前后的方法列表,已经两个class的superClass。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-(void)viewDidLoad {
// 通过使用字符串的方式获取Class
Class cls1 = class_getSuperclass(objc_getClass("Person"));
[self printClassAllMethod:objc_getClass("Person")];
NSLog(@"cls1 = %@", cls1);

self.person = [[Person alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];

[self printClassAllMethod:objc_getClass("NSKVONotifying_Person")];
Class cls2 = class_getSuperclass(objc_getClass("NSKVONotifying_Person"));
NSLog(@"cls2 = %@", cls2);
}

#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls {
NSLog(@"----%@----", cls);
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);

NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 监听前
KVODemo[74347:5007124] ----Person----
KVODemo[74347:5007124] name-0x105ec6550
KVODemo[74347:5007124] .cxx_destruct-0x105ec6580
KVODemo[74347:5007124] setName:-0x105ec64f0
KVODemo[74347:5007124] cls1 = NSObject

// 监听后
KVODemo[74347:5007124] ----NSKVONotifying_Person----
KVODemo[74347:5007124] setName:-0x7fff207bab57
KVODemo[74347:5007124] class-0x7fff207b9662
KVODemo[74347:5007124] dealloc-0x7fff207b940b
KVODemo[74347:5007124] _isKVOA-0x7fff207b9403
KVODemo[74347:5007124] cls2 = Person

我们发现:

  • 监听前Person类中有3个方法,super是NSObject
  • 监听后Person类中有4个方法,super是Person

进一步验证了我们之前的猜测,NSKVONotifying_PersonPerson的子类,并且重些了setName:, class, dealloc方法,因为地址都已经发生了变化。

打印一下对应的IMP地址,看看所在的调用栈:

1
2
3
4
5
6
7
(lldb) po (IMP)0x105ec64f0
(KVODemo`-[Person setName:] at Person.m:12)

(lldb) po (IMP)0x7fff207bab57
(Foundation`_NSSetObjectValueAndNotify)
return class_getSuperclass(object_getClass(self));

也说明了,根本不是同一个IMP。当然_NSSetObjectValueAndNotify也不仅仅只有一种,使用~ nm Foundation | grep ValueAndNotify命令可以在iTerm2上查看对应的方法。比如:NSSetBoolValueAndNotify、NSSetIntValueAndNotify等等,根据当前属性的类型来判断的。

也正是因为重写了,setName:才会在外部调用时,person.name也会同时改变。
重新class方法,是为了不暴露NSKVONotifying_Person类,在外界调用时防止混淆。

自定义KVO

根据上面的分析,如果自定义KVO的话,我们需要从下面几个方面入手:

  1. 先判断key有没有set方法,有set方法才行。

  2. 动态生成子类:

    1. 判断是否已经存在子类,没有创建新的子类。
    2. 注册类
    3. 添加class方法,重新class方法
    4. 添加setter方法,重新set方法,这里需要处理消息,发送给父类,通知修改值。
  3. 修改isa指向

  4. 保存信息,方便回调。

  5. remove时,重新设置isa指向父类。

代码就不贴了,哪都有~

通过GNU解析

gnu源码

gnu源码可以清晰的看到整体的流程,只是读起来可能会稍微费力一点,查看代码中的重要逻辑其实就可以了。

FBKVOController

稍微说一下这个,内部实现还是很值得学习的。

我们直接看源码实现:

首先创建一个FBKVOController类型的实例变量。

1
2
3
4
5
6
7
8
9
10
11
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
_observer = observer;
NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
_objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
pthread_mutex_init(&_lock, NULL);
}
return self;
}

这里会生成一个NSMapTable类型的数据,里面存放的是<id, NSMutableSet<_FBKVOInfo *> *>这种格式的数据。

然后走到添加监听的方法,这里也没啥好说的,就是创建了一个_FBKVOInfo,存放系统KVO需要的所有东西,重点再下一句代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}

// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

// observe object with info
[self _observe:object info:info];
}

好了,重点来了。会从map表中查找对应的object是否有对应的数据。然后与新创建的info进行比较,没有则添加到map表中。

_FBKVOSharedController是一个单利,所有的观察者都通过它来进行监听,内部使用的系统的KVO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);

NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// check for info existence
_FBKVOInfo *existingInfo = [infos member:info];
if (nil != existingInfo) {
// observation info already exists; do not observe it again

// unlock and return
pthread_mutex_unlock(&_lock);
return;
}

// lazilly create set of infos
if (nil == infos) {
infos = [NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];
}

// add info and oberve
[infos addObject:info];

// unlock prior to callout
pthread_mutex_unlock(&_lock);

[[_FBKVOSharedController sharedController] observe:object info:info];
}

在系统方法接收到改变时,会通过block、方法或者系统方法来返回数据。

1
2
3
4
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSString *, id> *)change
context:(nullable void *)context

最后就是移除。需要注意的是,添加的时候是新创建了一个info,移除的时候,为啥也是新创建了一个info?

1
2
3
4
5
6
7
8
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath
{
// create representative info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath];

// unobserve object property
[self _unobserve:object info:info];
}

_unobserve:info:的内部实现与添加的时候有点类似,都是通过map去找对应的_FBKVOInfo。那新创建一个info能起到移除的效果吗?答案是肯定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);

// get observation infos
NSMutableSet *infos = [_objectInfosMap objectForKey:object];

// lookup registered info instance
_FBKVOInfo *registeredInfo = [infos member:info];

if (nil != registeredInfo) {
[infos removeObject:registeredInfo];

// remove no longer used infos
if (0 == infos.count) {
[_objectInfosMap removeObjectForKey:object];
}
}

// unlock
pthread_mutex_unlock(&_lock);

// unobserve
[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}

因为创建的临时变量info,是通过NSMutableSet获取member来获取的,是怎么获取到的。

Each element of the set is checked for equality with object until a match is found or the end of the set is reached. Objects are considered equal if isEqual: returns YES.

member:方法是通过isEqual:来判断是否是对应的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//_FBKVOInfo
- (NSUInteger)hash
{
return [_keyPath hash];
}

- (BOOL)isEqual:(id)object
{
if (nil == object) {
return NO;
}
if (self == object) {
return YES;
}
if (![object isKindOfClass:[self class]]) {
return NO;
}
return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}

重写了hash方法和isEqual:方法,这样就可以直接通过member获取了。这源码的设计思路简直爽的一批~~~网上找到FBKVO流程图:

网上找到FBKVO流程图

总结:

  1. KVO的变量依赖
  2. KVO的原理:
    1. 动态生成子类NSKVONotifying_A
    2. 注册类
    3. 动态添加class方法,返回父类
    4. 动态添加set方法,消息回传给父类,通知修改值
    5. 修改isa指向子类
    6. 移除KVO,修改isa执行父类
  3. GNU源码
  4. FBKVOController源码设计思路。

KVC

官方文档:About Key-Value Coding

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.

键值编码是由NSKeyValueCoding非正式协议启用的一种机制,对象采用这种机制来提供对其属性的间接访问。当对象是键值编码兼容的对象时,可以通过简洁,统一的消息传递接口通过字符串参数来访问其属性。这种间接访问机制补充了实例变量及其关联的访问器方法提供的直接访问。

KVC - API

1. 常用方法

  • 获取key对应的value:

    1
    - (nullable id)valueForKey:(NSString *)key;
  • 通过key来设置value:

    1
    - (void)setValue:(nullable id)value forKey:(NSString *)key;
  • 通过路径取值,一般情况下是model1中有一个model2,获取model2的属性值。

    1
    - (nullable id)valueForKeyPath:(NSString *)keyPath;
  • 获取对应路径的值,一般情况下是model1中有一个model2,设置model2的属性值。

    1
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
  • 获取一个可变类型:

    1
    2
    3
    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

    - (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
  • 默认返回YES,如果当前没有设置key对应的属性(没有找到set方法),会按照_key, _iskey, key, iskey的顺序搜索变量。如果返回NO,则不查询。

    1
    + (BOOL)accessInstanceVariablesDirectly;
  • 如果你在SetValue方法时面给Value传nil,则会调用这个方法

    1
    - (void)setNilValueForKey:(NSString *)key;
  • 如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。

    1
    2
    3
    - (nullable id)valueForUndefinedKey:(NSString *)key;

    - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
  • KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。

    1
    - (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

2. set、get流程

声明一个Person类,声明4个变量。注意这里没有添加属性(添加属性默认会生成set、get方法),就是为了验证set、get流程。

1
2
3
4
5
6
7
8
9
@interface Person : NSObject
{
NSString *_name; // 1.
NSString *_isName; // 2.
NSString *name; // 3.
NSString *isName; // 4.
}

@end

调用setValue:forKey方法,然后打印

1
2
3
4
5
6
7
8
9
10
11
12
Person *person = [[Person alloc] init];
// KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"kvc" forKey:@"name"];

// 1.
NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
// 2.
//NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
// 3.
//NSLog(@"%@-%@",person->name,person->isName);
// 4.
//NSLog(@"%@",person->isName);

分别按照顺序1-2-3-4,每次注释一个变量,每次只执行一句NSLog,看看打印结果:

1
2
3
4
5
6
7
8
// 1.
kvc-(nill)-(nill)-(nill)
// 2.
kvc-(nill)-(nill)
// 3.
kvc-(nill)
// 4.
kvc

为了进一步验证,可以在Person.m中,实现对应的set和get方法,分别打断点,按照1-2-3-4的顺序分别注释,可以进一步验证,set、get的流程。

setValue:forKey::按照set<key>, _set<key>, setIs<key>进行设置。有一个执行,其他的不执行。

注意: _setIs<key>这个方法不会执行。

valueForKey:按照get<key>, <key>, is<key>, _<key>顺序进行查找。有一个执行,不执行其他的。

这里有官方的设置key-value的流程:Accessor Search Patterns,写的很详细。

2.1 Get 流程

  1. 按照访问方法-get<Key>,-<key>,-is<Key>,-_<key>的顺序进行查找,如果找到执行步奏【5】。否则执行步奏【2】。
  2. 在实例中搜索①countOf<Key>,②objectIn<Key>AtIndex:(与NSArray类定义的原始方法<key>AtIndexes:相对应)或③objectsAtIndexes:(与NSArray方法相对应)。如果找到①,再找到②或③中的一个,则创建一个响应所有NSArray方法的集合代理对象并将其返回。否则,请继续执行步骤【3】。
  3. 如果找到了①countOf<Key>,没有找到②或③,那么会去找enumeratorOf<Key>memberOf<Key>:(对应NSSet类)。如果找到了所有三个方法,则创建一个响应所有NSSet方法的集合代理对象并将其返回。否则,请继续执行步骤【4】。
  4. 如果接收器的类方法+(BOOL)accessInstanceVariablesDirectly返回YES,则按照_<key>,_is<Key>,<key>,is<Key>的顺序搜索实例变量。如果找到,直接获取实例变量的值,然后继续执行步骤【5】。否则,继续执行步骤【6】。
  5. 如果获取到的变量是对象指针,则只需返回结果。
    如果该值是可以转换位NSNumber类型,则将其存储在NSNumber实例中并返回该实例。
    如果结果是NSNumber不支持的类型,请转换为NSValue对象并返回该对象。
  6. 如果其他所有方法均失败,则调用valueForUndefinedKey:。默认情况下会引发异常。

2.2 Set流程

  1. 按此顺序查找第一个名为set<Key>:, _set<Key>:, setIsName:的set方法。如果找到,调用它并完成。
  2. 如果没有找到,如果类方法+(BOOL)accessInstanceVariablesDirectly返回YES,则按照顺序_<key>,_is<Key>,<key>,is<Key>查找实例变量。如果找到,则直接对变量进行赋值。
  3. 如果步奏【1】和【2】都失败了,则调用setValue:forUndefinedKey:。默认情况下会引发异常。

集合类型

1. 集合类型

Person类中,声明一个不可变数组

1
@property (nonatomic, copy) NSArray *array;

对不可变类型进行赋值时,可以使用mutableArrayValueForKey先获取一个可变数组,然后直接赋值就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
person.array = @[@"1",@"2",@"3"];
// 修改数组
// person.array[0] = @"100";// 这种方式不可用
// 1. 获取一个新的数组 - KVC 赋值
NSArray *array = [person valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);

// 2. 使用mutableArrayValueForKey
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);

输出结果:

1
2
3
4
5
6
7
8
9
10
2021-05-09 10:17:58.004460+0800 KVCDemo[70852:4744247] (
100,
2,
3
)
2021-05-09 10:17:58.005523+0800 KVCDemo[70852:4744247] (
200,
2,
3
)

如果声明的是一个可变数组,那通过[person valueForKey:@"mArray"];获取到的就是一个可变数组。

2. 集合类型set、get流程补充

直接上代码:这里使用的key是一个没有在类中声明的变量/属性pens

1
2
3
4
5
6
7
8
9
10
11
12
person.arr = @[@"pen0", @"pen1", @"pen2", @"pen3"];
// 直接运行,在这里会发生crash
NSArray *array = [person valueForKey:@"pens"];
NSLog(@"%@",[array objectAtIndex:1]);
NSLog(@"%d",[array containsObject:@"pen1"]);

// set 集合
person.set = [NSSet setWithArray:person.arr];
NSSet *set = [person valueForKey:@"books"];
[set enumerateObjectsUsingBlock:^(id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"set遍历 %@",obj);
}];

直接运行,会发生crash。

*** Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<LGPerson 0x6000024610c0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key pens.’

按照上面的流程分析,我们需要对NSArray和NSSet类型提供方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//MARK: - NSArray
// 个数
- (NSUInteger)countOfPens{
NSLog(@"%s",__func__);
return [self.arr count];
}

// 获取值
- (id)objectInPensAtIndex:(NSUInteger)index {
NSLog(@"%s",__func__);
return [NSString stringWithFormat:@"pens %lu", index];
}

//MARK: - set
// 个数
- (NSUInteger)countOfBooks{
NSLog(@"%s",__func__);
return [self.set count];
}

// 是否包含这个成员对象
- (id)memberOfBooks:(id)object {
NSLog(@"%s",__func__);
return [self.set containsObject:object] ? object : nil;
}

// 迭代器
- (id)enumeratorOfBooks {
// objectEnumerator
NSLog(@"%s",__func__);
return [self.arr reverseObjectEnumerator];
}

补充完整上述方法,即可正常运行,对数组进行操作。

结构体

声明一个结构体类型的属性。

1
2
3
4
5
typedef struct {
float x, y, z;
} ThreeFloats;

@property (nonatomic) ThreeFloats threeFloats;
1
2
3
4
5
6
7
8
9
ThreeFloats floats = {1.,2.,3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *value1 = [person valueForKey:@"threeFloats"];
NSLog(@"%@",value1);

ThreeFloats th;
[value1 getValue:&th];
NSLog(@"%f-%f-%f",th.x,th.y,th.z);

对于结构体类型的数据,需要先转化成NSValue类型。常量类型会先转化成NSNumber类型

自定义KVC

根据set、get流程分析,自定义主要分为以下几个流程,需要注意的是要做安全判断,防止发生异常。

1. kvc自定义set

  1. 判断key,value的情况
  2. 通过传进来的key生成对应的set方法。
  3. 判断生成的3种set方法是否可以被响应,可以被响应直接return。
  4. 判断accessInstanceVariablesDirectly是否返回YES。
  5. 判断4种实例变量是否存在,存在则赋值,否则crash异常处理。

2. KVC 自定义Get

  1. 判断key的值。
  2. 生成对应的-get<Key>,-<key>,-is<Key>,-_<key>方法。判断是否可以响应。
  3. 不响应判断get流程种NSArray的处理。
  4. 不想要判断get流程种NSSet的处理。
  5. 判断accessInstanceVariablesDirectly是否返回YES。
  6. 判断变量是否存在,存在直接返回。
  7. 异常处理。

自定义set、get完全是按照set和get的流程处理的。代码就不上了,太占地方。

补充,KVC的高级使用

1
2
3
4
5
6
7
8
9
10
11
NSArray *arrStr = @[@"1", @"10", @"100"];
NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];

for (NSString *str in arrCapStr) {
NSLog(@"%@", str);
}

NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber *length in arrCapStrLength) {
NSLog(@"%ld", (long)length.integerValue);
}

打印出来的结果:

1
2
3
4
5
6
1
10
100
1
2
3

还有关于model中嵌套model的也差不多类似,大家探索一下吧。

总结

  1. KVC可以间接访问私有变量。
  2. valueForKey返回key对应的类型数据。如果是不可变数组,通过mutableArrayValueForKey获取的也会是可变类型。
  3. setValue:forKey:, valueForKey:的流程。
  4. 自定义KVC。

OC代码的精髓其实就是objc_msgSend。而OC的反汇编其实就是查看其中的方法调用。

objc_msgSend有两个参数,第一个是id类型,第二个是SEL类型。id、SEL其实都是一个结构体,内部有isa指针,所以这两个在内存中占有8个字节。

1. OC汇编

声明一个Person类,并添加两个属性,一个类方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Person : NSObject
@property(nonatomic, copy) NSString * name;
@property(nonatomic, assign) int age;

+(instancetype)person;

@end

@implementation Person

+ (instancetype)person {
return [[self alloc] init];
}

@end

int main(int argc, char * argv[]) {
Person * p = [Person person];
return 0;
}

放在main函数里,直接调用类方法,生成一个临时变量。
打上断点,直接在汇编模式下进行debug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Demo`main:
...
...
0x10405a16c <+24>: adrp x8, 3
0x10405a170 <+28>: add x8, x8, #0x648 ; =0x648
-> 0x10405a174 <+32>: ldr x0, [x8]

0x10405a178 <+36>: adrp x8, 3
0x10405a17c <+40>: add x8, x8, #0x638 ; =0x638
0x10405a180 <+44>: ldr x1, [x8]
0x10405a184 <+48>: bl 0x10405a4d4 ; symbol stub for: objc_msgSend
0x10405a188 <+52>: mov x29, x29
0x10405a18c <+56>: bl 0x10405a4f8 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x10405a190 <+60>: add x8, sp, #0x8 ; =0x8
0x10405a194 <+64>: str x0, [sp, #0x8]
0x10405a198 <+68>: stur wzr, [x29, #-0x4]
0x10405a19c <+72>: mov x0, x8
0x10405a1a0 <+76>: mov x8, #0x0
0x10405a1a4 <+80>: mov x1, x8
0x10405a1a8 <+84>: bl 0x10405a510 ; symbol stub for: objc_storeStrong
...
...

这里隐藏了开辟栈空间和回收相关的代码。

objc_msgSend需要两个参数id和SEL,从上面的代码可以初步判断两个参数的值分别在x0、x1寄存器中。

首先我们看一下x0寄存器中的数据。按照老方法,adrp计算x8的地址是0x010405d648。x0的值存放在0x010405d648所指向的内存中。

1
2
3
4
5
6
7
8
(lldb) po 0x010405d648
<Person: 0x10405d648>
(lldb) x 0x010405d648
0x10405d648: 30 d7 05 04 01 00 00 00 68 d6 05 04 01 00 00 00 0.......h.......
0x10405d658: 08 00 00 00 08 00 00 00 10 00 00 00 08 00 00 00 ................

(lldb) po 0x010405d730
Person

我们确定了第一个参数是Person类。在看第二个参数:

1
2
3
4
5
(lldb) x 0x10405d638
0x10405d638: 05 3d 42 8f 01 00 00 00 00 00 00 00 00 00 00 00 .=B.............
0x10405d648: 30 d7 05 04 01 00 00 00 68 d6 05 04 01 00 00 00 0.......h.......
(lldb) po (SEL)0x018f423d05
"person"

没有毛病,就是一个方法person

不同的系统版本,实现的汇编是不一样,iOS11下,汇编对objc_alloc进行了优化,但是没有对init处理。

iOS14 :没有消息发送,直接objc_alloc_init
iOS11 : 一次消息发送,objc_alloc, objc_msgSend(self, init)
iOS9 : 两次消息发送,objc_msgSend(alloc),objc_msgSend(self, init)

1.1 objc_storeStrong

这里还看到一个这个东西,这个设计到oc源码的逻辑,我们稍微看一下。

1
2
3
4
5
6
7
8
9
10
11
void
objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}

这个函数需要两个参数,第一个是id *类型,这就是一个地址,第二个是id类型。我们反过来看汇编代码,看这两个变量,正常来说还是在x0、x1寄存器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// x8中存地址
0x10405a190 <+60>: add x8, sp, #0x8 ; =0x8
// 把x0寄存器的值放在x8中。
0x10405a194 <+64>: str x0, [sp, #0x8]
// 把0存起来
0x10405a198 <+68>: stur wzr, [x29, #-0x4]
// x0 = x8,是一个地址。
0x10405a19c <+72>: mov x0, x8
// x8置空
0x10405a1a0 <+76>: mov x8, #0x0
// x1 = 0
0x10405a1a4 <+80>: mov x1, x8
// 这里x0是一个地址, x1是个nil,两个变量
0x10405a1a8 <+84>: bl 0x10405a510 ; symbol stub for: objc_storeStrong

通过分析汇编,objc_storeStrong两个变量分别是一个地址,一个是nil。然后看一些源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
objc_storeStrong(id *location, id obj)
{
// location有值, obj = nil
id prev = *location;
// 不相等
if (obj == prev) {
return;
}
// 对nil进行retain
objc_retain(obj);
// 寻址之后置空,也就是把id对象置空
*location = obj;
// 释放
objc_release(prev);
}

所以这个函数不仅仅只是用来强引用的,还可以进行释放操作,在这里就是一个很明显的例子。

1.2 属性

我们在mian函数中,对实例对象p的两个属性进行赋值。

1
2
3
4
5
6
7
int main(int argc, char * argv[]) {
Person * p = [Person person];
p.name = @"name";
p.age = 18;

return 0;
}

在真机上执行一下,然后我们使用之前的Hopper工具进行看一下。

这里就很详细的标注了整个内容,看起来比读汇编代码省事很多。

3. block的汇编

在main函数中直接声明一个栈区的block,全局区的也是一样的道理。

1
2
3
4
5
6
int a = 10;
void(^block)(void) = ^() {
NSLog(@"block--%d",a);
};

block();

然后真机上运行。这里省去了很大一部分的代码,只拿了关键部分的逻辑。

1
2
3
4
Demo`main:
0x10052a0cc <+36>: adrp x10, 2
0x10052a0d0 <+40>: ldr x10, [x10]
-> 0x10052a0d4 <+44>: str x10, [sp, #0x8]

先获取x10寄存器的值,也就是0x10052c000,lldb调试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(lldb) x 0x10052c000
0x10052c000: 20 9a 44 29 02 00 00 00 0c c8 66 ef 01 00 00 00 .D)......f.....
0x10052c010: 18 a5 52 00 01 00 00 00 24 a5 52 00 01 00 00 00 ..R.....$.R.....
// 这是一个栈block
(lldb) po 0x10052c000
<__NSStackBlock__: 0x10052c000>

// 这里拿到的是0x10052c010地址指向的内存区域,这个就是block的invoke。
(lldb) dis -s 0x010052a518
0x10052a518: ldr w16, 0x10052a520
0x10052a51c: b 0x10052a500
0x10052a520: udf #0x0
0x10052a524: ldr w16, 0x10052a52c
0x10052a528: b 0x10052a500
0x10052a52c: udf #0xd
0x10052a530: ldr w16, 0x10052a538
0x10052a534: b 0x10052a500

这里看一下block的源码:

1
2
3
4
5
6
7
8
struct Block_layout {
void *isa; // 8个字节
volatile int32_t flags; // contains ref count //4个字节
int32_t reserved; // 4个字节
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};

block也是一个结构体,invoke所在的位置,就是isa之后的16个字节。所以我在内存中取的invoke的实现就是偏移了0x10。

接下来,我们用hopper看一下:

1. 指针

指针也就是内存地址,指针变量是用来存放内存地址的变量。不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。
可使用 & 运算符访问地址。

之前的文章中有过说明,指针在内存中占8个字节。
可以是用sizeof来打印指针的size。

1
2
3
4
5
6
void func() {
int *a;
a = (int *)100;
a ++;
printf("%d", a);
}

这里定义一个int类型的指针a,然后赋值位100,我们知道指针的size是8个字节,a++之后打印多少?

答案是104。是的,没有看错,这里是因为指针的自增和自减操作,与执行的数据类型的宽度有关。

如果a = (char *)100,则打印的就是101。

1
2
3
4
5
6
void func() {
int *a;
a = (int *)100;
a = a + 1;
printf("%d", a);
}

这个不是指针的自增、自减了,这个时候就跟指针的size有关了,打印108。

1
2
3
4
5
6
7
8
9
10
void func_add() {
int *a;
a = (int *)100;

int *b;
b = (int *)200;

int x = a - b;
printf("x = %d", x);
}

先说答案,打印的结果是x = -25

a - b = -100, 然后除以4就得到了这个结果。

指针的运算单位是执行的数据类型的宽度。

1.1 二级指针

1
2
3
4
5
6
void func() {
int **a;
a = (int **)100;
a = a + 1;
printf("%d", a);
}

这个时候a运算时,执行的类型就是 char *类型,这是一个指针,8个字节。所以结果就是108。

2. 指针的汇编

1
2
3
4
5
void func() {
int *a;
int b = 10;
a = &b;
}

按照我们正常的理解,上述代码的意思就是把b的地址给到a,这个时候*a=10

看一下上面的代码汇编之后是什么样子的。

1
2
3
4
5
6
7
8
9
10
11
12
Demo`func:
0x100206130 <+0>: sub sp, sp, #0x10 ; =0x10
// 1. x8 = sp + 0x4,x8指向这个位置
0x100206134 <+4>: add x8, sp, #0x4 ; =0x4
// 2. 局部变量,w9=10
0x100206138 <+8>: mov w9, #0xa
// 3. 把w9的值放在x8所在的地址上。
0x10020613c <+12>: str w9, [sp, #0x4]
// 4. 把x8存储的地址放在sp + 0x8的位置上。
-> 0x100206140 <+16>: str x8, [sp, #0x8]
0x100206144 <+20>: add sp, sp, #0x10 ; =0x10
0x100206148 <+24>: ret

通过lldb打印一下相关数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(lldb) register read sp
sp = 0x000000016fbff880

(lldb) register read x8
x8 = 0x000000016fbff884

(lldb) register read x9
x9 = 0x000000000000000a

// 打印一下x8寄存器里的内存地址情况,里头存的值是0xa
(lldb) x 0x000000016fbff884
0x16fbff884: 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x16fbff894: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

// x8的地址放在了sp+0x8的位置,打印一下内存,就是x8存储的地址。
(lldb) x 0x000000016fbff888
0x16fbff888: 84 f8 bf 6f 01 00 00 00 00 00 00 00 00 00 00 00 ...o............
0x16fbff898: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

2.1 数组

1
2
3
4
5
6
void func() {
int arr[5] = {1,2,3,4,5};
for (int i = 0; i < 5; i++) {
printf("%d\n", *(arr + i));
}
}

2.2 野指针

1
2
3
4
void func() {
char *p;
char a = *p;
}

通过代码,我们知道,只是把*p的值给了a
为什么会发生野指针呢?

1
2
3
4
5
6
7
8
9
Demo`func:
0x100812134 <+0>: sub sp, sp, #0x10 ; =0x10
// 1. 因为p是指针。把sp + 0x8的地址中的值给x8
-> 0x100812138 <+4>: ldr x8, [sp, #0x8]
// 2. 把x8寄存器中存的地址的值给w9
0x10081213c <+8>: ldrb w9, [x8]
0x100812140 <+12>: strb w9, [sp, #0x7]
0x100812144 <+16>: add sp, sp, #0x10 ; =0x10
0x100812148 <+20>: ret
  1. 第一步寻址操作,获取x8寄存器的值的地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    (lldb) register read sp
    sp = 0x000000016f5f3880

    // sp + 0x8 = 0x000000016f5f3888
    (lldb) x 0x000000016f5f3888
    0x16f5f3888: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
    0x16f5f3898: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

    (lldb) register read x8
    x8 = 0x0000000000000000

    这里发现 x8寄存器中存的地址是空,全是0。

  2. 把x8寄存器中地址所在的值给w9。寻址操作
    这里寻址是从0x00000000上找值,从空地址上找值,就会发生crash。

本篇使用的objc源码版本位818.2

1. clang介绍

Clang是一个由Apple主导的使用C++编写、基于LLVM、发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器。它与GNU C语言规范几乎完全兼容(当然,也有部分不兼容的内容, 包括编译命令选项也会有点差异),并在此基础上增加了额外的语法特性,比如C函数重载 (通过__attribute__((overloadable))来修饰函数),其目标(之一)就是超越GCC。

2013年4月,Clang已经全面支持C++11标准,并开始实现C++1y特性(也就是C++14,这是 C++的下一个小更新版本)。Clang将支持其普通lambda表达式、返回类型的简化处理以及更好的处理constexpr关键字。

1.1 clang的简单使用

我们通常想看代码的内部实现逻辑,通常会把源文件转换成cpp文件

1
clang -rewrite-objc main.m -o main.cpp
  • main.m 目标文件
  • main.cpp 转换后的文件

1.2 UIKit报错问题

当我们想转化带有UIKit相关的的东西时,上面的命令就会报错了。使用如下命令即可

clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.3.sdk ViewController.m

如果还会报错,多数是因为iPhoneSimulator14.3.sdk没有找到,则通过xcode-contents找到对应的sdk即可。

1.3 xcrun

xcode安装的时候顺带安装了xcrun命令,xcrun命令在clang的基础上进行了一些封装,要更好用一些。

  • 模拟器 - 使用如下命令
    xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

  • 真机
    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

2. 类

在main.m下创建一个Person类,然后通过上面的clang命令,找到我们需要的cpp文件。

1
2
3
4
5
6
7
@interface Person : NSObject
// 添加一个属性,方便确认这就是我们要找的类
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
@end

转化之后,在cpp文件里,我们找到了如下的结构体。

2.1 类的声明

1
2
3
4
5
6
7
8
9
// @interface Person : NSObject。声明
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};

struct NSObject_IMPL {
Class isa;
};

我们发现,一个对象,它本身就是一个结构体,内部有一个变量Class isa

1
2
3
4
5
6
7
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

通过objc的源码,我们找到了objc_object的定义,其内部就是一个Class isa。与我们clang编译之后的NSObject_IMPL是一致的。所以NSObject_IVARS就是我们经常说的isa指针。

我们经常使用id类型来声明变量时不用带*,就是因为在底层已经做了处理。

2.2 类的实现

1
2
3
4
5
6
7
8
9
// @implementation Person

static NSString * _I_Person_name(Person * self, SEL _cmd)
{ return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name)
{ objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }
// @end

我们在上面的代码里,看到了两个方法

  1. _I_Person_name:这是一个get方法,直接做了一个return操作。
  2. _I_Person_setName_:这是一个set方法,调用了objc_setProperty

2.2.1 set方法

通过objc的源码,我们查找objc_setProperty方法。

1
2
3
4
5
6
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

内部判断是通过copy还是mutableCopy,然后调用reallySetProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}

id oldValue;
id *slot = (id*) ((char*)self + offset);

if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}

if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}

objc_release(oldValue);
}

这里最主要的操作,就是对oldvalue进行release操作,新值进行retain操作。

这也是经常在面试时,经常会问的,声明一个@property内部有哪些操作的的答案:

  1. 自动创建带有_的变量。
  2. 自动实现set、get方法。

苹果的这种设计思路很值得我们学习。它提供了一个对外的接口供上层调用,其内部调用底层的方法。这样上层无论怎么变化,都不会影响底层接口及实现。

3. isa

我们应该还记得在【alloc、init、new】这一节中有callAlloc这个方法,这个方法有一步操作是进行对象关联。

1
2
3
4
5
6
7
8
9
10
obj->initInstanceIsa(cls, hasCxxDtor);

inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());

initIsa(cls, true, hasCxxDtor);
}

接下来就看看这里是怎么搞的。对代码进行了简化,如果有需要请自行查看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
// 这个是重点,创建一个isa_t,这个isa_t是啥呢?我们点进去看一下。
isa_t newisa(0);

// 以下代码可以等先看我isa_t之后再回过头来看。

// 对bits内容赋默认值
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
// 这里是关联对象,就是本节的重点内容,我们进去看这个setClass是怎么实现的。
newisa.setClass(cls, this);
newisa.extra_rc = 1;
isa = newisa;
}

这里我们先看下isa_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private: //这是个私有的,不会主动赋值,而是通过赋值别的变量(bits)时给的。
Class cls;

public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif

void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
}

上面的代码做了精简处理,看起来会容易点,这个其实就是一个【联合位域】
union是联合体。里面有一个struct。这种方式就是为了优化内存空间,在极少的内存情况下,来使用。举个例子来看一下:

如果我们需要声明一个car的类,定义4个属性,前后左右行驶。如果是int类型的数据,那就是需要4 * 4 = 16个字节的空间,也就是128位。但是如果使用联合位域的话,就可以极大的减少空间。只需要4位就可以了。

1
2
3
4
5
6
7
8
union car {
struct {
char forward; //1
char back; //1
char left; //1
char right; //1
}
}

也就是0000,第一个0代表的是前,第二个0代表后,依次类推。

知道了联合位域的大概情况,我们就看一下这个ISA_BITFIELD是个什么东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
define ISA_MASK        0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t unused : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)

注意:这个是ARM64下的存储,而使用(非M1芯片)电脑本地运行的的都是基于x86_64的,所以这里面的值存储的位置是有些变化的。

特别提一下shiftcls,在ARM64下是33位,在x86下是44位,导致magic开始的位置分别是36和47,这个位置一会有用到。

  • nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等,在iOS中,正常情况下生成的对象nonpointer都等于1。
  • has_assoc:关联对象标志位,0没有,1存在
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。oc中的dealloc
  • shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
  • magic:用于调试器判断当前对象是真的对象还是没有初始化的空
  • weakly_referenced:志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
  • unsed:不同版本的是deallocating,标志对象是否正在释放内存
  • has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
  • extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。

我们了解了isa是啥东西了之后,在回过头看看是怎么进行管理对象的。了解上面的代码之后,我们继续看setCalss是怎么实现的。

1
2
3
4
5
6
// 对代码进行了简化,
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
shiftcls = (uintptr_t)newCls >> 3;
}

是不是很不可思议,只是通过newCls向右偏移了3位。为啥偏移3位?
我们知道isa->shiftcls存储类指针的值。是从isa的内存里面第3位开始的。就这么简单。因为在内存里没有办法直接存储类名,所以通过存储数字替带。

3.1 验证isa指针的关联过程

Person *p = [[Person alloc] init]; 运行objc源码工程。
断点进入objc_object::initIsa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
// ① 创建newisa
isa_t newisa(0);

// ② 对bits内容赋默认值
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
// ③
newisa.has_cxx_dtor = hasCxxDtor;
// ④ 这里是关联对象,就是本节的重点内容,我们进去看这个setClass是怎么实现的。
newisa.setClass(cls, this);
newisa.extra_rc = 1;
isa = newisa;
}

当断点走到②的时候。我们输出一些newisa的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(lldb) p newisa
(isa_t) $1 = {
bits = 0
cls = nil
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 0
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}

继续执行下一步,仍然输出newisa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(lldb) p newisa
(isa_t) $5 = {
bits = 8303511812964353
cls = 0x001d800000000001
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 59
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}

发现有了变化,bits有初值了,cls也被赋值了,而且magic也被赋值了。这些都是默认值,我们上面说了isa的内部是64位的数据。我们把cls的值,放在二进制的计算器里,看看是什么内容。第一位1对应的是nonpointer=1

看这个图,第47位开始的6位数据是110111,这个二进制数是什么?正好是59。

之后,继续断点下一步。走到④。然后进到setClass方法内部,我们执行语句,看看cls偏移后的值。

1
2
3
4
(lldb) po (uintptr_t)newCls
(uintptr_t) $15 = 4295000320
(lldb) po (uintptr_t)newCls >> 3
536875040

然后继续下一步,打印newisa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
lldb) p newisa
(isa_t) $11 = {
bits = 8303516107964673
cls = Person
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 536875040
magic = 59
weakly_referenced = 0
unused = 0
has_sidetable_rc = 0
extra_rc = 0
}
}

嗯哼。。。。。是不是,就是这么牛。shiftcls是啥,存储类指针的值。也验证了我们上面说的,是从isa的内存里面第3位开始的。就这么简单。因为在内存里没有办法直接存储类名,所以通过存储数字替带。

我们继续执行,返回到_class_createInstanceFromZone这个函数里,
然后先停一停哈,不要走断点了哈~我们来通过object_getClass在来验证一下。

3.2 反向验证 ISA_MASK

我们通过object_getClass来反向验证isa指向。这里全部对代码进行了简化。如有需要请自行查看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}

inline Class
objc_object::getIsa()
{
if (fastpath(!isTaggedPointer())) return ISA();
}


inline Class
objc_object::ISA(bool authenticated)
{
ASSERT(!isTaggedPointer());
return isa.getDecodedClass(authenticated);
}

inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
uintptr_t clsbits = bits;
clsbits &= ISA_MASK;
return (Class)clsbits;
}

终于看到了结果了,最后就是通过bits & ISA_MASK来返回当前class的。还记得bits是啥吗?往上翻一下,bits是isa指针内部的第一个元素。所以我们按照这个&运算来验证一些,返回的数据是不是person

1
2
3
4
5
6
7
(lldb) x/4gx obj
0x10060d9b0: 0x011d800100008101 0x0000000000000000
0x10060d9c0: 0x0000000000000000 0x86c8f7c495bce30f

// 拿第一位的地址进行&运算,注意这里是在mac上,所以使用x86下的ISA_MASK值
(lldb) po 0x011d800100008101 & 0x00007ffffffffff8ULL
Person

以上就是isa的全部内容了。但是isa里面的这些东西是真的有用吗?肯定是有用的啊,我们从dealloc的函数实现去找到蛛丝马迹。

4.补充 dealloc

在objc源码中找到dealloc的方法。

1
2
3
4
5
6
7
8
9
10
11
- (void)dealloc {
_objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
ASSERT(obj);

obj->rootDealloc();
}

好了,见证奇迹的时候到了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?

if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor &&
#else
!isa.getClass(false)->hasCxxDtor() &&
#endif
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}

上面objc_object::rootDealloc中对isa的各个属性的值来判断是执行free操作或者object_dispose。free函数就不用多说了,来看看dispose操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
id 
object_dispose(id obj)
{
if (!obj) return nil;

objc_destructInstance(obj);
free(obj);

return nil;
}

void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
obj->clearDeallocating();
}

return obj;
}

这里就是整个的dealloc的流程。通过源码只是来加深对这些流程的印象。
到这里,对象的alloc、init、dealloc都已经出现了,接下来就是类相关了。