續言
? ? ? 在頁面間跳轉的性能優化(一)中介紹了一些基礎知識,講述了情形一與情形二的優化方式及原理,但有許多人對情形二最后兩種處理方式的原理表示不理解,不清楚處理過程,接下來會詳細分步地講述這兩種方式的原理,如果你還沒看過頁面間跳轉的性能優化(一),請先閱讀。
? ? ? 點擊下載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的一大特色,Core Animation的存在使得我們實現一些基礎動畫變得十分簡單,動畫可以使我們的App體驗更好,但動畫雖好,可不要亂加,因為動畫也是有坑的,如果處理不當,動畫就會成為App的累贅,體驗的殺手。下期將會講述動畫和動畫的坑,但不會講述怎么實現華麗的動畫。
? ? ? 工作原因,更新的速度不快,只要有時間我就會更新的。