前言
現在App的頁面越來越複雜,頁面初始化的工作越來越多,載入頁面所需的時間也隨之增長,如果頁面載入的時間過長,這將會影響App的流暢度及使用者體驗,我們需要解決這一問題。觀察過一些日常使用的App,頁面間跳轉的效能問題總結為以下三種情形:
1).A頁面跳轉到B頁面,由於B頁面需要載入大量的資料,所以導致頁面跳轉延遲。
2).A頁面跳轉到B頁面,由於B頁面需要載入大量UI元素,所以導致頁面跳轉延遲。
3).A頁面跳轉到B頁面,由於A或B頁面的GPU使用率過高,所以導致面頁跳轉時出現過場動畫不流暢,緩慢等。
情形一比較容易解決,利用輔助執行緒加資料即可;由於圖層樹的更新(即UI頁面的更新)需要在主執行緒上完成,所以情形二的效能優化讓很多開發人員頭痛;雖然網上有很多檢視效能優化的技術文,但據瞭解,其實大部份團隊都不會去做檢視的效能優化,情形三也是最普遍存在。本文將會講述這三種情形的效能優化,但並不會講述頁面間跳轉的過渡動畫,及頁面間跳轉的原理,這部份在網上已經有大量技術文講述。關於情形三所涉及的畫素混合,畫素對齊,離屏渲染等知識點將不進行講述,本文會講述一種偷懶的方式來優化情形三。
點選下載Demo,或https://github.com/IOSDelpan/SmoothTransitionDemo。
目錄
基礎知識
-渲染服務程式
-UIView與CALayer
-圖層樹,呈現樹,渲染樹
-UI更新過程
-RunLoop更新UI的工作
情形一
情形二
基礎知識
想在螢幕上顯示一個檢視,我們只需要簡單地實現以下程式碼,並執行Application到模擬器或真機即可。
-渲染服務程式
雖然看到的效果跟Application的程式碼是一一對應的,但檢視繪製渲染的工作並不是由Application完成的,而是由一個名為渲染服務的程式(BackBoard)來完成的,這個程式的工作便是你在螢幕上看到的一切內容。既然做實際繪製渲染工作的是渲染服務程式,那麼渲染服務程式要進行繪製渲染的依據是什麼呢?而Application跟渲染服務程式又是怎麼互動的呢?
-UIView與CALayer
為了方便往後的講述,首先簡單講述一下UIView與CALayer的關係(不講述兩者的區別)。簡單來說,UIView就是CALayer的管理器,CALayer的主要工作是為螢幕的繪製渲染提供所需的資料來源,也就是說,你在螢幕上看到的內容,都是來源於CALayer。每一個UIView都有一個Backing Layer,UIView的UI屬性跟CALayer的屬性是一一對應的,設定UIView的UI屬性實際上是設定CALayer對應的屬性,即UIView的繪製渲染工作是由CALayer完成。UIView物件之間存在著一定的層級關係,那麼所以UIView的Backing Layer也相應的存在著一定的層級關係,這個層級關係叫做圖層樹(模型樹)。接下來的知識點直接用圖層來講述。
-圖層樹,呈現樹,渲染樹
使用Core Animation的Application(iOS預設使用),除了圖層樹,還有呈現樹和渲染樹,每個圖層物件集合都扮演著不同的角色。圖層樹中的圖層物件負責儲存在螢幕上顯示的目標值,呈現樹中的圖層物件負責儲存在螢幕上顯示的瞬時值,而渲染樹的圖層物件是渲染服務程式用來繪製渲染所使用的。Application使用到的是圖層樹與呈現樹,上圖中的程式碼,使用的則是圖層樹中的圖層物件。既然渲染服務程式使用的是渲染樹,那麼圖層樹中的圖層物件所儲存的目標值又是如何顯示在螢幕上呢?
-UI更新過程
在Application的主執行緒中設定圖層樹中的圖層物件時,被設定的圖層物件會被標記為待處理狀態(在輔助執行緒設定圖層物件,圖層物件不會被標記),當Application的主執行緒即將進入休眠時,Core Animation會打包圖層樹中待處理的圖層物件,並通過IPC傳送到渲染服務程式,IPC是通過埠互動的,訊息在兩個埠間傳遞,而渲染服務程式的埠是不公開的(更多關於核心方面的資料可以閱讀《OS X與iOS核心程式設計》),當打包的圖層傳送到渲染服務程式時,這些圖層會被反序列化成渲染樹,渲染服務程式便可以開始繪製渲染的工作。
-RunLoop更新UI的工作
Application的主執行緒為了保持存活狀態,啟動了執行迴圈(RunLoop),RunLoop是一個事件處理迴圈,使用RunLoop的目的是讓你的執行緒在有工作的時候忙於工作,而沒工作的時候處於休眠狀態。下圖為RunLoop排程的順序。
從RunLoop排程的順序得知,當沒有未處理事件時,執行緒就會進入休眠狀態。在RunLoop中註冊了一個觀察者,這個觀察者用於監聽執行緒即將進入休眠的狀態,當執行緒即將進入休眠時,觀察者會執行監聽回撥_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),這個函式實現了Core Animation打包圖層樹中待處理的圖層物件,並通過IPC傳送到渲染服務程式的工作。本文不會提及深入的RunLoop原理,深入部份會在RunLoop篇講述。
情形一
絕大多數的App頁面都是用來展示各式各樣的資料,如果跳轉頁面的同時,在主執行緒載入大量的資料,便會出現以下情況。
如Gif圖所示,螢幕卡頓了一會才出現頁面跳轉的過場動畫,即出現了頁面跳轉延遲的情況。從基礎知識的UI更新過程,RunLoop更新UI的工作中得知,Application的UI更新在於主執行緒即將進入休眠時,RunLoop觀察者的回撥函式_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),只要該函式執行完,我們就可以在螢幕上看到UI更新的結果。既然知道這是由於在主執行緒載入大量資料所致,那麼我們來解決這一情形,首先需要知道是那個函式佔用了CPU,使用Instruments的Time Profiler測試一下。
從測試的結果可以看到,是setUpData這個方法佔用了主執行緒,而setUpData方法是在viewDidLoad裡被呼叫的,那麼viewDidLoad又是在何時被呼叫的呢?
從主執行緒活動的狀態以及執行堆疊可以看出,viewDidLoad是在_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()裡被呼叫的,大致過程如下圖。
知道了問題函式和主執行緒的執行堆疊,那麼解決這一問題就變得很簡單。只需要把載入資料的setUpData方法放到輔助執行緒中執行並返回結果到主執行緒顯示即可。
圖2
當我們使用多執行緒去載入資料時,由於主執行緒沒有被阻塞,所以沒有出現頁面跳轉延遲的情況,具體程式碼請看Demo。
情形二
在頁面跳轉時,除了載入資料,還需要載入UI元素,而載入UI元素的工作一般會在viewDidLoad中完成,如果需要載入的UI元素過多,同樣會出現頁面跳轉延遲的情況。
如Gif圖所示,出現了頁面跳轉延遲的情況,這是由於在viewDidLoad中生成大量的UI元素所致。在情形一中,我們用輔助執行緒載入資料解決了頁面跳轉延遲的情況,那麼我們可以以同樣的方式來載入UI元素。
雖然我們可以把生成UI元素的工作放到輔助執行緒中完成,且看到的效果相同,但這種處理方式的效率非常低,這種方式生成大量UI元素所需要的時間比直接在主執行緒中生成要多數倍,增加載入頁面所需要的時間,這顯然不是我們想要的結果,我們想要的是既可以在主執行緒生成UI,又可以不出現頁面跳轉延遲的情況。
我們知道當Application的主執行緒即將進入休眠時,Core Animation會打包圖層樹中待處理的圖層物件,除了打包圖層物件,Core Animation還會打包基礎動畫物件,一併傳送到渲染服務程式,渲染服務程式接收到圖層物件和動畫物件後,會根據動畫物件來不斷計算和繪製圖層物件,形成螢幕上看到的動畫效果,所以動畫物件能否及時傳送到渲染服務程式就顯得非常重要,這關係到你App的使用者體驗。頁面跳轉時的過場動畫的打包工作,跟viewDidLoad是在同一次RunLoop中,所以viewDidLoad的執行時間就顯得很關鍵。除了viewDidLoad以外,在UIViewController的生命週期裡還有另外幾個方法,我們來看一下這幾個方法的被排程的情況。
從列印資訊中得知,viewWillAppear,viewWillLayoutSubviews,viewDidLayoutSubviews是緊跟viewDidLoad之後執行的,所以這幾個方法的執行時間同樣很重要,但我們發現viewDidAppear方法並沒有被排程,即viewDidAppear跟前面幾個方法並在不同一次RunLoop中,既然如此,我們可以便使用viewDidAppear來解決頁面跳轉延遲的情況。
Gif圖顯示的效果和根據基礎知識猜想的結果一樣,解決了頁面跳轉延遲的情況,那麼viewDidAppear何時被呼叫?
從主執行緒的執行堆疊可得知,viewDidAppear是在過場動畫結束後被呼叫的,而過場動畫的持續時間是0.35秒。
我們來算一下整個過程所需要的時間,假設生成頁面需要0.5秒,那麼優化前後所需要的時間都是0.85秒(經測試,其實時間有減少,只是少到可以忽略,時間減少的部份應該是GPU計算量的問題),雖然問題解決了,但效果並不理想,因為完成整個過程所需要的時間並沒有減少,所以我們需要進一步優化。嘗試過很多種方式,但似乎沒有什麼方式可以很好地減少生成UI元素所需要的時間,那麼我們只能把優化的方向放在過場動畫的持續時間上了。
從Gif圖顯示的效果可以看到,完成整個過程所需要的時間明顯減少了,實現原理請看下圖。
如圖所示,把生成UI元素的任務從本次RunLoop中抽出,提交到下一次的RunLoop當中,因為本次RunLoop沒有被阻塞,所以能及時把圖層物件和動畫物件傳送到渲染服務程式,渲染服務程式便開始進行過場動畫的繪製與渲染,與此同時,Application的主執行緒RunLoop進入下一次Loop,開始執行生成UI元素的任務,即,可以理解為渲染服務程式繪製渲染過場動畫,和Application生成UI元素的任務同時進行,這樣我們便把動畫的時間也利用上,從而大大減小了整個過程所需的時間。
在Demo中,是使用GCD的方式來實現,也可以使用performSelector: withObject: afterDelay:方法來實現同樣的效果,但不建議,因為這樣會增加主執行緒RunLoop的執行時間。
我們還可以把這個耗時的任務分解成若干個小的任務來實現。
如Gif圖所示,沒有出現頁面跳轉延遲的情況。使用定器時把任務分解,可以得到同樣的結果,若是加上一些動畫,效果會更棒。在Demo中,用到的定時器是CADisplayLink,用NSTimer可以達得到樣的效果,關於CADisplayLink,建議能不用就不用,因為它會使目標執行緒長期處於活躍狀態。
情形三將會在頁面間跳轉的效能優化(二)中講述。如果文中有講錯的地方,還望指出。
Tips:雖然黑科技很強大,但也很危險,在你沒有足夠了解它的情況下,不能輕易去使用,更不能濫用。本文的講述旨在如何利用基礎知識來解決日常開發中遇到的問題,並不是硬式化地講解使用方式。