background

There have been blog posts stating that the following code can get the timing of the list reloadData completion, but in fact I have encountered situations in my work where the following code does not work. Also, there are people on Stack Overflow who have the same problem as me.

[self.tableView reloadData]; Dispatch_async (dispatch_get_main_queue(), ^{// list refreshed});Copy the code

Therefore, this article examines runloop’s source code and UI refresh timing to show why the above code sometimes works and sometimes doesn’t.

Analysis of the

Problem specification

Code 1

The code is as follows:

[self.tableView reloadData];
NSLog(@"reloadData");
dispatch_async(dispatch_get_main_queue(), ^{
  NSLog(@"dispatch_get_main_queue");
});
Copy the code

The output is as follows, reloadData is followed by mainQueue’s log, and a runloop is followed until kCFRunLoopBeforeWaiting fires cellForRow. Obviously, you can see that this code cannot be used to determine that the list refresh is complete.

kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
reloadData
dispatch_get_main_queue
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
cellForRowAtIndexPath
Copy the code

Sometimes, however, reloadData is called by clicking a button, and its output looks like this. As you can see, this code magically tells you that the list refresh is complete.

kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
reloadData
cellForRowAtIndexPath
dispatch_get_main_queue
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
Copy the code

Code 2

Let’s call the outer block block1 and the inner block block2 for convenience.

//block1
dispatch_async(dispatch_get_main_queue(), ^{
  NSLog(@"dispatch_get_main_queue_1");
  [self.tableView reloadData];
  NSLog(@"reloadData");
  //block2
  dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"dispatch_get_main_queue_2");
  });
});
Copy the code

The output is as follows: Block1 calls reloadData, followed by runloop, going to the beforeWaitding phase and triggering the list cellForRow. The runloop then goes to sleep, after which block2 is distributed to the main queue. One conclusion you can draw from the log is that the list is indeed flushed in block2.

kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
dispatch_get_main_queue_1
reloadData
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
cellForRowAtIndexPath
kCFRunLoopAfterWaiting
dispatch_get_main_queue_2
Copy the code

Runloop source code analysis

Before I explain the above, I need to review the source code of Runloop, which has the following pseudo-code.

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {do {// Notify listener of kCFRunLoopBeforeTimers observer __CFRunLoopDoObservers(RL, RLM, kCFRunLoopBeforeTimers); // Notify observer of kCFRunLoopBeforeSources __CFRunLoopDoObservers(rl, RLM, kCFRunLoopBeforeSources); // Call the block __CFRunLoopDoBlocks(rl, RLM) added to runloop; // call source0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, RLM, stopAfterHandle); if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(rl, rlm); } if (MACH_PORT_NULL ! = dispatchPort && ! DidDispatchPortLastTime) {didDispatchPortLastTime (didDispatchPortLastTime) { If (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(MSg_buffer), &livePort, 0, &voucherState, NULL)) { goto handle_msg; } } didDispatchPortLastTime = false; // Notify observer listening on kCFRunLoopBeforeWaiting __CFRunLoopDoObservers(RL, RLM, kCFRunLoopBeforeWaiting); / / runloop dormancy __CFRunLoopSetSleeping (rl); . // The observer listening for kCFRunLoopAfterWaiting __CFRunLoopDoObservers(rl, RLM, kCFRunLoopAfterWaiting); // The observer listening for kCFRunLoopAfterWaiting __CFRunLoopDoObservers(rl, RLM, kCFRunLoopAfterWaiting); // goto is defined here handle_msg:; If (MACH_PORT_NULL == livePort) {else if (livePort == rl->_wakeUpPort) {else if (RLM ->_timerPort! = MACH_PORT_NULL && livePort == RLM ->_timerPort) {// Handle Timer event if (! __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { __CFArmNextTimerInMode(rlm, rl); }} else if (livePort == dispatchPort) {cfrunloop_is_servicing_the_main_dispatch_queue (MSG); didDispatchPortLastTime = true; } else {// handle source1 __CFRunLoopDoSource1(rl, RLM, RLS, MSG, MSG ->msgh_size, &reply)}} while (...) }Copy the code

The most important logic in runloop source code is the do-while loop, which is described as follows:

  1. Inform kCFRunLoopBeforeTimers
  2. Inform kCFRunLoopBeforeSources
  3. Perform Sources0
  4. Jump to 9 if the main queue has a task and the task assigned to the main queue was not processed in the last loop
  5. Inform kCFRunLoopBeforeWaiting
  6. dormancy
  7. Be awakened
  8. Inform kCFRunLoopAfterWaiting
  9. conditional
    1. If the timer wakes up, process the timer task
    2. If awakened by Dispatch, processes tasks dispatched to the main queue
    3. If awakened by source1, handle the source1 event

Too long to look at the version:

CellForRow call timing

CATransaction registers an observer of runloop to listen for kCFRunLoopBeforeWaiting, and calls commit at this stage to trigger UI updates. The cellForRow proxy for the list is triggered in the COMMIT method of the CATransaction.

In addition, runloop wakes up from source1 and continues to process tasks in Source0, such as gesture tasks. Here’s the Trace. In this process, UIKit sometimes calls the CATransaction flush method to trigger a UI flush, so it’s possible to call cellForRow in source0 as well.

By looking at the disassembled UIKitCore code, you can see that under certain circumstances the flush method of the CATransaction is triggered.

Problem solving

Code 1

In code 1, reloadData is fired in the source0 event, and runloop determines if there are any GCD main queue tasks that can be processed, and if there are, goto processes them directly, skipping beforeWaiting and hibernation. The main queue task is executed just before cellForRow.

Set the didDispatchPortLastTime identifier to true after processing the GCD main queue task. The identifier will be determined after the next processing of source0. If true, the GCD task cannot be skipped again. This logic is easy to understand and prevents the runloop from processing tasks while the CGD main queue is always occupied.

In code 1, if UIKit fires the CATransaction flush method for some reason after reloadData is called, cellForRow is called synchronously, and the GCD main queue task must fire after the list refresh is complete.

Code 2

In code 2, runloop completes source0 and detects that there are tasks in the GCD main queue, so it goes directly to sleep to process the GCD main queue and triggers reloadData in block1. Since the GCD primary queue is processed after this loop wake up, the goto cannot continue after source0 to skip sleep, but instead goes to beforeWaiting, triggering cellForRow of the list, after which runloop sleeps. After sleep, the GCD main queue will continue to be processed, at which point block2 must be after cellForRow, so you can judge that the list refresh is complete.

conclusion

There are two types of list refresh: synchronous refresh and runloop refresh at beforeWaiting time. There are two types of GCD main queue tasks: directly processing without beforeWaiting time and processing after waiting for hibernation. Therefore, in actual development, GCD is used to judge whether the list refresh is completed sometimes takes effect and sometimes fails.