iOS 事件處理機制與圖像渲染過程html
iOS RunLoop都幹了什麼ios
iOS 爲何必須在主線程中操做UIgit
事件響應github
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__();
Observer事件,runloop中狀態變化時進行通知。(微信卡頓監控就是利用這個事件通知來記錄下最近一次main runloop活動時間,在另外一個check線程中用定時器檢測當前時間距離最後一次活動時間太久來判斷在主線程中的處理邏輯耗時和卡主線程)。這裏還須要特別注意,CAAnimation是由RunloopObserver觸發回調來重繪,接下來會講到。
Block事件,非延遲的NSObject PerformSelector當即調用,dispatch_after當即調用,block回調。
Main_Dispatch_Queue事件:GCD中dispatch到main queue的block會被dispatch到main loop執行。
Timer事件:延遲的NSObject PerformSelector,延遲的dispatch_after,timer事件。
Source0事件:處理如UIEvent,CFSocket這類事件。須要手動觸發。觸摸事件實際上是Source1接收系統事件後在回調 __IOHIDEventSystemClientQueueCallback() 內觸發的 Source0,Source0 再觸發的 _UIApplicationHandleEventQueue()。source0必定是要喚醒runloop及時響應並執行的,若是runloop此時在休眠等待系統的 mach_msg事件,那麼就會經過source1來喚醒runloop執行。
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不是線程安全的。試想下面這幾種狀況:
兩個線程同時設置同一個背景圖片,那麼頗有可能由於當前圖片被釋放了兩次而致使應用崩潰。
兩個線程同時設置同一個UIView的背景顏色,那麼頗有可能渲染顯示的是顏色A,而此時在UIView邏輯樹上的背景顏色屬性爲B。
兩個線程同時操做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 在系統內部,被維護着三份一樣的樹形數據結構,分別是:
圖層樹(這裏是代碼能夠操縱的,設置屬性的最終值會馬上在這裏更新);
呈現樹(是一箇中間層,系統就在這一層上更改屬性,進行各類渲染操做。好比一個動畫是更改alpha值從0到1,那麼在邏輯樹上此屬性會被馬上更新爲最終屬性1,而在動畫樹上會根據設置的動畫時間從0逐步變化到1);
渲染樹(其屬性值就是當前正被顯示在屏幕上的屬性值);
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屏幕渲染有如下兩種方式:
On-Screen Rendering
意爲當前屏幕渲染,指的是GPU的渲染操做是在當前用於顯示的屏幕緩衝區中進行。
2. Off-Screen Rendering
意爲離屏渲染,指的是GPU在當前屏幕緩衝區之外新開闢一個緩衝區進行渲染操做。
按照這樣的說法,若是將不在GPU的當前屏幕緩衝區中進行的渲染都稱爲離屏渲染,那麼就還有另外一種特殊的「離屏渲染」方式:CPU渲染。若是咱們重寫了drawRect方法,而且使用任何Core Graphics的技術進行了繪製操做,就涉及到了CPU渲染。整個渲染過程由CPU在App內同步地完成,渲染獲得的bitmap最後再交由GPU用於顯示。
相比於當前屏幕渲染,離屏渲染的代價是很高的,主要體如今兩個方面:
建立新緩衝區
要想進行離屏渲染,首先要建立一個新的緩衝區。
2. 上下文切換
離屏渲染的整個過程,須要屢次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束之後,將離屏緩衝區的渲染結果顯示到屏幕上有須要將上下文環境從離屏切換到當前屏幕。而上下文環境的切換是要付出很大代價的。
設置瞭如下屬性時,都會觸發離屏繪製:
shouldRasterize(光柵化)
masks(遮罩)
shadows(陰影)
edge antialiasing(抗鋸齒)
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介紹》
參考文章
runloop原理 (https://github.com/ming1016/study/wiki/CFRunLoop)
深刻理解runloop (http://blog.ibireme.com/2015/05/18/runloop/)
線程安全類的設計 (http://objccn.io/issue-2-4/)
iOS保持界面流暢的技巧和AsyncDisplay介紹 (http://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/)
離屏渲染 (http://foggry.com/blog/2015/05/06/chi-ping-xuan-ran-xue-xi-bi-ji/)
ios核心動畫高級技巧 (https://zsisme.gitbooks.io/ios-/content/index.html)