頁(yè)面間跳轉(zhuǎn)的性能優(yōu)化(二)

續(xù)言

? ? ? 在頁(yè)面間跳轉(zhuǎn)的性能優(yōu)化(一)中介紹了一些基礎(chǔ)知識(shí),講述了情形一與情形二的優(yōu)化方式及原理,但有許多人對(duì)情形二最后兩種處理方式的原理表示不理解,不清楚處理過(guò)程,接下來(lái)會(huì)詳細(xì)分步地講述這兩種方式的原理,如果你還沒(méi)看過(guò)頁(yè)面間跳轉(zhuǎn)的性能優(yōu)化(一),請(qǐng)先閱讀。

? ? ? 點(diǎn)擊下載Demo,或https://github.com/IOSDelpan/SmoothTransitionDemo。

? ? ? 頁(yè)面間的跳轉(zhuǎn)大致分為幾個(gè)任務(wù):1.生成將即顯示的頁(yè)面視圖;2.生成我們所需要的UI元素;3.生成頁(yè)面跳轉(zhuǎn)的動(dòng)畫(huà);而這幾個(gè)任務(wù)是在同一次Loop中執(zhí)行的。我們知道每一次Loop都會(huì)檢測(cè)圖層樹(shù)是否有更新,若圖層樹(shù)有更新,RunLoop會(huì)在觀察者的回調(diào)函數(shù)_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()執(zhí)行完成時(shí),發(fā)送圖層樹(shù)的更新到渲染服務(wù)進(jìn)程進(jìn)行繪制渲染,如果一次Loop的時(shí)間過(guò)長(zhǎng),這將會(huì)使圖層樹(shù)的更新延遲,這也就是我們所說(shuō)的屏幕卡頓(CPU層面的卡頓)。為了解決頁(yè)面跳轉(zhuǎn)延遲,我們把原本在一次Loop中所需要執(zhí)行的任務(wù)進(jìn)行分解,分解成幾次Loop來(lái)執(zhí)行,這樣就可以既不影響App的流暢度,也不影響UI的更新。

? ? ? 在Demo中,我們用GCD的方式來(lái)實(shí)現(xiàn)“在RunLoop下一次循環(huán)加載UI”。

? ? ? 調(diào)用dispatch_async()函數(shù)把生成UI元素的任務(wù)[self loadAllLabels]提交到GCD的主隊(duì)列,在Application的主線程RunLoop進(jìn)入下一次Loop時(shí),會(huì)執(zhí)行GCD主隊(duì)列里面的任務(wù),整個(gè)頁(yè)面跳轉(zhuǎn)的過(guò)程,即兩次Loop的工作如下。

? ? ? 在第一次Loop中,把耗時(shí)的任務(wù)[self loadAllLabels]提交到Main Queue,生成即將顯示的頁(yè)面視圖和頁(yè)面跳轉(zhuǎn)的過(guò)渡動(dòng)畫(huà)并發(fā)送到渲染服務(wù)進(jìn)程進(jìn)行繪制渲染,與此同時(shí),由于Main Queue有任務(wù)待處理,GCD發(fā)送消息mach_msg()到Mach Message Server,目標(biāo)端口為Application Main RunLoop的dispatch Port。

? ? ? 由于有端口事件待處理,RunLoop被喚醒并進(jìn)入下一次Loop,RunLoop通過(guò)發(fā)送dispatch Port到Mach Message Server來(lái)接收dispatch Port的消息,當(dāng)RunLoop接收到dispatch Port消息后,獲取Main Queue待處理的任務(wù)[self loadAllLabels]并處理,處理完成后,把圖層樹(shù)的更新發(fā)送到渲染服務(wù)進(jìn)程進(jìn)行繪制渲染。

? ? ? 定時(shí)器處理方式的原理跟“在RunLoop下一次循環(huán)加載UI”的原理大致相同,但Loop的次數(shù)更多。

? ? ? Main RunLoop的端口事件源基本分為三類,GCD事件,定時(shí)源事件,輸入源事件(Source1),而這三類事件分別對(duì)應(yīng)著三個(gè)不同的端口,dispatch Port,Timer Port和Source Port。每次Loop都會(huì)有兩次檢測(cè)是否有端口事件需要處理的機(jī)會(huì),但是一次Loop只有一次機(jī)會(huì)處理端口事件,即在步驟5或步驟7觸發(fā)處理端口事件。RunLoop在純粹處理dispatch Port事件或Timer Port事件時(shí),可以完整地運(yùn)行一次RunLoop從被喚醒到進(jìn)入休眠,即從步驟8返回到步驟7(順序8,9,2,3,4,5,6,7),所以,可以用GCD異步嵌套的方式來(lái)實(shí)現(xiàn)跟定時(shí)器相同的效果。

? ? ? 當(dāng)Main RunLoop處理dispatch Port事件時(shí),會(huì)獲取Main Queue的所有待處理任務(wù)并處理,需要注意的是以下兩種方式的實(shí)際執(zhí)行過(guò)程是不一樣的。

? ? ? 方式一是一次提交一個(gè)任務(wù)到Main Queue,即一次Loop處理一個(gè)任務(wù),而方式二是一次提交三個(gè)任務(wù)到Main Queue,即一次處理完三個(gè)任務(wù)。

? ? ? 所以,方式二跟以下這種方式是一樣的。

? ? ? 以上便是“在RunLoop下一次循環(huán)加載UI”處理方式的實(shí)現(xiàn)原理。

情形三

? ? ? 看到Gif圖是否有種似曾相識(shí)的感覺(jué)?對(duì)頭,這一情形是最普遍存在的,存在于大部份App當(dāng)中,其中還不乏一些大廠出品的App(對(duì)此個(gè)人是比較好奇的,可能是臨時(shí)工寫(xiě)的,作為天朝最基層的子民,我完全可以接受這個(gè)解釋??)。從這一情形的普遍程度也側(cè)面反映出,其實(shí)絕大多數(shù)的團(tuán)隊(duì)都不會(huì)去做視圖方面的性能優(yōu)化,更不要說(shuō)什么深入的優(yōu)化了,不過(guò)還是能理解的,視圖的性能優(yōu)化并不是團(tuán)隊(duì)一兩個(gè)人的事,開(kāi)展起來(lái)各種困難,吐嘈完了??,進(jìn)入主題情形三。

? ? ? 情形一與情形二講述了CPU方面的頁(yè)面跳轉(zhuǎn)延遲,除了CPU性能會(huì)導(dǎo)致頁(yè)面跳轉(zhuǎn)延遲外,GPU壓力過(guò)大同樣會(huì)出現(xiàn)性能問(wèn)題,導(dǎo)致面頁(yè)跳轉(zhuǎn)時(shí)出現(xiàn)過(guò)場(chǎng)動(dòng)畫(huà)不流暢,緩慢等。從Gif圖我們可以看到,整個(gè)跳轉(zhuǎn)動(dòng)畫(huà)掉幀的情況非常嚴(yán)重,由于我們已經(jīng)假定這一情形是由于GPU壓力過(guò)大所導(dǎo)致,所以不再檢測(cè)CPU方面的情況。

? ? ? 利用位圖形變而強(qiáng)制GPU發(fā)生離屏渲染,在Demo中(根據(jù)你的機(jī)器情況,適當(dāng)調(diào)整圖位的數(shù)量來(lái)實(shí)現(xiàn)效果),有30個(gè)位圖發(fā)生了形變,GPU需要進(jìn)行30次離屏渲染,而且由于需要離屏渲染的位圖寸尺比較大,所以大大增加了GPU的壓力,使得整個(gè)動(dòng)畫(huà)出現(xiàn)了嚴(yán)重掉幀的情況,我們需要一個(gè)方法,既可以快速解決動(dòng)畫(huà)掉幀又不需要做頁(yè)面的優(yōu)化。

? ? ? 從Gif圖我們可以看到,優(yōu)化后頁(yè)面跳轉(zhuǎn)的整個(gè)過(guò)程并沒(méi)有出現(xiàn)過(guò)場(chǎng)動(dòng)畫(huà)不流暢,緩慢等情況,即沒(méi)有出現(xiàn)掉幀的情況。因?yàn)閳D層是繪制渲染的數(shù)據(jù)源,所以我們需要知道優(yōu)化后圖層樹(shù)發(fā)生了什么變化。

? ? ? 優(yōu)化原理是對(duì)視圖控制器的圖層做一次截圖,把截圖的結(jié)果設(shè)置為新圖層的寄宿圖,并把新圖層添加到圖層樹(shù)中(沒(méi)有與圖層樹(shù)相關(guān)聯(lián)的圖層不會(huì)被送到渲染引擎)。這種處理方式從CPU的層面看,Core Animation可以舍棄所有被完全遮蓋住的圖層,減少CPU的計(jì)算量,從GPU的層面看,GPU不需要再進(jìn)行任何合成,直接Copy頂端紋理作為目標(biāo)像素,減少了GPU的計(jì)算量,從而總體地提高了性能。每一種處理方式都很難做到兩全其美,很多時(shí)候我們需要在時(shí)間密度與空間密度中做出選擇,這種處理方式的缺點(diǎn)在于會(huì)增加內(nèi)存的損耗(這個(gè)我倒是覺(jué)得可以忽略,創(chuàng)建全屏毛玻璃的時(shí)候都沒(méi)有心痛內(nèi)存,現(xiàn)在倒心痛起內(nèi)存來(lái)了??),所以這種處理方式適合用于應(yīng)急。對(duì)于Application如何保持高幀數(shù),還是要從視圖性能優(yōu)化入手,這部份會(huì)在頁(yè)面性能優(yōu)化篇講述。

總結(jié)

? ? ? 上圖為WWDC2014講述渲染模塊所用的圖(First,Second,Third是我加上去的)。這個(gè)圖非常清晰地講述了整個(gè)渲染過(guò)程,Application打包提交圖層樹(shù)并發(fā)送到渲染服務(wù)進(jìn)程,渲染服務(wù)進(jìn)程對(duì)圖層樹(shù)進(jìn)行反序列化得到渲染樹(shù),利用渲染樹(shù)繪制位圖,GPU合成位圖,最終顯示出來(lái)。由上圖得知,整個(gè)渲染過(guò)程分為三步,每一步都存在于獨(dú)立的空間當(dāng)中,即每一步都是存在于獨(dú)立的幀里,iOS是以每秒60次速度刷新屏幕,即一秒60幀(fps),每一幀的時(shí)間為16.67ms,所以渲染過(guò)程的每一步理想的處理時(shí)間為16.67ms,若其中一步的處理時(shí)間超過(guò)16.67ms,就會(huì)導(dǎo)致屏幕刷新失敗,即掉幀或屏幕卡頓,掉幀主要發(fā)生在第一步或第二步。

? ? ? 第一步的關(guān)鍵點(diǎn)在于Application Main RunLoop的每一次Loop是否及延時(shí),而第二步的關(guān)鍵點(diǎn)則在于GPU的壓力。從前面的講述我們可以得知情形一,二,三的瓶頸處于那一步,情形一,二的瓶頸處于第一步,而情形三的瓶頸則處于第二步。

? ? ? 情形二主要講述了如何把會(huì)阻塞主線程的UI任務(wù)進(jìn)行分解,解決頁(yè)面跳轉(zhuǎn)延遲的問(wèn)題。當(dāng)UI任務(wù)會(huì)阻塞主線程,但阻塞的時(shí)間并不長(zhǎng)的時(shí)候,可以選擇用“在RunLoop下一次循環(huán)加載UI”的方式解決;如果UI任務(wù)會(huì)阻塞主線程且時(shí)間較長(zhǎng),可以選擇用“GCD嵌套加載UI”把UI任務(wù)進(jìn)一步分解的方式解決;如果UI任務(wù)會(huì)阻塞主線程且希望UI可以有序出現(xiàn),可以選擇用“定時(shí)器加載UI”的方式解決。

? ? ? 情形三主要講述了怎么偷懶地解決頁(yè)面跳轉(zhuǎn)時(shí)出現(xiàn)過(guò)場(chǎng)動(dòng)畫(huà)不流暢,緩慢等問(wèn)題,而處理方式適合用來(lái)應(yīng)急,想Application保持高幀數(shù),還是要從視圖性能優(yōu)化入手。

? ? ? 本文大部份內(nèi)容都在講述基礎(chǔ)知識(shí),因?yàn)樘幚矸绞绞墙⒃谶@些基礎(chǔ)知識(shí)之上的,沒(méi)有這些基礎(chǔ)知識(shí),即使你想優(yōu)化也找不到方向。若文中講述有誤,還望指出。

下期預(yù)告

? ? ? 動(dòng)畫(huà)是iOS的一大特色,Core Animation的存在使得我們實(shí)現(xiàn)一些基礎(chǔ)動(dòng)畫(huà)變得十分簡(jiǎn)單,動(dòng)畫(huà)可以使我們的App體驗(yàn)更好,但動(dòng)畫(huà)雖好,可不要亂加,因?yàn)閯?dòng)畫(huà)也是有坑的,如果處理不當(dāng),動(dòng)畫(huà)就會(huì)成為App的累贅,體驗(yàn)的殺手。下期將會(huì)講述動(dòng)畫(huà)和動(dòng)畫(huà)的坑,但不會(huì)講述怎么實(shí)現(xiàn)華麗的動(dòng)畫(huà)。

? ? ? 工作原因,更新的速度不快,只要有時(shí)間我就會(huì)更新的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容