1. 程式人生 > >iOS開發筆記--iOS 事件處理機制與影象渲染過程

iOS開發筆記--iOS 事件處理機制與影象渲染過程

iOS 事件處理機制與影象渲染過程

·iOS RunLoop都幹了什麼

·iOS 為什麼必須在主執行緒中操作UI

·事件響應

·CALayer

·CADisplayLink 和 NSTimer

·iOS 渲染過程

·渲染時機

·CPU 和 GPU渲染

·Core Animation

·Facebook Pop介紹

·AsyncDisplay介紹

·參考文章

iOS RunLoop都幹了什麼

RunLoop是一個接收處理非同步訊息事件的迴圈,一個迴圈中:等待事件發生,然後將這個事件送到能處理它的地方。

如圖1-1所示,描述了一個觸控事件從作業系統層傳送到應用內的main runloop中的簡單過程。

圖1-1

簡單的說,RunLoop是事件驅動的一個大迴圈,如下程式碼所示

int main(int argc, char * argv[]) {
     //程式一直執行狀態
     while (AppIsRunning) {
          //睡眠狀態,等待喚醒事件
          id whoWakesMe = SleepForWakingU  p();
          //得到喚醒事件
          id event = GetEvent(whoWakesMe);
          //開始處理事件
          HandleEvent(event);
     }
     return
0; }

RunLoop主要處理以下6類事件:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static
void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

1.Observer事件,runloop中狀態變化時進行通知。(微信卡頓監控就是利用這個事件通知來記錄下最近一次main runloop活動時間,在另一個check執行緒中用定時器檢測當前時間距離最後一次活動時間過久來判斷在主執行緒中的處理邏輯耗時和卡主執行緒)。這裡還需要特別注意,CAAnimation是由RunloopObserver觸發回撥來重繪,接下來會講到。

2.Block事件,非延遲的NSObject PerformSelector立即呼叫,dispatch_after立即呼叫,block回撥。

3.Main_Dispatch_Queue事件:GCD中dispatch到main queue的block會被dispatch到main loop執行。

4.Timer事件:延遲的NSObject PerformSelector,延遲的dispatch_after,timer事件。

5.Source0事件:處理如UIEvent,CFSocket這類事件。需要手動觸發。觸控事件其實是Source1接收系統事件後在回撥 __IOHIDEventSystemClientQueueCallback() 內觸發的 Source0,Source0 再觸發的 _UIApplicationHandleEventQueue()。source0一定是要喚醒runloop及時響應並執行的,如果runloop此時在休眠等待系統的 mach_msg事件,那麼就會通過source1來喚醒runloop執行。

6.Source1事件:處理系統核心的mach_msg事件。(推測CADisplayLink也是這裡觸發)。

RunLoop執行順序的虛擬碼

SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
//通知即將進入runloop__CFRUNLLOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(KCFRunLoopEntry);
do {
     __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
     __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
     __CFRunLoopDoBlocks();  //一個迴圈中會呼叫兩次,確保非延遲的NSObject PerformSelector呼叫和非延遲的dispatch_after呼叫在當前runloop執行。還有回撥block
     __CFRunLoopDoSource0(); //例如UIKit處理的UIEvent事件
     CheckIfExistMessagesInMainDispatchQueue(); //GCD dispatch main queue
     __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); //即將進入休眠,會重繪一次介面
     var wakeUpPort = SleepAndWaitForWakingUpPorts();
     // mach_msg_trap,陷入核心等待匹配的核心mach_msg事件
     // Zzz...
     // Received mach_msg, wake up
     __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
     // Handle msgs
     if (wakeUpPort == timerPort) {
          __CFRunLoopDoTimers();
     } else if (wakeUpPort == mainDispatchQueuePort) {
          //GCD當呼叫dispatch_async(dispatch_get_main_queue(),block)時,libDispatch會向主執行緒的runloop傳送mach_msg訊息喚醒runloop,並在這裡執行。這裡僅限於執行dispatch到主執行緒的任務,dispatch到其他執行緒的仍然是libDispatch來處理。
          __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
     } else {
          __CFRunLoopDoSource1();  //CADisplayLink是source1的mach_msg觸發?
     }
     __CFRunLoopDoBlocks();
} while (!stop && !timeout);
//通知observers,即將退出runloop
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBERVER_CALLBACK_FUNCTION__(CFRunLoopExit);

結合上面的Runloop事件執行順序,思考下面程式碼邏輯中為什麼可以標識tableview是否reload完成

dispatch_async(dispatch_get_main_queue(), ^{
    _isReloadDone = NO;
    [tableView reload]; //會自動設定tableView layoutIfNeeded為YES,意味著將會在runloop結束時重繪table
    dispatch_async(dispatch_get_main_queue(),^{
        _isReloadDone = YES;
    });
});

提示:這裡在GCD dispatch main queue中插入了兩個任務,一次RunLoop有兩個機會執行GCD dispatch main queue中的任務,分別在休眠前和被喚醒後。

iOS 為什麼必須在主執行緒中操作UI

因為UIKit不是執行緒安全的。試想下面這幾種情況:

1.兩個執行緒同時設定同一個背景圖片,那麼很有可能因為當前圖片被釋放了兩次而導致應用崩潰。

2.兩個執行緒同時設定同一個UIView的背景顏色,那麼很有可能渲染顯示的是顏色A,而此時在UIView邏輯樹上的背景顏色屬性為B。

3.兩個執行緒同時操作view的樹形結構:線上程A中for迴圈遍歷並操作當前View的所有subView,然後此時執行緒B中將某個subView直接刪除,這就導致了錯亂還可能導致應用崩潰。 

iOS4之後蘋果將大部分繪圖的方法和諸如 UIColor 和 UIFont 這樣的類改寫為了執行緒安全可用,但是仍然強烈建議講UI操作保證在主執行緒中執行。

事件響應

蘋果註冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回撥函式為 __IOHIDEventSystemClientQueueCallback()。

當一個硬體事件(觸控/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收。

SpringBoard 只接收按鍵(鎖屏/靜音等),觸控,加速,接近感測器等幾種 Event,隨後用 mach port 轉發給需要的App程序。隨後蘋果註冊的那個 Source1 就會觸發回撥,並呼叫 _UIApplicationHandleEventQueue() 進行應用內部的分發。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理螢幕旋轉/傳送給 UIWindow 等。通常事件比如 UIButton 點選、touchesBegin/Move/End/Cancel 事件都是在這個回撥中完成的。

CALayer

在iOS當中,所有的檢視都從一個叫做UIVIew的基類派生而來,UIView可以處理觸控事件,可以支援基於Core Graphics繪圖,可以做仿射變換(例如旋轉或者縮放),或者簡單的類似於滑動或者漸變的動畫。

CALayer類在概念上和UIView類似,同樣也是一些被層級關係樹管理的矩形塊,同樣也可以包含一些內容(像圖片,文字或者背景色),管理子圖層的位置。它們有一些方法和屬性用來做動畫和變換。和UIView最大的不同是CALayer不處理使用者的互動。CALayer並不清楚具體的響應鏈。

UIView和CALayer是一個平行的層級關係,每一個UIView都有一個CALayer例項的圖層屬性,也就是所謂的backing layer,檢視的職責就是建立並管理這個圖層,以確保當子檢視在層級關係中新增或者被移除的時候,他們關聯的圖層也同樣對應在層級關係樹當中有相同的操作。實際上這些背後關聯的Layer圖層才是真正用來在螢幕上顯示和做動畫,UIView僅僅是對它的一個封裝,提供了一些iOS類似於處理觸控的具體功能,以及Core Animation底層方法的高階介面。

UIView 的 Layer 在系統內部,被維護著三份同樣的樹形資料結構,分別是:

1.圖層樹(這裡是程式碼可以操縱的,設定屬性的最終值會立刻在這裡更新);

2.呈現樹(是一箇中間層,系統就在這一層上更改屬性,進行各種渲染操作。比如一個動畫是更改alpha值從0到1,那麼在邏輯樹上此屬性會被立刻更新為最終屬性1,而在動畫樹上會根據設定的動畫時間從0逐步變化到1);

3.渲染樹(其屬性值就是當前正被顯示在螢幕上的屬性值);

CADisplayLink 和 NSTimer

NSTimer 其實就是 CFRunLoopTimerRef。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會為其重複的時間點註冊好事件。

RunLoop為了節省資源,並不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到後,容許有多少最大誤差。如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回撥也會跳過去,不會延後執行。

RunLoop 是用GCD的 dispatch_source_t 實現的 Timer。 當呼叫 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並新增到當前執行緒的 RunLoop 中。所以如果當前執行緒沒有 RunLoop,則這個方法會失效。當呼叫 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的執行緒去,同樣的,如果對應執行緒沒有 RunLoop 該方法也會失效。

CADisplayLink 是一個和螢幕重新整理率(每秒重新整理60次)一致的定時器(但實際實現原理更復雜,和 NSTimer 並不一樣,其內部實際是操作了一個 Source)。如果在兩次螢幕重新整理之間執行了一個長任務,那其中就會有一幀被跳過去,造成介面卡頓的感覺。

iOS 渲染過程
圖2-1
通常來說,計算機系統中 CPU、GPU、顯示器是以上面這種方式協同工作的。CPU 計算好顯示內容提交到 GPU,GPU 渲染完成後將渲染結果放入幀緩衝區,隨後視訊控制器會按照 VSync 訊號如下圖1-4所示,逐行讀取幀緩衝區的資料,經過可能的數模轉換傳遞給顯示器顯示。

圖2-2
在 VSync 訊號到來後,系統圖形服務會通過 CADisplayLink 等機制通知 App,App 主執行緒開始在 CPU 中計算顯示內容,比如檢視的建立、佈局計算、圖片解碼、文字繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 訊號到來時顯示到螢幕上。由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是介面卡頓的原因。從上圖中可以看到,CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現象。所以開發時,也需要分別對 CPU 和 GPU 壓力進行評估和優化。

iOS 的顯示系統是由 VSync 訊號驅動的,VSync 訊號由硬體時鐘生成,每秒鐘發出 60 次(這個值取決裝置硬體,比如 iPhone 真機上通常是 59.97)。iOS 圖形服務接收到 VSync 訊號後,會通過 IPC 通知到 App 內。App 的 Runloop 在啟動後會註冊對應的 CFRunLoopSource 通過 mach_port 接收傳過來的時鐘訊號通知,隨後 Source 的回撥會驅動整個 App 的動畫與顯示。

Core Animation 在 RunLoop 中註冊了一個 Observer,監聽了 BeforeWaiting 和 Exit 事件。當一個觸控事件到來時,RunLoop 被喚醒,App 中的程式碼會執行一些操作,比如建立和調整檢視層級、設定 UIView 的 frame、修改 CALayer 的透明度、為檢視新增一個動畫;這些操作最終都會被 CALayer 標記,並通過 CATransaction 提交到一箇中間狀態去。當上面所有操作結束後,RunLoop 即將進入休眠(或者退出)時,關注該事件的 Observer 都會得到通知。這時 Core Animation 註冊的那個 Observer 就會在回撥中,把所有的中間狀態合併提交到 GPU 去顯示;如果此處有動畫,通過 DisplayLink 穩定的重新整理機制會不斷的喚醒runloop,使得不斷的有機會觸發observer回撥,從而根據時間來不斷更新這個動畫的屬性值並繪製出來。

為了不阻塞主執行緒,Core Animation 的核心是 OpenGL ES 的一個抽象物,所以大部分的渲染是直接提交給GPU來處理。 而Core Graphics/Quartz 2D的大部分繪製操作都是在主執行緒和CPU上同步完成的,比如自定義UIView的drawRect裡用CGContext來畫圖。

渲染時機

上面已經提到過:Core Animation 在 RunLoop 中註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件 。當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動呼叫了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理,並被提交到一個全域性的容器去。當Oberver監聽的事件到來時,回撥執行函式中會遍歷所有待處理的UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 介面。

這個函式內部的呼叫棧大概是這樣的:

_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];

CPU 和 GPU渲染

OpenGL中,GPU螢幕渲染有以下兩種方式:

1.On-Screen Rendering

意為當前螢幕渲染,指的是GPU的渲染操作是在當前用於顯示的螢幕緩衝區中進行。

2.Off-Screen Rendering

意為離屏渲染,指的是GPU在當前螢幕緩衝區以外新開闢一個緩衝區進行渲染操作。

按照這樣的說法,如果將不在GPU的當前螢幕緩衝區中進行的渲染都稱為離屏渲染,那麼就還有另一種特殊的“離屏渲染”方式:CPU渲染。如果我們重寫了drawRect方法,並且使用任何Core Graphics的技術進行了繪製操作,就涉及到了CPU渲染。整個渲染過程由CPU在App內同步地完成,渲染得到的bitmap最後再交由GPU用於顯示。

相比於當前螢幕渲染,離屏渲染的代價是很高的,主要體現在兩個方面:

1.建立新緩衝區

要想進行離屏渲染,首先要建立一個新的緩衝區。

2.上下文切換

離屏渲染的整個過程,需要多次切換上下文環境:先是從當前螢幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以後,將離屏緩衝區的渲染結果顯示到螢幕上有需要將上下文環境從離屏切換到當前螢幕。而上下文環境的切換是要付出很大代價的。

設定了以下屬性時,都會觸發離屏繪製:

1.shouldRasterize(光柵化)

2.masks(遮罩)

3.shadows(陰影)

4.edge antialiasing(抗鋸齒)

5.group opacity(不透明) 

需要注意的是,如果shouldRasterize被設定成YES,在觸發離屏繪製的同時,會將光柵化後的內容快取起來,如果對應的layer及其sublayers沒有發生改變,在下一幀的時候可以直接複用。這將在很大程度上提升渲染效能。

而其它屬性如果是開啟的,就不會有快取,離屏繪製會在每一幀都發生。

在開發時需要根據實際情況來選擇最優的實現方式,儘量使用On-Screen Rendering。簡單的Off-Screen Rendering可以考慮使用Core Graphics讓CPU來渲染。

Core Animation

1.隱式動畫

隱式動畫是系統框架自動完成的。Core Animation在每個runloop週期中自動開始一次新的事務,即使你不顯式的用[CATransaction begin]開始一次事務,任何在一次runloop迴圈中屬性的改變都會被集中起來,然後做一次0.25秒的動畫。

在iOS4中,蘋果對UIView添加了一種基於block的動畫方法:+animateWithDuration:animations:。

這樣寫對做一堆的屬性動畫在語法上會更加簡單,但實質上它們都是在做同樣的事情。

CATransaction的+begin和+commit方法在+animateWithDuration:animations:內部自動呼叫,這樣block中所有屬性的改變都會被事務所包含。

Core Animation通常對CALayer的所有屬性(可動畫的屬性)做動畫,但是UIView是怎麼把它關聯的圖層的這個特性關閉了呢?

每個UIView對它關聯的圖層都扮演了一個委託,並且提供了-actionForLayer:forKey的實現方法。當不在一個動畫塊的實現中,UIView對所有圖層行為返回nil,但是在動畫block範圍之內,它就返回了一個非空值。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //test layer action when outside of animation block
    NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //begin animation block
    [UIView beginAnimations:nil context:nil];
    //test layer action when inside of animation block
    NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
    //end animation block
    [UIView commitAnimations];
}
@end
$ LayerTest[21215:c07] Outside: $ LayerTest[21215:c07] Inside:

2.顯式動畫

Core Animation提供的顯式動畫型別,既可以直接對退曾屬性做動畫,也可以覆蓋預設的圖層行為。

我們經常使用的CABasicAnimation,CAKeyframeAnimation,CATransitionAnimation,CAAnimationGroup等都是顯式動畫型別,這些CAAnimation型別可以直接提交到CALayer上。

無論是隱式動畫還是顯式動畫,提交到layer後,經過一系列處理,最後都經過上文描述的繪製過程最終被渲染出來。

Facebook Pop介紹

在計算機的世界裡面,其實並不存在絕對連續的動畫,你所看到的螢幕上的動畫本質上都是離散的,只是在一秒的時間裡面離散的幀多到一定的數量人眼就覺得是連續的了,

在iOS中,最大的幀率是60幀每秒。 iOS提供了Core Animation框架,只需要開發者提供關鍵幀資訊,比如提供某個animatable屬性終點的關鍵幀資訊,然後中間的值則通過一定的演算法進行插值計算,從而實現補間動畫。 Core Aniamtion中進行插值計算所依賴的時間曲線由CAMediaTimingFunction提供。

Pop Animation在使用上和Core Animation很相似,都涉及Animation物件以及Animation的載體的概念,不同的是Core Animation的載體只能是CALayer,而Pop Animation可以是任意基於NSObject的物件。當然大多數情況Animation都是介面上顯示的可視的效果,所以動畫執行的載體一般都直接或者間接是UIView或者CALayer。

但是如果你只是想研究Pop Animation的變化曲線,你也完全可以將其應用於一個普通的資料物件。Pop Animation應用於CALayer時,在動畫執行的任何時刻,layer和其presentationLayer的相關屬性值始終保持一致,而Core Animation做不到。 Pop Animation可以應用任何NSObject的物件,而Core Aniamtion必須是CALayer。

下面這個例子就是自定義Pop readBlock和writeBlock處理自定義的動畫屬性:

prop = [POPAnimatableProperty propertyWithName:@"com.foo.radio.volume" initializer:^(POPMutableAnimatableProperty *prop) {
    // read value
    prop.readBlock = ^(id obj, CGFloat values[]) {
        values[0] = [obj volume];
    };
    // write value
    prop.writeBlock = ^(id obj, const CGFloat values[]) {
        [obj setVolume:values[0]];
    };
    // dynamics threshold
    prop.threshold = 0.01;
}];
POPSpringAnimation *anim = [POPSpringAnimation animation];
anim.property = prop;

Pop實現依賴的核心就是CADisplayLink。

最後附上一篇介紹Facebook Pop如何使用的文章 《Introducing Facebook Pop》

AsyncDisplay介紹

阻塞主執行緒的繪製任務主要是這三大類:Layout計算檢視佈局文字寬高、Rendering文字渲染圖片解碼圖片繪製、UIKit物件建立更新釋放。除了UIKit和CoreAnimation相關操作必須在主執行緒中進行,其他的都可以挪到後臺執行緒非同步執行。

AsyncDisplay通過抽象UIView的關係建立了ASDisplayNode類,ASDisplayNode是執行緒安全的,它可以在後臺執行緒建立和修改。Node 剛建立時,並不會在內部新建 UIView 和 CALayer,直到第一次在主執行緒訪問 view 或 layer 屬性時,它才會在內部生成對應的物件。當它的屬性(比如frame/transform)改變後,它並不會立刻同步到其持有的 view 或 layer 去,而是把被改變的屬性儲存到內部的一箇中間變數,稍後在需要時,再通過某個機制一次性設定到內部的 view 或 layer。從而可以實現非同步併發操作。

AsyncDisplay實現依賴如同Core Animation在runloop中註冊observer事件來觸發。

同樣附上一篇介紹AsyncDisplay的好文 《iOS保持介面流暢的技巧和AsyncDisplay介紹》