iOS知識梳理 - UI(一)渲染&觸摸

梳理一下iOS的UI。性能優化

iOS的UI相關的重要概念就幾個:Window,ViewController,View,Layer。bash

首先咱們要知道,在這些概念中,UI展現的核心的layer,全部決定最終展現的信息都在layer上。框架

View是對layer的直接封裝,提供了更簡潔的接口,並對部分外部輸入(touch事件等)做出處理。每一個View都關聯了一個Layer,對這個View的UI相關修改基本上都會被同步到Layer上;當View在addSubview時,subview的layer也會被添加到layer上。ide

ViewController是MVC中的Controller層,負責對View的組織管理,以及VC間的跳轉等處理。能夠簡單理解爲頁面級的管理器。跟View和Layer的關係相似,經過VC的方法改變UI時,是經過其View實現的。push一個VC時,實質上是在移動VC關聯的View。佈局

UIWindow實際上是UIView的子類,做爲一種特殊的View,它表明了iOS UI中最頂層的層級劃分。在iOS的應用框架下,全部UI是必須掛在Window下才可以展現的,Window能夠理解爲UI的入口。另外Window也是承載用戶交互的核心。性能

下面梳理幾個值得關注的點。優化

渲染流程

iOS的渲染流程跟Core Animation這個框架關係比較大,Core Animation其實不僅是作動畫,它管理着圖層相關的一切。咱們的UI會以圖層的方式存儲在圖層樹中,Core Animation的職責就是將這些圖層儘量快地組合並渲染到屏幕上。動畫

渲染流程沒有開源,相關資料不是不少,ui

程序內的有:spa

  • 佈局 - 這是準備你的視圖/圖層的層級關係,以及設置圖層屬性(位置,背景色,邊框等等)的階段。
  • 顯示 - 這是圖層的寄宿圖片被繪製的階段。繪製有可能涉及你的-drawRect:-drawLayer:inContext:方法的調用路徑。
  • 準備 - 這是Core Animation準備發送動畫數據到渲染服務的階段。這同時也是Core Animation將要執行一些別的事務例如解碼動畫過程當中將要顯示的圖片的時間點。
  • 提交 - 這是最後的階段,Core Animation打包全部圖層和動畫屬性,而後經過IPC(內部處理通訊)發送到渲染服務進行顯示。

程序外的(系統渲染服務)有:

  • 對全部的圖層屬性計算中間值,設置OpenGL幾何形狀(紋理化的三角形)來執行渲染
  • 在屏幕上渲染可見的三角形

咱們直接接觸的Layer構成的樹結構稱爲模型樹,它記錄了全部屬性。若是有動畫應用到Layer上,當前的模型樹的數據實際上是動畫結束後的數據,而Layer當前應當展現的數據,對應着Presentation Layer,構成了呈現樹。渲染時呈現樹被打包發送給渲染服務,渲染服務將其反序列化爲一顆渲染樹來執行最終渲染。

在一個通用的渲染流程中,拿到圖層後,一般須要進行光柵化和合成。光柵化是指,在一個原始的Layer中,一般只是保存了繪製指令或相關屬性等原始數據,經過這些原始數據生成每一個像素的顏色,也就是內存中的圖形數據的過程,稱爲光柵化;而合成是指,一個界面有不少個Layer構成,若是每一個layer獨立生成了本身的圖形數據,須要將其合成在一塊兒。

光柵化的過程,也能夠是每一個Layer光柵化到對應屏幕的目標緩衝區中,而非本身單獨的緩衝區,這種稱爲直接光柵化。若是全部Layer都直接光柵化,那麼合成這個步驟就沒有必要存在了。但不少時候仍是須要給一些Layer分配本身的緩衝區的,也就是間接光柵化。一方面是性能優化,有些Layer內容沒變,每次都去重繪代價比較高,能夠分配一個獨立的緩衝區,只在須要的時候重繪,每次屏幕刷新只是從這個緩衝區copy到目標緩衝區,性能消耗大大下降了;另外一方面,根據內容的不一樣,有的Layer須要CPU繪製,有的須要GPU,兩個繪製一般是不在一個串行的流水線上的,而且CPU繪製性能通常會差一些,所以每每給CPU繪製的內容分配獨立的緩衝區。

CoreGraphics是個主要依賴CPU渲染的框架,若是咱們在drawrect或drawLayer方法中使用了CoreGraphics或直接把一個CGImage賦值給Layer的contents,那麼在渲染前,Layer就在內存中分配了一個緩衝區存放了圖形數據,實際上是在發送給渲染服務前就進行了軟件光柵化;而通常的Layer,自己實際上是繪製指令/屬性的集合,須要生成OpenGL/Mental的繪製指令,是發送給系統渲染服務後,經過GPU進行光柵化。這一部分,可能大多數時候是直接光柵化。

可是也有些時候由於能力的問題,必須進行間接光柵化。好比parent layer設置了 cornerRadius+clipsToBounds,就只能先將這個Layer和它的全部sublayer先合成後再作裁剪,最終再copy到目標緩衝區。這在iOS目前的渲染流程下是不可避免的,被稱爲離屏渲染,須要咱們在開發過程當中酌情避免。常見的引發離屏渲染的屬性,除了 cornerRadius+clipsToBounds 外,還有shadow、group opacity、mask、UIBlurEffect等。

響應鏈

這裏主要關注一下touch事件。

touch事件的根源是屏幕(硬件),能拿到的只是屏幕座標,而後經過系統分發到應用而後按UIKit這套邏輯來處理。

那麼顯然,應用首先感知到touch事件的應當是UIApplication。而後經過視圖位置與層級關係尋找到真正點擊的View。

事件響應的完整流程是:

從KeyWindow開始經過hitTest方法層層遍歷找到目標View,這裏主要是經過位置關係進行尋找;

找到目標View(First Responser)後,再按視圖層級向上傳遞。

從上下向下的查找邏輯主要是依賴視圖的frame的,簡單寫個僞代碼以下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 不在本身內,直接return
    if(![self pointInside:point withEvent:event]){
        return nil;
    }
    // 倒着遍歷,從外到內第一個符合條件的subview
    for(int i = self.subViews.length - 1;i>=0;i--){
        UIView *subView = self.subViews[i];
        UIView *targetView = [subView hitTest:point withEvent:event];
        if(targetView){
            return targetView;
        }
    }
    // subview都不知足條件,返回self
    return self;
}
複製代碼

從下向上的響應鏈傳遞主要是依賴視圖的層級關係:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
  self.next.touchesBegan(touches, with: event);
}
複製代碼

next是UIResponder的屬性,

響應鏈

如上圖所示,若是一個View是UIViewController的根View,那麼它的Next responder是ViewController,不然是它的父View,直到傳給Window再到UIApplication。

這兩個過程當中咱們能夠作的主要是:

  1. 自上而下的第一響應者尋找過程,能夠重寫HitTest/pointInside方法,使得一個View改變響應區域。
  2. 自下而上的事件傳遞中,能夠在必要時阻斷事件傳遞或轉發給其它響應者進行處理。

UIControl

UIControl繼承自UIView,UIControl的子類們如UIButton能夠添加點擊等事件:

button.addTarget(self, action: #selector(onClickButton), for: UIControl.Event.touchUpInside);

UIControl主要有兩個特色:

  1. UIControl對全部Touch事件都會阻斷
  2. UIControl只有本身是第一響應者的時候纔會處理UIControl.Event

手勢

手勢是對touch事件的更高層封裝,能夠對應一個或一系列的touch事件。

所以手勢的識別會有個狀態機進行維護:

img

如圖所示,對於離散的手勢,狀態比較簡單,只有Possible、Failed、Recognized三種狀態。

而對於連續的手勢,在第一次識別到touch時會變爲Began,而後變爲Changed,而後一直Changed -> Changed,直到用戶手指離開視圖,狀態爲Recognized。

默認地,多個手勢識別器不會同時識別,且有默認順序。能夠經過gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:控制多個手勢同時識別;能夠經過requireGestureRecognizerToFail:控制手勢識別的順序。

手勢和Touch

img

默認地,touchesBegan和touchesMoved事件是同時傳遞給手勢識別器和View的,而touchesEnd事件則會先傳遞給手勢識別器,手勢識別器若是識別成功,會傳遞touchsCanceled給View,若是識別失敗,則把touchedEnd傳給View。

手勢識別器有幾個屬性會影響這一過程:

  • delaysTouchesBegan(默認NO): touchesBegan/Move會先發給手勢識別器,保證當一個手勢識別器在識別手勢時,不會有touch事件傳遞給View。
  • delaysTouchesEnded(默認爲YES):即上面所說的,會等到手勢識別出結果再發送Canceled/Ended給視圖。
相關文章
相關標籤/搜索