標籤:ati 建立 強制 臨時工 nal 理解 純粹 cap 方向
續言
在頁面間跳轉的效能最佳化(一)中介紹了一些基礎知識,講述了情形一與情形二的最佳化方式及原理,但有許多人對情形二最後兩種處理方式的原理表示不理解,不清楚處理過程,接下來會詳細分步地講述這兩種方式的原理,如果你還沒看過頁面間跳轉的效能最佳化(一),請先閱讀。
點擊下載Demo,或https://github.com/IOSDelpan/SmoothTransitionDemo。
頁面間的跳轉大致分為幾個任務:1.產生將即顯示的整頁模式;2.產生我們所需要的UI元素;3.產生頁面跳轉的動畫;而這幾個任務是在同一次Loop中執行的。我們知道每一次Loop都會檢測圖層樹是否有更新,若圖層樹有更新,RunLoop會在觀察者的回呼函數_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()執行完成時,發送圖層樹的更新到渲染服務進程進行繪製渲染,如果一次Loop的時間過長,這將會使圖層樹的更新延遲,這也就是我們所說的螢幕卡頓(CPU層面的卡頓)。為瞭解決頁面跳轉延遲,我們把原本在一次Loop中所需要執行的任務進行分解,分解成幾次Loop來執行,這樣就可以既不影響App的流暢度,也不影響UI的更新。
在Demo中,我們用GCD的方式來實現“在RunLoop下一次迴圈載入UI”。
調用dispatch_async()函數把產生UI元素的任務[self loadAllLabels]提交到GCD的主隊列,在Application的主線程RunLoop進入下一次Loop時,會執行GCD主隊列裡面的任務,整個頁面跳轉的過程,即兩次Loop的工作如下。
在第一次Loop中,把耗時的任務[self loadAllLabels]提交到Main Queue,產生即將顯示的整頁模式和頁面跳轉的過渡動畫並發送到渲染服務進程進行繪製渲染,與此同時,由於Main Queue有任務待處理,GCD發送訊息mach_msg()到Mach Message Server,目標連接埠為Application Main RunLoop的dispatch Port。
由於有連接埠事件待處理,RunLoop被喚醒並進入下一次Loop,RunLoop通過發送dispatch Port到Mach Message Server來接收dispatch Port的訊息,當RunLoop接收到dispatch Port訊息後,擷取Main Queue待處理的任務[self loadAllLabels]並處理,處理完成後,把圖層樹的更新發送到渲染服務進程進行繪製渲染。
定時器處理方式的原理跟“在RunLoop下一次迴圈載入UI”的原理大致相同,但Loop的次數更多。
Main RunLoop的連接埠事件來源基本分為三類,GCD事件,定時源事件,輸入源事件(Source1),而這三類事件分別對應著三個不同的連接埠,dispatch Port,Timer Port和Source Port。每次Loop都會有兩次檢測是否有連接埠事件需要處理的機會,但是一次Loop只有一次機會處理連接埠事件,即在步驟5或步驟7觸發處理連接埠事件。RunLoop在純粹處理dispatch Port事件或Timer Port事件時,可以完整地運行一次RunLoop從被喚醒到進入休眠,即從步驟8返回到步驟7(順序8,9,2,3,4,5,6,7),所以,可以用GCD非同步嵌套的方式來實現跟定時器相同的效果。
當Main RunLoop處理dispatch Port事件時,會擷取Main Queue的所有待處理任務並處理,需要注意的是以下兩種方式的實際執行過程是不一樣的。
方式一是一次提交一個任務到Main Queue,即一次Loop處理一個任務,而方式二是一次提交三個任務到Main Queue,即一次處理完三個任務。
所以,方式二跟以下這種方式是一樣的。
以上便是“在RunLoop下一次迴圈載入UI”處理方式的實現原理。
情形三
看到Gif圖是否有種似曾相識的感覺?對頭,這一情形是最普遍存在的,存在於大部份App當中,其中還不乏一些大廠出品的App(對此個人是比較好奇的,可能是臨時工寫的,作為天朝最基層的子民,我完全可以接受這個解釋??)。從這一情形的普遍程度也側面反映出,其實絕大多數的團隊都不會去做視圖方面的效能最佳化,更不要說什麼深入的最佳化了,不過還是能理解的,視圖的效能最佳化並不是團隊一兩個人的事,開展起來各種困難,吐嘈完了??,進入主題情形三。
情形一與情形二講述了CPU方面的頁面跳轉延遲,除了CPU效能會導致頁面跳轉延遲外,GPU壓力過大同樣會出現效能問題,導致面頁跳轉時出現過場動畫不流暢,緩慢等。從Gif圖我們可以看到,整個跳轉動畫掉幀的情況非常嚴重,由於我們已經假定這一情形是由於GPU壓力過大所導致,所以不再檢測CPU方面的情況。
利用位元影像形變而強制GPU發生離屏渲染,在Demo中(根據你的機器情況,適當調整圖位的數量來實現效果),有30個位元影像發生了形變,GPU需要進行30次離屏渲染,而且由於需要離屏渲染的位元影像寸尺比較大,所以大大增加了GPU的壓力,使得整個動畫出現了嚴重掉幀的情況,我們需要一個方法,既可以快速解決動畫掉幀又不需要做頁面的最佳化。
從Gif圖我們可以看到,最佳化後頁面跳轉的整個過程並沒有出現過場動畫不流暢,緩慢等情況,即沒有出現掉幀的情況。因為圖層是繪製渲染的資料來源,所以我們需要知道最佳化後圖層樹發生了什麼變化。
最佳化原理是對視圖控制器的圖層做一次,把的結果設定為新圖層的寄宿圖,並把新圖層添加到圖層樹中(沒有與圖層樹相關聯的圖層不會被送到渲染引擎)。這種處理方式從CPU的層面看,Core Animation可以捨棄所有被完全遮蓋住的圖層,減少CPU的計算量,從GPU的層面看,GPU不需要再進行任何合成,直接Copy頂端紋理作為目標像素,減少了GPU的計算量,從而總體地提高了效能。每一種處理方式都很難做到兩全其美,很多時候我們需要在時間密度與空間密度中做出選擇,這種處理方式的缺點在於會增加記憶體的損耗(這個我倒是覺得可以忽略,建立全屏毛玻璃的時候都沒有心痛記憶體,現在倒心痛起記憶體來了??),所以這種處理方式適合用於應急。對於Application如何保持高幀數,還是要從視圖效能最佳化入手,這部份會在頁面效能最佳化篇講述。
總結
為WWDC2014講述渲染模組所用的圖(First,Second,Third是我加上去的)。這個圖非常清晰地講述了整個渲染過程,Application打包提交圖層樹並發送到渲染服務進程,渲染服務進程對圖層樹進行還原序列化得到渲染樹,利用渲染樹繪製位元影像,GPU合成位元影像,最終顯示出來。由得知,整個渲染過程分為三步,每一步都存在於獨立的空間當中,即每一步都是存在於獨立的幀裡,iOS是以每秒60次速度重新整理螢幕,即一秒60幀(fps),每一幀的時間為16.67ms,所以渲染過程的每一步理想的處理時間為16.67ms,若其中一步的處理時間超過16.67ms,就會導致螢幕重新整理失敗,即掉幀或螢幕卡頓,掉幀主要發生在第一步或第二步。
第一步的關鍵點在於Application Main RunLoop的每一次Loop是否及延時,而第二步的關鍵點則在於GPU的壓力。從前面的講述我們可以得知情形一,二,三的瓶頸處於那一步,情形一,二的瓶頸處於第一步,而情形三的瓶頸則處於第二步。
情形二主要講述了如何把會阻塞主線程的UI任務進行分解,解決頁面跳轉延遲的問題。當UI任務會阻塞主線程,但阻塞的時間並不長的時候,可以選擇用“在RunLoop下一次迴圈載入UI”的方式解決;如果UI任務會阻塞主線程且時間較長,可以選擇用“GCD嵌套載入UI”把UI任務進一步分解的方式解決;如果UI任務會阻塞主線程且希望UI可以有序出現,可以選擇用“定時器載入UI”的方式解決。
情形三主要講述了怎麼偷懶地解決頁面跳轉時出現過場動畫不流暢,緩慢等問題,而處理方式適合用來應急,想Application保持高幀數,還是要從視圖效能最佳化入手。
本文大部份內容都在講述基礎知識,因為處理方式是建立在這些基礎知識之上的,沒有這些基礎知識,即使你想最佳化也找不到方向。若文中講述有誤,還望指出。
iOS頁面間跳轉的效能最佳化(二)