It all starts with a crash analysis on Fabric.

Crash Log

There are some sudden crashes on Fabric caused by downloading services using GCD group, as follows:

#0. Crashed: com.apple.main-thread
0  libdispatch.dylib              0x192759b3c dispatch_group_leave.cold.1 + 36
1  libdispatch.dylib              0x19272ad84 _dispatch_group_wake + 114
2  MTXX                           0x103be1af8 __38-[xxxxxx downloadCompletion]_block_invoke + 108 (xxxxxx.m:108)
3  libdispatch.dylib              0x192728b7c _dispatch_call_block_and_release + 32
4  libdispatch.dylib              0x192729fd8 _dispatch_client_callout + 20
5  libdispatch.dylib              0x192735cc8 _dispatch_main_queue_callback_4CF + 968
6  CoreFoundation                 0x1929ffcc8 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
7  CoreFoundation                 0x1929faa24 __CFRunLoopRun + 1980
8  CoreFoundation                 0x1929f9f40 CFRunLoopRunSpecific + 480
9  GraphicsServices               0x19cc8a534 GSEventRunModal + 108
10 UIKitCore                      0x196b85580 UIApplicationMain + 1940
11 MTXX                           0x105c6af10 main + 16 (main.m:16)
12 libdyld.dylib                  0x192878e18 start + 4
Copy the code

Based on previous experience, this is clearly a problem caused by the enter/leave mismatch of the GCD group. The dispatch_group_Enter function is explicitly used in conjunction with dispatch_group_leave.

/ *! * @function dispatch_group_enter * * @abstract * Manually indicate a block has entered the group * * @discussion * Calling this function indicates another block has joined the group through * a means other than dispatch_group_async(). Calls to this function must be * balanced with dispatch_group_leave(). * * @param group * The dispatch group to update. * The result of passing NULL in this parameter is undefined. */
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
void
dispatch_group_enter(dispatch_group_t group);
Copy the code

First analysis

Then, after careful review, it is found that there is indeed a bug that may cause dispatch_group_leave not to be executed. The code logic is roughly as follows, listing only some pseudocode that may be relevant to this article:

- (void)downloadURLs:(NSURL *)urls finishCompletion:(void(^)(NSURL *URL))finishCompletion { dispatch_group_t dispatchGroup = dispatch_group_create(); for (NSURL *url in urls) { dispatch_group_enter(dispatchGroup); [self downloadURL:url finishCompletion:^(NSURL *url, BOOL isSuccess) {// logiccode for download success // XXXXX dispatch_group_leave(dispatchGroup);}]; } dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ if (finishCompletion) { finishCompletion(); }}); } - (void)downloadURL:(NSURL *)url finishCompletion:(void(^)(NSURL * url, BOOL isSuccess))finishCompletion {// all logic, If -else judgment etc... Why the project code is older. DownloadItem * DownloadItem = [self downloadItemForURL:url]; If (the downloadItem is suspended) {// continue the download operation return; } // XXXXXX // trigger the actual download operation}Copy the code

Note that because the code is longer, the finishCompletion is not passed when the download continues, so the finishCompletion has no chance to execute. Therefore, the enter/leave of the group does not match. Modify the code as follows:

if(downloadItem is pausing) {// Continue the download operation
    downloadItem.finishCompletion = finishCompletion;
    return;
}
Copy the code

A discovery

After the change, the heart still feel not too dependable, really is such modification?

Consider this explanation:

dispatch_group_enter: Calling this function indicates another block has joined the group through a means other than dispatch_group_async(). Calls to this function must be balanced with dispatch_group_leave().

Or that the lack of dispatch_group_leave will cause a crash? Try it out in code:

The test code

The lack of dispatch_group_leave

- (void)group_leave_not_crash_1 {
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"global_queue block 1");
    });
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"global_queue block 2");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_group_notify");
    });
    
    NSLog(@"done");
}
Copy the code

Output:

done
global_queue block 1
global_queue block 2
Copy the code

There was no crash. There was some slap on the face.

The lack of dispatch_group_enter

- (void)group_leave_crash {
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"dispatch_group_notify main_queue block 1");
        dispatch_group_leave(group);
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"dispatch_group_notify main_queue block 2");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_group_notify");
    });
    
    NSLog(@"done");
}
Copy the code

Either excessive call to dispatch_group_leave will cause a crash.

The value of “dispatch_group_Enter” and “dispatch_group_leave” do not match strictly

- (void)group_leave_not_crash_2 {
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"global_queue block 1");
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"global_queue block 2");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_group_notify");
    });
    
    NSLog(@"done");
}

- (void)group_leave_not_crash_3 {
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"global_queue block 1");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"global_queue block 2");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_group_notify");
    });
    
    NSLog(@"done");
}
Copy the code

The output is:

done
global_queue block 1
global_queue block 2
dispatch_group_notify
Copy the code

Dispatch_group_enter and dispatch_group_LEAVE are not strictly one-to-one, but the notification block of dispatch_group_notify was executed successfully. This is kind of weird…

conclusion

  1. Only dispatch_group_Enter is used, but dispatch_group_leave is not used. But the logic of the code is problematic.
  2. When the “dispatch_group_enter” command is executed, the “dispatch_group_leave” command crashes
  3. The number of dispatch_group_Enter and dispatch_group_leave are not strictly matched, but they do not crash. But again, the code logic can be problematic.

Libdispatch source parsing

Analyzing the crash stack

The demo that lacks dispatch_group_enter is in dispatch_group_leave(group); Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) If a group object is printed, it is

.

Take a look at the call stack:

0x108e8fb30 <+0>:  pushq  %rbp
     0x108e8fb31 <+1>:  movq   %rsp, %rbp
     0x108e8fb34 <+4>:  subq   $0x20, %rsp
     0x108e8fb38 <+8>:  movq   %rdi, -0x8(%rbp)
     0x108e8fb3c <+12>: movq   %rsi, -0x10(%rbp)
     0x108e8fb40 <+16>: callq  0x108e900a8               ; symbol stub for: dispatch_group_create
     0x108e8fb45 <+21>: movq   %rax, -0x18(%rbp)
     0x108e8fb49 <+25>: movq   -0x18(%rbp), %rdi
     0x108e8fb4d <+29>: callq  0x108e900b4               ; symbol stub for: dispatch_group_leave
 ->  0x108e8fb52 <+34>: xorl   %ecx, %ecx
     0x108e8fb54 <+36>: movl   %ecx, %esi
     0x108e8fb56 <+38>: leaq   -0x18(%rbp), %rax
     0x108e8fb5a <+42>: movq   %rax, %rdi
     0x108e8fb5d <+45>: callq  0x108e900f6               ; symbol stub for: objc_storeStrong
     0x108e8fb62 <+50>: addq   $0x20, %rsp
     0x108e8fb66 <+54>: popq   %rbp
     0x108e8fb67 <+55>: retq
Copy the code
libdispatch.dylib`dispatch_group_leave:
     0x10f528955 <+0>:  movl   $0x4, %eax
     0x10f52895a <+5>:  lock
     0x10f52895b <+6>:  xaddq  %rax, 0x30(%rdi)
     0x10f528960 <+11>: cmpl   $-0x4, %eax
     0x10f528963 <+14>: jae    0x10f52896d               ; <+24>
     0x10f528965 <+16>: andl   $-0x4, %eax
     0x10f528968 <+19>: testl  %eax, %eax
     0x10f52896a <+21>: je     0x10f5289a3               ; <+78>
     0x10f52896c <+23>: retq
     0x10f52896d <+24>: addq   $0x4, %rax
     0x10f528971 <+28>: movq   %rax, %rsi
     0x10f528974 <+31>: movq   %rax, %rcx
     0x10f528977 <+34>: andq   $-0x4, %rcx
     0x10f52897b <+38>: testl  $0xfffffffc, %esi         ; imm = 0xFFFFFFFC
     0x10f528981 <+44>: cmovneq %rax, %rcx
     0x10f528985 <+48>: andq   $-0x3, %rcx
     0x10f528989 <+52>: cmpq   %rcx, %rax
     0x10f52898c <+55>: je     0x10f528999               ; <+68>
     0x10f52898e <+57>: movq   %rsi, %rax
     0x10f528991 <+60>: lock
     0x10f528992 <+61>: cmpxchgq %rcx, 0x30(%rdi)
     0x10f528997 <+66>: jne    0x10f528971               ; <+28>
     0x10f528999 <+68>: movl   $0x1, %edx
     0x10f52899e <+73>: jmp    0x10f5289af               ; _dispatch_group_wake
     0x10f5289a3 <+78>: pushq  %rbp
     0x10f5289a4 <+79>: movq   %rsp, %rbp
     0x10f5289a7 <+82>: movq   %rax, %rdi
     0x10f5289aa <+85>: callq  0x10f55a66d               ; dispatch_group_leave.cold1.
Copy the code
libdispatch.dylib`dispatch_group_leave.cold1.:
     0x10f55a66d <+0>:  movq   %rdi, %rax
     0x10f55a670 <+3>:  leaq   0x5bd6(%rip), %rcx        ; "BUG IN CLIENT OF LIBDISPATCH: Unbalanced call to dispatch_group_leave()"
     0x10f55a677 <+10>: movq   %rcx, 0x27ad2(%rip)       ; gCRAnnotations + 8
     0x10f55a67e <+17>: movq   %rax, 0x27afb(%rip)       ; gCRAnnotations + 56
 ->  0x10f55a685 <+24>: ud2
Copy the code

The key information for crashes is as follows, which also indicates that they are actually causing these calls and that they are consistent with the crash log on the Fabric.

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

dispatch_group_leave(group);
callq  0x10f55a66d               ; dispatch_group_leave.cold1.
"BUG IN CLIENT OF LIBDISPATCH: Unbalanced call to dispatch_group_leave()"
Copy the code

Therefore, we can be sure that the crash was also caused by excessive calls to the dispatch_group_leave function, so the first change was indeed wrong.

Excessive calls to dispatch_group_leave do crash, but why? To understand these above, only to study the source code of GCD.

dispatch_group_leave

Dispatch_group_leave source:

void
dispatch_group_leave(dispatch_group_t dg)
{
	// The value is incremented on a 64bits wide atomic so that the carry for
	// the -1 -> 0 transition increments the generation atomically.
	uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
			DISPATCH_GROUP_VALUE_INTERVAL, release);
	uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);

	if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {
		old_state += DISPATCH_GROUP_VALUE_INTERVAL;
		do {
			new_state = old_state;
			if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
				new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
				new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
			} else {
				// If the group was entered again since the atomic_add above,
				// we can't clear the waiters bit anymore as we don't know for
				// which generation the waiters are for
				new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
			}
			if (old_state == new_state) break;
		} while(unlikely(! os_atomic_cmpxchgv2o(dg, dg_state, old_state, new_state, &old_state, relaxed)));return _dispatch_group_wake(dg, old_state, true);
	}

	if (unlikely(old_value == 0)) {
		DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
				"Unbalanced call to dispatch_group_leave()"); }}Copy the code

The time when these calls appear is when old_value is 0. The OS_ATOMIC_ADD_ORIG2O operation is an addition operation. That is, the value “DISPATCH_GROUP_VALUE_INTERVAL” is added to the dg_bits field of the “dispatch_group_T” object, and the value before the addition is old_value.

Therefore, when old_value is already 0, the dispatch_group_leave call is executed again, which will trigger a crash of these calls.

dispatch_group_enter

There’s only one dispatch_group_Enter, and it doesn’t crash without a corresponding leave. Now, what happens if it’s a dispatch_group_Enter call?

void
dispatch_group_enter(dispatch_group_t dg)
{
	// The value is decremented on a 32bits wide atomic so that the carry
	// for the 0 -> -1 transition is not propagated to the upper 32bits.
	uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
			DISPATCH_GROUP_VALUE_INTERVAL, acquire);
	uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
	if (unlikely(old_value == 0)) {
		_dispatch_retain(dg); // <rdar://problem/22318411>
	}
	if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
		DISPATCH_CLIENT_CRASH(old_bits,
				"Too many nested calls to dispatch_group_enter()"); }}Copy the code

So this enter makes sense. The OS_ATOMIC_SUB_ORIG2O operation is a subtracting operation. That is, the value of “DISPATCH_GROUP_VALUE_INTERVAL” is subtracted from the dg_bits field of the “dispatch_group_T” object by a value of “DISPATCH_GROUP_VALUE_INTERVAL”. The old value before subtracting is “old_value”. When “old_value” is “DISPATCH_GROUP_VALUE_MAX” and the “dispatch_group_Enter” call is executed, these calls will crash.

Test it out:

- (void)group_enter_crash_1 { dispatch_group_t group = dispatch_group_create(); while (YES) { dispatch_group_enter(group); // <OS_dispatch_group: group[0x600003c73a70] = { xref = 1, ref = 2, count = 0, gen = 0, waiters = 0, notifs = 0 }> } }Copy the code

Crashes did occur, but it took a few seconds for the OS_atomic_sub_ORIG2O operation to occur in a significant number for the condition that olD_value is DISPATCH_GROUP_VALUE_MAX to occur. At this time, the key stack information is:

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    
dispatch_group_enter
callq  0x10e155687               ; dispatch_group_enter.cold1.
movl   %eax, %edi
callq  0x10e155697               ; dispatch_group_enter.cold2.
"BUG IN CLIENT OF LIBDISPATCH: Too many nested calls to dispatch_group_enter()"
Copy the code

Therefore, GCD’s group Enter /leave operation will add/subtract a field value, while avoiding these calls will be paired. This explains why dispatch_group_Enter and dispatch_group_leave do not exactly match each other and will not crash.

dispatch_group_create

The source code for dispatch_group_CREATE is as follows. Obviously, this is just an initialization operation, and then an initial value is assigned to the field value required by the corresponding group Enter /leave, which should be 0 in this case.

DISPATCH_ALWAYS_INLINE
static inline dispatch_group_t
_dispatch_group_create_with_count(uint32_t n)
{
	dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
			sizeof(struct dispatch_group_s));
	dg->do_next = DISPATCH_OBJECT_LISTLESS;
	dg->do_targetq = _dispatch_get_default_queue(false);
	if (n) {
		os_atomic_store2o(dg, dg_bits,
				-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
		os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
	}
	return dg;
}

dispatch_group_t
dispatch_group_create(void)
{
	return _dispatch_group_create_with_count(0);
}
Copy the code

Dispatch Group Enter/Leave principle

At this point, the principle of dispatch_group_Enter and dispatch_group_leave is basically clear. The value of dg_bits in a certain field of the dispatch_group_T object is subtracted by dispatch_group_Enter, and the value of dg_bits in the dispatch_group_T object is added by dispatch_group_leave. When executing the dispatch_group_leave command, ensure that dispatch_group_Enter is less than one. Balanced Call Is also called.

Second analysis

Based on the above analysis, it is clear that the first analysis is wrong.

Take a look at the timing of the call to finishCompletion in the actual operation of downloadURL:

- (void)downloadURL:(NSURL *)url finishCompletion:(void(^)(NSURL * url, BOOL isSuccess))finishCompletion {// all logic, If -else judgment etc... Why the project code is older. If (determines whether a download is in progress based on the URL and downloadItems) {// The corresponding operation return; } // DownloadItem * DownloadItem = [self downloadItemForURL:url]; // DownloadItem * DownloadItem = [self downloadItemForURL:url]; If (the downloadItem is suspended) {// continue the download operation return; } // XXXXXX // Trigger the actual download operation // 1. Use the url to build an NSURLRequest, build a AFDownloadRequestOperation again. / / 2. Build a downloadItem object based on the URL, pass in the download completion callback finishCompletion, and load it into the downloadItems dictionary. // 3. Set the CompletionBlock, which obtains the downloadItem according to the URL and executes its finishCompletion according to the condition // 4. Add to queue, Download a request [task setCompletionBlockWithSuccess: ^ (AFHTTPRequestOperation * operation, Id responseObject) {DownloadItem * DownloadItem = [self getDownloadItem: operation. Request the URL]; / / according to download state, Execute finishCompletion}] in downloadItem; }Copy the code

The e code uses the downloadItems dictionary to store the download wrapper object downloadItem, and finishCompletion is the download completion callback passed in from the outside.

self.downloadItems[url] = downloadItem;

- (DownloadItem *)getDownloadItem:(NSURL *)url
{
    return self.downloadItems[url.absoluteString];
}
Copy the code

Although the code is a bit old, the process seems to be ok… On second thought, however, the logic involving the downloadItems dictionary seems to be the easiest to bury, and that’s when you think about it.

The problem really lies in the downloadItems dictionary:

  1. Suppose you pass in the URL, build the downloadItem A object, pass in finishCompletion A, and save it to the downloadItems dictionary. Initiate download operation A based on the URL. This process is normal.
  2. Download the same URL again, normal will not be a problem, but what if there is a multithreaded scenario? The code does not thread-protect operations related to the downloadItems dictionary.
  3. Assume a multi-threaded scenario: use the same URL, may simultaneously meet the filter conditions, and trigger the actual download operation. That is, build the downloadItem B object, pass finishCompletion B, and save it to the downloadItems dictionary. Initiate download operation B based on URL.
  4. And then in the downloadItems dictionary, the downloadItem that corresponds to the URL goes from downloadItem A to downloadItem B.
  5. After both download operations are complete, the downloadItem can be retrieved according to the URL. At this time, only downloadItem B can be obtained and finishCompletion B, which contains a dispatch_group_leave operation, is executed. So both the download operation A and the download operation B trigger finishCompletion B. A dispatch_group_leave call is generated for the processes related to B, causing a crash.

Know the root cause is well done, change is also very simple, in callback setCompletionBlockWithSuccess download task to complete, do not remove downloadItem from downloadItems dictionary. Instead, you get the correct downloadItem by capturing the current local variable downloadItem.

conclusion

Most of the official iOS documentation is very well written. But there are also some individual, such as the GCD group, write too brief, let people easily understand. At this point, it is time to show me the code.

One More Thing

Knowing the principle of GCD Group Enter /leave, I believe that I will not make similar mistakes in the future. Finally, how exactly does the notification block in dispatch_group_notify trigger execution?

dispatch_group_t

The structure of dispatch_group_t has not been analyzed before.

typedef struct dispatch_group_s *dispatch_group_t;

struct dispatch_group_s {
	DISPATCH_OBJECT_HEADER(group);
	DISPATCH_UNION_LE(uint64_t volatile dg_state,
			uint32_t dg_bits,
			uint32_t dg_gen
	) DISPATCH_ATOMIC64_ALIGN;
	struct dispatch_continuation_s *volatile dg_notify_head;
	struct dispatch_continuation_s *volatile dg_notify_tail;
};
Copy the code

Dg_bits is the field value required by Enter /leave, which is also required in other GCD interfaces. Two dispatch_continuation_t objects, dg_notify_head and DG_notify_tail are related to the group Notification block, As you can see, the structure that encapsulates the Notification block is held in a group in the form of a linked list.

typedef struct dispatch_continuation_s {
	DISPATCH_CONTINUATION_HEADER(continuation);
} *dispatch_continuation_t;

// If dc_flags is less than 0x1000, then the object is a continuation.
// Otherwise, the object has a private layout and memory management rules. The
// layout until after 'do_next' must align with normal objects.
#defineDISPATCH_CONTINUATION_HEADER(x) \ union { \ const void *do_vtable; \ uintptr_t dc_flags; The \}; \ union { \ pthread_priority_t dc_priority; \ int dc_cache_cnt; \ uintptr_t dc_pad; The \}; \ struct voucher_s *dc_voucher; \ struct dispatch_##x##_s *volatile do_next; \ dispatch_function_t dc_func; \ void *dc_ctxt; \ void *dc_data; \ void *dc_other
Copy the code

The value of “dispatch_continuation_t” is not much, but it’s not commented on, so you can’t really tell.

dispatch_group_notify

Take a look at the source for dispatch_group_notify:

DISPATCH_ALWAYS_INLINE
static inline void
_dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
		dispatch_continuation_t dsn)
{
	uint64_t old_state, new_state;
	dispatch_continuation_t prev;

	dsn->dc_data = dq;
	_dispatch_retain(dq);

	prev = os_mpsc_push_update_tail(os_mpsc(dg, dg_notify), dsn, do_next);
	if (os_mpsc_push_was_empty(prev)) _dispatch_retain(dg);
	os_mpsc_push_update_prev(os_mpsc(dg, dg_notify), prev, dsn, do_next);
	if (os_mpsc_push_was_empty(prev)) {
		os_atomic_rmw_loop2o(dg, dg_state, old_state, new_state, release, {
			new_state = old_state | DISPATCH_GROUP_HAS_NOTIFS;
			if ((uint32_t)old_state == 0) {
				os_atomic_rmw_loop_give_up({
					return _dispatch_group_wake(dg, new_state, false); }); }}); }}DISPATCH_NOINLINE
void
dispatch_group_notify_f(dispatch_group_t dg, dispatch_queue_t dq, void *ctxt,
		dispatch_function_t func)
{
	dispatch_continuation_t dsn = _dispatch_continuation_alloc();
	_dispatch_continuation_init_f(dsn, dq, ctxt, func, 0, DC_FLAG_CONSUME);
	_dispatch_group_notify(dg, dq, dsn);
}

#ifdef __BLOCKS__
void
dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
		dispatch_block_t db)
{
	dispatch_continuation_t dsn = _dispatch_continuation_alloc();
	_dispatch_continuation_init(dsn, dq, db, 0, DC_FLAG_CONSUME);
	_dispatch_group_notify(dg, dq, dsn);
}
#endif
Copy the code

The execution of a notification block is obviously triggered by the _dispatch_group_wake call. If the dispatch_group_enter function is not executed before the dispatch_group_notify function is invoked, the _dispatch_group_wake function is directly triggered.

The dispatch_group_notify function uses the _dispatch_continuation_init function to store a dispatch_block_t object DB in the dispatch_group_t object dg.

_dispatch_continuation_init

In the _dispatch_continuation_init function, various initialization operations are performed on the dispatch_continuation_t object.

DISPATCH_ALWAYS_INLINE
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)
{
	pthread_priority_t pp = 0;
	dc->dc_flags = dc_flags | DC_FLAG_ALLOCATED;
	dc->dc_func = f;
	dc->dc_ctxt = ctxt;
	// in this context DISPATCH_BLOCK_HAS_PRIORITY means that the priority
	// should not be propagated, only taken from the handler if it has one
	if(! (flags & DISPATCH_BLOCK_HAS_PRIORITY)) { pp = _dispatch_priority_propagate(); } _dispatch_continuation_voucher_set(dc, flags);return _dispatch_continuation_priority_set(dc, dqu, pp, flags);
}

DISPATCH_ALWAYS_INLINE
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)
{
	void *ctxt = _dispatch_Block_copy(work);

	dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
	if (unlikely(_dispatch_block_has_private_data(work))) {
		dc->dc_flags = dc_flags;
		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);
	}

	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_ALWAYS_INLINE
static inline dispatch_qos_t
_dispatch_continuation_priority_set(dispatch_continuation_t dc,
		dispatch_queue_class_t dqu,
		pthread_priority_t pp, dispatch_block_flags_t flags)
{
	dispatch_qos_t qos = DISPATCH_QOS_UNSPECIFIED;
#if HAVE_PTHREAD_WORKQUEUE_QOS
	dispatch_queue_t dq = dqu._dq;

	if (likely(pp)) {
		bool enforce = (flags & DISPATCH_BLOCK_ENFORCE_QOS_CLASS);
		bool is_floor = (dq->dq_priority & DISPATCH_PRIORITY_FLAG_FLOOR);
		bool dq_has_qos = (dq->dq_priority & DISPATCH_PRIORITY_REQUESTED_MASK);
		if (enforce) {
			pp |= _PTHREAD_PRIORITY_ENFORCE_FLAG;
			qos = _dispatch_qos_from_pp_unsafe(pp);
		} else if(! is_floor && dq_has_qos) { pp =0;
		} else {
			qos = _dispatch_qos_from_pp_unsafe(pp);
		}
	}
	dc->dc_priority = pp;
#else
	(void)dc; (void)dqu; (void)pp; (void)flags;
#endif
	return qos;
}
Copy the code

Notice that in the _dispatch_continuation_init function, the value of dispatch_block_t work is the incoming notification block.

void *ctxt = _dispatch_Block_copy(work);
// xxxxxx
dc->dc_ctxt = ctxt;
Copy the code

Notification block is actually stored in dc_CTxt field of the DISPATch_continuation_t object DC.

_dispatch_group_wake

DISPATCH_NOINLINE
static void
_dispatch_group_wake(dispatch_group_t dg, uint64_t dg_state, bool needs_release)
{
	uint16_t refs = needs_release ? 1 : 0; // <rdar://problem/22318411>

	if (dg_state & DISPATCH_GROUP_HAS_NOTIFS) {
		dispatch_continuation_t dc, next_dc, tail;

		// Snapshot before anything is notified/woken <rdar://problem/8554546>
		dc = os_mpsc_capture_snapshot(os_mpsc(dg, dg_notify), &tail);
		do {
			dispatch_queue_t dsn_queue = (dispatch_queue_t)dc->dc_data;
			next_dc = os_mpsc_pop_snapshot_head(dc, tail, do_next);
			_dispatch_continuation_async(dsn_queue, dc,
					_dispatch_qos_from_pp(dc->dc_priority), dc->dc_flags);
			_dispatch_release(dsn_queue);
		} while ((dc = next_dc));

		refs++;
	}

	if (dg_state & DISPATCH_GROUP_HAS_WAITERS) {
		_dispatch_wake_by_address(&dg->dg_gen);
	}

	if (refs) _dispatch_release_n(dg, refs);
}

#define os_mpsc_capture_snapshot(Q, tail)  ({ \
		os_mpsc_node_type(Q) _head = os_mpsc_get_head(Q); \
		os_atomic_store(_os_mpsc_head Q, NULL, relaxed); \
		/* 22708742: set tail to NULL with release, so that NULL write */ \
		/* to head above doesn't clobber head from concurrent enqueuer */\ *(tail) = os_atomic_xchg(_os_mpsc_tail Q, NULL, release); \ _head; The \})

#define os_mpsc_pop_snapshot_head(head, tail, _o_next) ({ \
		typeof(head) _head = (head), _tail = (tail), _n = NULL; \
		if(_head ! = _tail) _n = os_mpsc_get_next(_head, _o_next); \ _n; The \})
Copy the code

Run the following commands to define OS_MPsc_POP_snapshot_head: next_dc = OS_MPSC_POP_snapshot_head (dc, tail, do_next); Continuation_dc, continuation_dc, continuation_dc, continuation_dc, continuation_dc, continuation_dc The execution function call _dispatch_continuation_async is the actual code that triggers the execution of the Notification block.

_dispatch_continuation_async(dsn_queue, dc,
					_dispatch_qos_from_pp(dc->dc_priority), dc->dc_flags);
Copy the code

_dispatch_continuation_async

DISPATCH_ALWAYS_INLINE
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);
}
Copy the code

Look at this dx_push(dqu._dq, dc, qos);

#define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
#define dx_vtable(x) (&(x)->do_vtable->_os_obj_vtable)
Copy the code

What is this do_vtable? These are the fields defined in the DISPATCH_CONTINUATION_HEADER macro when the dispatch_continuation_t object was built.

union{\const void *do_vtable; \
		uintptr_tdc_flags; The \}; \Copy the code
#define DISPATCH_QUEUE_VTABLE_HEADER(x); \
	DISPATCH_OBJECT_VTABLE_HEADER(x); \
	void (*const dq_activate)(dispatch_queue_class_t, bool *allow_resume); \
	void (*const dq_wakeup)(dispatch_queue_class_t, dispatch_qos_t, \
			dispatch_wakeup_flags_t); \
	void (*const dq_push)(dispatch_queue_class_t, dispatch_object_t, \
			dispatch_qos_t)
Copy the code

Therefore, it can be seen that a complete GCD group process is completed by dropping the notification block (DX_push) into the specified queue when _dispatch_group_wake is called.

_dispatch_workloop_push

Dx_vtable (x)->dq_push(x, y, z) Dq_push is associated with _dispatch_workloop_push by the DISPATCH_VTABLE_INSTANCE macro.

DISPATCH_VTABLE_INSTANCE(workloop,
	.do_type        = DISPATCH_WORKLOOP_TYPE,
	.do_dispose     = _dispatch_workloop_dispose,
	.do_debug       = _dispatch_queue_debug,
	.do_invoke      = _dispatch_workloop_invoke,

	.dq_activate    = _dispatch_queue_no_activate,
	.dq_wakeup      = _dispatch_workloop_wakeup,
	.dq_push        = _dispatch_workloop_push,
);
Copy the code

The function prototype for _dispatch_workloop_push is as follows:

void
_dispatch_workloop_push(dispatch_workloop_t dwl, dispatch_object_t dou,
		dispatch_qos_t qos)
{
	struct dispatch_object_s *prev;

	if (unlikely(_dispatch_object_is_waiter(dou))) {
		return _dispatch_workloop_push_waiter(dwl, dou._dsc, qos);
	}

	if (qos < _dispatch_priority_qos(dwl->dq_priority)) {
		qos = _dispatch_priority_qos(dwl->dq_priority);
	}
	if (qos == DISPATCH_QOS_UNSPECIFIED) {
		qos = _dispatch_priority_fallback_qos(dwl->dq_priority);
	}
	prev = _dispatch_workloop_push_update_tail(dwl, qos, dou._do);
	if (unlikely(os_mpsc_push_was_empty(prev))) {
		_dispatch_retain_2_unsafe(dwl);
	}
	_dispatch_workloop_push_update_prev(dwl, qos, prev, dou._do);
	if (unlikely(os_mpsc_push_was_empty(prev))) {
		return_dispatch_workloop_wakeup(dwl, qos, DISPATCH_WAKEUP_CONSUME_2 | DISPATCH_WAKEUP_MAKE_DIRTY); }}Copy the code

Dx_push (dqu._dq, dc, qos); That is equivalent to _dispatch_workloop_push(dqu._dq, dc, qos); Operation.

The call to _dispatch_workloop_push finishes dropping the dispatch_continuation_t object DC into _DQ of dispatch_queue_class_t (queue) along with qos parameters.

As for the actual execution code of the blocks in the queue, it’s time to look at the GCD source for answers. Let’s bury a hole here and fill it in later.

The resources

  1. dispatch
  2. An in-depth study of iOS Troubleshooting dispatch_group crash