UIView/CALayer渲染的觸發時機

1. 測試觸發時機

爲了探究渲染的觸發時機,咱們自定義一個TestView並複寫 drawRect: 方法。ios

雖然開發中不推薦這麼使用 drawRect: 方法,這裏是爲了設置斷點,探究渲染的觸發時機。git

@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
複製代碼

咱們在 drawRect: 方法中設置一個斷點,而後運行咱們的測試代碼。github

1.1 狀況1

剛運行起來就會進入斷點,咱們在 lldb 中使用 bt 打印一下調用棧。app

(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
複製代碼

從調用棧咱們能夠看到,App運行起來主線程的 RunLoop 會觸發 -[UIApplication _firstCommitBlock] 進行第一次的繪製。ide

1.2 狀況2

咱們之前看其餘博客的時候常常看到如下說法:函數

當在操做UI時,好比改變了frame、更新了UIView/CALayer的層次時,或者手動調用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法後,這個UIView/CALayer就被標記爲待處理,並被提交到一個全局的容器去。oop

蘋果註冊了一個Observer監聽BeforeWaiting(即將進入休眠)和Exit(即將退出Loop)事件,回調去執行一個很長的函數: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv。測試

這個函數裏會遍歷全部待處理的UIView/CAlayer以執行實際的繪製和調整,並更新UI界面。flex

下面咱們來驗證一下。繼續運行,執行完下面代碼的中改變視圖背景色的部分後ui

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    self.view.backgroundColor = [UIColor blueColor];
});
複製代碼

會進入第二次的斷點:

(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
複製代碼

這一次咱們發現主線程的 RunLoop 經過一個Observer觸發了CA::Transaction::observer_callback

咱們知道 __CFRunLoopDoObservers 的API申明以下:

static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity)
複製代碼

第三個參數爲CFRunLoopActivity類型:

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
};
複製代碼

咱們打印一下第三個參數:

(lldb) po $arg3
32
複製代碼

正好是咱們的kCFRunLoopBeforeWaiting事件。

下面咱們打印主線程的 RunLoop 並搜索 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>}
...
複製代碼

咱們能夠看到主線程的 RunLoop 即將進入休眠的時候觸發了這個觀察者。

有些同窗對於主線程的 RunLoop 在觸發這個觀察者以後,界面的渲染事件有疑問:

這時觸發的渲染,是在當前輪次,仍是下一輪RunLoop進行顯示。

其實 BeforeWaiting 指的是,提交到這一輪 RunLoop 的事務都作完了,要進入休眠了。能夠理解爲每一輪 RunLoopcompletion callback ,能夠開始進行新一輪的事務提交了。而監聽到 BeforeWaiting 的Observer會調用 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv ,進行下一次的事務的準備和提交。

_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];
複製代碼

即,下一輪的RunLoop纔會刷新界面

1.3 狀況3

咱們點擊按鈕以後,也會更改視圖的背景色:

- (void)buttonPressed
{
    self.view.backgroundColor = [UIColor redColor];
}
複製代碼

咱們知道,點擊按鈕會觸發主線程的 RunLoopsource1 執行 __IOHIDEventSystemClientQueueCallback ,喚醒下一輪 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
複製代碼

進入斷點以後,咱們發現喚醒 下一輪RunLoop 以後由 source0 執行 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION 回調,進而觸發 QuartzCore`CA::Transaction::commit() 方法。

Source1在處理任務的時候,有時會跟Source0一塊兒配合,把一些任務分發給Source0去執行。例如剛剛提到的點擊事件,先由Source1處理硬件事件,以後Source1將事件包裝分發給Source0繼續進行處理。

2. 總結

除了首次執行 commit 之外,更改視圖顯示效果以後,均在 下一輪RunLoop 纔會生效,提交給 Render Server 進行渲染。

3.擴展閱讀

  1. iOS 保持界面流暢的技巧
  2. iOS Core Animation Advanced Techniques
  3. iOS 圖像渲染原理

若是以爲本文對你有所幫助,給我點個贊吧~

相關文章
相關標籤/搜索