1. Test the trigger timing

To explore when rendering is triggered, we define a TestView and override the drawRect: method.

Although it is not recommended to use the drawRect: method this way, the purpose is to set a breakpoint and explore when to trigger the rendering.

@interface TestView : UIView
@end

@implementation TestView
- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
}
@end

@implementation ViewController
- (void)loadView
{
    self.view = [[TestView alloc] initWithFrame:[UIScreen mainScreen].bounds];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.view.backgroundColor = [UIColor blueColor];
    });
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
    [button addTarget:self action:@selector(buttonPressed) forControlEvents:UIControlEventTouchUpInside];
    button.center = self.view.center;
    [self.view addSubview:button];
}

- (void)buttonPressed
{
    self.view.backgroundColor = [UIColor redColor];
}
@end
Copy the code

We set a breakpoint in the drawRect: method and run our test code.

1.1 case 1

As soon as it starts running, it will enter a breakpoint, so let’s print out the call stack using BT in the LLDB.

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x000000010bf67094 OCDemo`-[TestView drawRect:](self=0x00007fbd0a70ec00, _cmd="drawRect:", rect=(origin = (x = 0, y = 0), size = (width = 375, height = 812))) at ViewController.m:19:5
    frame #1: 0x00007fff491928ae UIKitCore`-[UIView(CALayerDelegate) drawLayer:inContext:] + 632
    frame #2: 0x000000010e0b7941 UIKit`-[UIViewAccessibility drawLayer:inContext:] + 74
    frame #3: 0x00007fff2b4c5ca4 QuartzCore`-[CALayer drawInContext:] + 286
    frame #4: 0x00007fff2b391d58 QuartzCore`CABackingStoreUpdate_ + 196
    frame #5: 0x00007fff2b4ce665 QuartzCore`___ZN2CA5Layer8display_Ev_block_invoke + 53
    frame #6: 0x00007fff2b4c5616 QuartzCore`-[CALayer _display] + 2026
    frame #7: 0x00007fff2b4d7d72 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 520
    frame #8: 0x00007fff2b420c04 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double) + 324
    frame #9: 0x00007fff2b4545ef QuartzCore`CA::Transaction::commit() + 649
    frame #10: 0x00007fff48ca3747 UIKitCore`__34-[UIApplication _firstCommitBlock]_block_invoke_2 + 81
    frame #11: 0x00007fff23da0b5c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
    frame #12: 0x00007fff23da0253 CoreFoundation`__CFRunLoopDoBlocks + 195
    frame #13: 0x00007fff23d9b043 CoreFoundation`__CFRunLoopRun + 995
    frame #14: 0x00007fff23d9a944 CoreFoundation`CFRunLoopRunSpecific + 404
    frame #15: 0x00007fff38ba6c1a GraphicsServices`GSEventRunModal + 139
    frame #16: 0x00007fff48c8b9ec UIKitCore`UIApplicationMain + 1605
    frame #17: 0x000000010bf677ba OCDemo`main(argc=1, argv=0x00007ffee3c97d28) at main.m:18:12
    frame #18: 0x00007fff51a231fd libdyld.dylib`start + 1
    frame #19: 0x00007fff51a231fd libdyld.dylib`start + 1
Copy the code

From the call stack we can see that the main thread of the App runsRunLoopWill trigger-[UIApplication _firstCommitBlock]Do the first drawing.

The above statement is not quite correct, correct it. SetNeedsDisplay is triggered when the view is initialized.

Adding to the view hierarchy also triggers setNeedsLayout.

Finally, the drawRect: method, which is triggered by the main thread RunLoop -[UIApplication _firstCommitBlock], is drawn for the first time. Therefore, initialization also takes effect in the next RunLoop.

1.2 case 2

Here’s what we used to see when we read other blogs:

When in operation the UI, such as changing the frame, update the UIView/CALayer level, or manually invoked the UIView/CALayer setNeedsLayout/setNeedsDisplay method, The UIView/CALayer is marked to be processed and submitted to a global container.

Apple registers an Observer to listen for BeforeWaiting(about to go to sleep) and Exit(about to Exit Loop) events, calling back to perform a long function: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv.

This function iterates through all the UIViews/Calayers to be processed to perform the actual drawing and adjustment, and update the UI.

So let’s verify that. Go ahead and execute the part of the code below that changes the view background color

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    self.view.backgroundColor = [UIColor blueColor];
});
Copy the code

Will enter the second break point:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001040cde64 OCDemo`-[TestView drawRect:](self=0x00007fdd2dc09ee0, _cmd="drawRect:", rect=(origin = (x = 0, y = 0), size = (width = 375, height = 812))) at ViewController.m:19:5
    frame #1: 0x00007fff491928ae UIKitCore`-[UIView(CALayerDelegate) drawLayer:inContext:] + 632
    frame #2: 0x00000001045a9941 UIKit`-[UIViewAccessibility drawLayer:inContext:] + 74
    frame #3: 0x00007fff2b4c5ca4 QuartzCore`-[CALayer drawInContext:] + 286
    frame #4: 0x00007fff2b391d58 QuartzCore`CABackingStoreUpdate_ + 196
    frame #5: 0x00007fff2b4ce665 QuartzCore`___ZN2CA5Layer8display_Ev_block_invoke + 53
    frame #6: 0x00007fff2b4c5616 QuartzCore`-[CALayer _display] + 2026
    frame #7: 0x00007fff2b4d7d72 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 520
    frame #8: 0x00007fff2b420c04 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double) + 324
    frame #9: 0x00007fff2b4545ef QuartzCore`CA::Transaction::commit() + 649
    frame #10: 0x00007fff2b454f81 QuartzCore`CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 79
    frame #11: 0x00007fff23da0127 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
    frame #12: 0x00007fff23d9abde CoreFoundation`__CFRunLoopDoObservers + 430
    frame #13: 0x00007fff23d9b12a CoreFoundation`__CFRunLoopRun + 1226
    frame #14: 0x00007fff23d9a944 CoreFoundation`CFRunLoopRunSpecific + 404
    frame #15: 0x00007fff38ba6c1a GraphicsServices`GSEventRunModal + 139
    frame #16: 0x00007fff48c8b9ec UIKitCore`UIApplicationMain + 1605
    frame #17: 0x00000001040ce77a OCDemo`main(argc=1, argv=0x00007ffeebb30d28) at main.m:18:12
    frame #18: 0x00007fff51a231fd libdyld.dylib`start + 1
    frame #19: 0x00007fff51a231fd libdyld.dylib`start + 1
Copy the code

This time we found the main thread of the RunLoop triggered by an Observer CA: : Transaction: : observer_callback.

We know that the __CFRunLoopDoObservers API is declared as follows:

static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity)
Copy the code

The third parameter is the CFRunLoopActivity type:

typedef CF_OPTIONS(CFOptionFlags.CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Copy the code

Let’s print out the third parameter:

(lldb) po $arg3
32
Copy the code

Happens to be our kCFRunLoopBeforeWaiting event.

Here we print the main thread RunLoop and search for observer_callback.

(lldb) po [NSRunLoop mainRunLoop]
...
// activities = 0xa0 kCFRunLoopBeforeWaiting | kCFRunLoopExit
4 : <CFRunLoopObserver 0x600000e1c3c0 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x7fff2b454f32), context = <CFRunLoopObserver context 0x0>}...Copy the code

We can see that the observer is triggered when the main thread RunLoop is about to go to sleep.

Some students have some questions about the interface render event after the main thread RunLoop triggers the observer:

The render triggered at this time will be displayed in the current round or in the next RunLoop.

BeforeWaiting means that all transactions committed to the RunLoop are finished and will go to sleep. This can be interpreted as a completion callback for each RunLoop, and you’re ready to start a new round of transaction commits. While listening to BeforeWaiting Observer will call _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv, prepare and submit for the next transaction.

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];
Copy the code

That is, the next RunLoop will refresh the interface.

1.3 case 3

After we click the button, we also change the background color of the view:

- (void)buttonPressed
{
    self.view.backgroundColor = [UIColor redColor];
}
Copy the code

As we know, click on the button will trigger the main thread of the RunLoop source1 __IOHIDEventSystemClientQueueCallback, wake up the next round RunLoop.

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000108105e64 OCDemo`-[TestView drawRect:](self=0x00007fdeaa51d430, _cmd="drawRect:", rect=(origin = (x = 0, y = 0), size = (width = 375, height = 812))) at ViewController.m:19:5
    frame #1: 0x00007fff491928ae UIKitCore`-[UIView(CALayerDelegate) drawLayer:inContext:] + 632
    frame #2: 0x000000010a254941 UIKit`-[UIViewAccessibility drawLayer:inContext:] + 74
    frame #3: 0x00007fff2b4c5ca4 QuartzCore`-[CALayer drawInContext:] + 286
    frame #4: 0x00007fff2b391d58 QuartzCore`CABackingStoreUpdate_ + 196
    frame #5: 0x00007fff2b4ce665 QuartzCore`___ZN2CA5Layer8display_Ev_block_invoke + 53
    frame #6: 0x00007fff2b4c5616 QuartzCore`-[CALayer _display] + 2026
    frame #7: 0x00007fff2b4d7d72 QuartzCore`CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 520
    frame #8: 0x00007fff2b420c04 QuartzCore`CA::Context::commit_transaction(CA::Transaction*, double) + 324
    frame #9: 0x00007fff2b4545ef QuartzCore`CA::Transaction::commit() + 649
    frame #10: 0x00007fff48c84fc8 UIKitCore`_UIApplicationFlushRunLoopCATransactionIfTooLate + 104
    frame #11: 0x00007fff48d32ab0 UIKitCore`__handleEventQueueInternal + 7506
    frame #12: 0x00007fff48d28fb9 UIKitCore`__handleHIDEventFetcherDrain + 88
    frame #13: 0x00007fff23da0d31 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #14: 0x00007fff23da0c5c CoreFoundation`__CFRunLoopDoSource0 + 76
    frame #15: 0x00007fff23da0434 CoreFoundation`__CFRunLoopDoSources0 + 180
    frame #16: 0x00007fff23d9b02e CoreFoundation`__CFRunLoopRun + 974
    frame #17: 0x00007fff23d9a944 CoreFoundation`CFRunLoopRunSpecific + 404
    frame #18: 0x00007fff38ba6c1a GraphicsServices`GSEventRunModal + 139
    frame #19: 0x00007fff48c8b9ec UIKitCore`UIApplicationMain + 1605
    frame #20: 0x000000010810677a OCDemo`main(argc=1, argv=0x00007ffee7af8d28) at main.m:18:12
    frame #21: 0x00007fff51a231fd libdyld.dylib`start + 1
    frame #22: 0x00007fff51a231fd libdyld.dylib`start + 1
Copy the code

After entering the breakpoint, we see that after waking up the next RunLoop, the CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION callback is executed by source0, This triggers the QuartzCore ‘CA::Transaction::commit() method.

Source1 sometimes works with Source0 to assign tasks to Source0 for execution. For example, for the click event mentioned earlier, Source1 handles the hardware event, and then Source1 wraps the event and distributes it to Source0 for further processing.

2. To summarize

Once the view is initialized or changed, it will take effect in the next RunLoop and be submitted to the Render Server for rendering.

3. Read more

  1. IOS tips for keeping the interface smooth
  2. iOS Core Animation Advanced Techniques
  3. IOS image rendering principles

If you found this article helpful, give me a thumbs up