頁面間跳轉(zhuǎn)的性能優(yōu)化(一)
來源:Delpan
鏈接:http://www.reibang.com/p/77847c0027c9
前言
現(xiàn)在App的頁面越來越復(fù)雜,頁面初始化的工作越來越多错邦,加載頁面所需的時(shí)間也隨之增長俗扇,如果頁面加載的時(shí)間過長粪狼,這將會(huì)影響App的流暢度及用戶體驗(yàn)视事,我們需要解決這一問題至耻。觀察過一些日常使用的App镣典,頁面間跳轉(zhuǎn)的性能問題總結(jié)為以下三種情形:
1).A頁面跳轉(zhuǎn)到B頁面,由于B頁面需要加載大量的數(shù)據(jù)运吓,所以導(dǎo)致頁面跳轉(zhuǎn)延遲渴邦。
2).A頁面跳轉(zhuǎn)到B頁面,由于B頁面需要加載大量UI元素拘哨,所以導(dǎo)致頁面跳轉(zhuǎn)延遲谋梭。
3).A頁面跳轉(zhuǎn)到B頁面,由于A或B頁面的GPU使用率過高倦青,所以導(dǎo)致面頁跳轉(zhuǎn)時(shí)出現(xiàn)過場動(dòng)畫不流暢瓮床,緩慢等。
情形一比較容易解決产镐,利用輔助線程加數(shù)據(jù)即可隘庄;由于圖層樹的更新(即UI頁面的更新)需要在主線程上完成,所以情形二的性能優(yōu)化讓很多開發(fā)人員頭痛癣亚;雖然網(wǎng)上有很多視圖性能優(yōu)化的技術(shù)文丑掺,但據(jù)了解,其實(shí)大部份團(tuán)隊(duì)都不會(huì)去做視圖的性能優(yōu)化述雾,情形三也是最普遍存在街州。本文將會(huì)講述這三種情形的性能優(yōu)化,但并不會(huì)講述頁面間跳轉(zhuǎn)的過渡動(dòng)畫玻孟,及頁面間跳轉(zhuǎn)的原理唆缴,這部份在網(wǎng)上已經(jīng)有大量技術(shù)文講述。關(guān)于情形三所涉及的像素混合黍翎,像素對(duì)齊面徽,離屏渲染等知識(shí)點(diǎn)將不進(jìn)行講述,本文會(huì)講述一種偷懶的方式來優(yōu)化情形三匣掸。
點(diǎn)擊下載Demo斗忌,或https://github.com/IOSDelpan/SmoothTransitionDemo质礼。
目錄
基礎(chǔ)知識(shí)
-渲染服務(wù)進(jìn)程
-UIView與CALayer
-圖層樹,呈現(xiàn)樹织阳,渲染樹
-UI更新過程
-RunLoop更新UI的工作
情形一
情形二
基礎(chǔ)知識(shí)
想在屏幕上顯示一個(gè)視圖,我們只需要簡單地實(shí)現(xiàn)以下代碼砰粹,并運(yùn)行Application到模擬器或真機(jī)即可唧躲。
-渲染服務(wù)進(jìn)程
雖然看到的效果跟Application的代碼是一一對(duì)應(yīng)的,但視圖繪制渲染的工作并不是由Application完成的碱璃,而是由一個(gè)名為渲染服務(wù)的進(jìn)程(BackBoard)來完成的弄痹,這個(gè)進(jìn)程的工作便是你在屏幕上看到的一切內(nèi)容。既然做實(shí)際繪制渲染工作的是渲染服務(wù)進(jìn)程嵌器,那么渲染服務(wù)進(jìn)程要進(jìn)行繪制渲染的依據(jù)是什么呢肛真?而Application跟渲染服務(wù)進(jìn)程又是怎么交互的呢?
-UIView與CALayer
為了方便往后的講述爽航,首先簡單講述一下UIView與CALayer的關(guān)系(不講述兩者的區(qū)別)蚓让。簡單來說,UIView就是CALayer的管理器讥珍,CALayer的主要工作是為屏幕的繪制渲染提供所需的數(shù)據(jù)源历极,也就是說,你在屏幕上看到的內(nèi)容衷佃,都是來源于CALayer趟卸。每一個(gè)UIView都有一個(gè)Backing Layer,UIView的UI屬性跟CALayer的屬性是一一對(duì)應(yīng)的氏义,設(shè)置UIView的UI屬性實(shí)際上是設(shè)置CALayer對(duì)應(yīng)的屬性锄列,即UIView的繪制渲染工作是由CALayer完成。UIView對(duì)象之間存在著一定的層級(jí)關(guān)系惯悠,那么所以UIView的Backing Layer也相應(yīng)的存在著一定的層級(jí)關(guān)系邻邮,這個(gè)層級(jí)關(guān)系叫做圖層樹(模型樹)。接下來的知識(shí)點(diǎn)直接用圖層來講述吮螺。
-圖層樹饶囚,呈現(xiàn)樹,渲染樹
使用Core Animation的Application(iOS默認(rèn)使用)鸠补,除了圖層樹萝风,還有呈現(xiàn)樹和渲染樹,每個(gè)圖層對(duì)象集合都扮演著不同的角色紫岩。圖層樹中的圖層對(duì)象負(fù)責(zé)存儲(chǔ)在屏幕上顯示的目標(biāo)值规惰,呈現(xiàn)樹中的圖層對(duì)象負(fù)責(zé)存儲(chǔ)在屏幕上顯示的瞬時(shí)值,而渲染樹的圖層對(duì)象是渲染服務(wù)進(jìn)程用來繪制渲染所使用的泉蝌。Application使用到的是圖層樹與呈現(xiàn)樹歇万,上圖中的代碼揩晴,使用的則是圖層樹中的圖層對(duì)象。既然渲染服務(wù)進(jìn)程使用的是渲染樹贪磺,那么圖層樹中的圖層對(duì)象所存儲(chǔ)的目標(biāo)值又是如何顯示在屏幕上呢硫兰?
-UI更新過程
在Application的主線程中設(shè)置圖層樹中的圖層對(duì)象時(shí),被設(shè)置的圖層對(duì)象會(huì)被標(biāo)記為待處理狀態(tài)(在輔助線程設(shè)置圖層對(duì)象寒锚,圖層對(duì)象不會(huì)被標(biāo)記)劫映,當(dāng)Application的主線程即將進(jìn)入休眠時(shí),Core Animation會(huì)打包圖層樹中待處理的圖層對(duì)象刹前,并通過IPC發(fā)送到渲染服務(wù)進(jìn)程泳赋,IPC是通過端口交互的,消息在兩個(gè)端口間傳遞喇喉,而渲染服務(wù)進(jìn)程的端口是不公開的(更多關(guān)于內(nèi)核方面的資料可以閱讀《OS X與iOS內(nèi)核編程》)祖今,當(dāng)打包的圖層發(fā)送到渲染服務(wù)進(jìn)程時(shí),這些圖層會(huì)被反序列化成渲染樹拣技,渲染服務(wù)進(jìn)程便可以開始繪制渲染的工作千诬。
-RunLoop更新UI的工作
Application的主線程為了保持存活狀態(tài),啟動(dòng)了運(yùn)行循環(huán)(RunLoop)过咬,RunLoop是一個(gè)事件處理循環(huán)大渤,使用RunLoop的目的是讓你的線程在有工作的時(shí)候忙于工作,而沒工作的時(shí)候處于休眠狀態(tài)掸绞。下圖為RunLoop調(diào)度的順序泵三。
從RunLoop調(diào)度的順序得知,當(dāng)沒有未處理事件時(shí)衔掸,線程就會(huì)進(jìn)入休眠狀態(tài)烫幕。在RunLoop中注冊了一個(gè)觀察者,這個(gè)觀察者用于監(jiān)聽線程即將進(jìn)入休眠的狀態(tài)敞映,當(dāng)線程即將進(jìn)入休眠時(shí)较曼,觀察者會(huì)執(zhí)行監(jiān)聽回調(diào)_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv(),這個(gè)函數(shù)實(shí)現(xiàn)了Core Animation打包圖層樹中待處理的圖層對(duì)象振愿,并通過IPC發(fā)送到渲染服務(wù)進(jìn)程的工作捷犹。本文不會(huì)提及深入的RunLoop原理,深入部份會(huì)在RunLoop篇講述冕末。
情形一
絕大多數(shù)的App頁面都是用來展示各式各樣的數(shù)據(jù)萍歉,如果跳轉(zhuǎn)頁面的同時(shí),在主線程加載大量的數(shù)據(jù)档桃,便會(huì)出現(xiàn)以下情況枪孩。
如Gif圖所示,屏幕卡頓了一會(huì)才出現(xiàn)頁面跳轉(zhuǎn)的過場動(dòng)畫,即出現(xiàn)了頁面跳轉(zhuǎn)延遲的情況蔑舞。從基礎(chǔ)知識(shí)的UI更新過程拒担,RunLoop更新UI的工作中得知,Application的UI更新在于主線程即將進(jìn)入休眠時(shí)攻询,RunLoop觀察者的回調(diào)函數(shù)_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()从撼,只要該函數(shù)執(zhí)行完,我們就可以在屏幕上看到UI更新的結(jié)果蜕窿。既然知道這是由于在主線程加載大量數(shù)據(jù)所致谋逻,那么我們來解決這一情形,首先需要知道是那個(gè)函數(shù)占用了CPU桐经,使用Instruments的Time Profiler測試一下。
從測試的結(jié)果可以看到浙滤,是setUpData這個(gè)方法占用了主線程阴挣,而setUpData方法是在viewDidLoad里被調(diào)用的,那么viewDidLoad又是在何時(shí)被調(diào)用的呢纺腊?
從主線程活動(dòng)的狀態(tài)以及執(zhí)行堆椗线郑可以看出,viewDidLoad是在_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()里被調(diào)用的揖膜,大致過程如下圖誓沸。
知道了問題函數(shù)和主線程的執(zhí)行堆棧,那么解決這一問題就變得很簡單壹粟。只需要把加載數(shù)據(jù)的setUpData方法放到輔助線程中執(zhí)行并返回結(jié)果到主線程顯示即可拜隧。
當(dāng)我們使用多線程去加載數(shù)據(jù)時(shí),由于主線程沒有被阻塞趁仙,所以沒有出現(xiàn)頁面跳轉(zhuǎn)延遲的情況洪添,具體代碼請(qǐng)看Demo。
情形二
在頁面跳轉(zhuǎn)時(shí)雀费,除了加載數(shù)據(jù)干奢,還需要加載UI元素,而加載UI元素的工作一般會(huì)在viewDidLoad中完成盏袄,如果需要加載的UI元素過多忿峻,同樣會(huì)出現(xiàn)頁面跳轉(zhuǎn)延遲的情況。
如Gif圖所示辕羽,出現(xiàn)了頁面跳轉(zhuǎn)延遲的情況逛尚,這是由于在viewDidLoad中生成大量的UI元素所致。在情形一中逛漫,我們用輔助線程加載數(shù)據(jù)解決了頁面跳轉(zhuǎn)延遲的情況黑低,那么我們可以以同樣的方式來加載UI元素。
雖然我們可以把生成UI元素的工作放到輔助線程中完成,且看到的效果相同克握,但這種處理方式的效率非常低蕾管,這種方式生成大量UI元素所需要的時(shí)間比直接在主線程中生成要多數(shù)倍,增加加載頁面所需要的時(shí)間菩暗,這顯然不是我們想要的結(jié)果掰曾,我們想要的是既可以在主線程生成UI,又可以不出現(xiàn)頁面跳轉(zhuǎn)延遲的情況停团。
我們知道當(dāng)Application的主線程即將進(jìn)入休眠時(shí)旷坦,Core Animation會(huì)打包圖層樹中待處理的圖層對(duì)象,除了打包圖層對(duì)象佑稠,Core Animation還會(huì)打包基礎(chǔ)動(dòng)畫對(duì)象秒梅,一并發(fā)送到渲染服務(wù)進(jìn)程,渲染服務(wù)進(jìn)程接收到圖層對(duì)象和動(dòng)畫對(duì)象后舌胶,會(huì)根據(jù)動(dòng)畫對(duì)象來不斷計(jì)算和繪制圖層對(duì)象捆蜀,形成屏幕上看到的動(dòng)畫效果,所以動(dòng)畫對(duì)象能否及時(shí)發(fā)送到渲染服務(wù)進(jìn)程就顯得非常重要幔嫂,這關(guān)系到你App的用戶體驗(yàn)辆它。頁面跳轉(zhuǎn)時(shí)的過場動(dòng)畫的打包工作,跟viewDidLoad是在同一次RunLoop中履恩,所以viewDidLoad的執(zhí)行時(shí)間就顯得很關(guān)鍵锰茉。除了viewDidLoad以外,在UIViewController的生命周期里還有另外幾個(gè)方法切心,我們來看一下這幾個(gè)方法的被調(diào)度的情況飒筑。
從打印信息中得知,viewWillAppear昙衅,viewWillLayoutSubviews扬霜,viewDidLayoutSubviews是緊跟viewDidLoad之后執(zhí)行的,所以這幾個(gè)方法的執(zhí)行時(shí)間同樣很重要而涉,但我們發(fā)現(xiàn)viewDidAppear方法并沒有被調(diào)度著瓶,即viewDidAppear跟前面幾個(gè)方法并在不同一次RunLoop中,既然如此啼县,我們可以便使用viewDidAppear來解決頁面跳轉(zhuǎn)延遲的情況材原。
Gif圖顯示的效果和根據(jù)基礎(chǔ)知識(shí)猜想的結(jié)果一樣,解決了頁面跳轉(zhuǎn)延遲的情況季眷,那么viewDidAppear何時(shí)被調(diào)用余蟹?
從主線程的執(zhí)行堆棧可得知子刮,viewDidAppear是在過場動(dòng)畫結(jié)束后被調(diào)用的威酒,而過場動(dòng)畫的持續(xù)時(shí)間是0.35秒窑睁。
我們來算一下整個(gè)過程所需要的時(shí)間,假設(shè)生成頁面需要0.5秒葵孤,那么優(yōu)化前后所需要的時(shí)間都是0.85秒(經(jīng)測試担钮,其實(shí)時(shí)間有減少,只是少到可以忽略尤仍,時(shí)間減少的部份應(yīng)該是GPU計(jì)算量的問題)箫津,雖然問題解決了,但效果并不理想宰啦,因?yàn)橥瓿烧麄€(gè)過程所需要的時(shí)間并沒有減少苏遥,所以我們需要進(jìn)一步優(yōu)化。嘗試過很多種方式赡模,但似乎沒有什么方式可以很好地減少生成UI元素所需要的時(shí)間田炭,那么我們只能把優(yōu)化的方向放在過場動(dòng)畫的持續(xù)時(shí)間上了。
從Gif圖顯示的效果可以看到漓柑,完成整個(gè)過程所需要的時(shí)間明顯減少了诫肠,實(shí)現(xiàn)原理請(qǐng)看下圖。
如圖所示欺缘,把生成UI元素的任務(wù)從本次RunLoop中抽出,提交到下一次的RunLoop當(dāng)中挤安,因?yàn)楸敬蜶unLoop沒有被阻塞谚殊,所以能及時(shí)把圖層對(duì)象和動(dòng)畫對(duì)象發(fā)送到渲染服務(wù)進(jìn)程,渲染服務(wù)進(jìn)程便開始進(jìn)行過場動(dòng)畫的繪制與渲染蛤铜,與此同時(shí)嫩絮,Application的主線程RunLoop進(jìn)入下一次Loop,開始執(zhí)行生成UI元素的任務(wù)围肥,即剿干,可以理解為渲染服務(wù)進(jìn)程繪制渲染過場動(dòng)畫,和Application生成UI元素的任務(wù)同時(shí)進(jìn)行穆刻,這樣我們便把動(dòng)畫的時(shí)間也利用上置尔,從而大大減小了整個(gè)過程所需的時(shí)間。
在Demo中氢伟,是使用GCD的方式來實(shí)現(xiàn)榜轿,也可以使用performSelector: withObject: afterDelay:方法來實(shí)現(xiàn)同樣的效果,但不建議朵锣,因?yàn)檫@樣會(huì)增加主線程RunLoop的執(zhí)行時(shí)間谬盐。
我們還可以把這個(gè)耗時(shí)的任務(wù)分解成若干個(gè)小的任務(wù)來實(shí)現(xiàn)。
如Gif圖所示诚些,沒有出現(xiàn)頁面跳轉(zhuǎn)延遲的情況飞傀。使用定器時(shí)把任務(wù)分解,可以得到同樣的結(jié)果,若是加上一些動(dòng)畫砸烦,效果會(huì)更棒弃鸦。在Demo中,用到的定時(shí)器是CADisplayLink外冀,用NSTimer可以達(dá)得到樣的效果寡键,關(guān)于CADisplayLink,建議能不用就不用雪隧,因?yàn)樗鼤?huì)使目標(biāo)線程長期處于活躍狀態(tài)西轩。
情形三將會(huì)在頁面間跳轉(zhuǎn)的性能優(yōu)化(二)中講述。如果文中有講錯(cuò)的地方脑沿,還望指出藕畔。
Tips:雖然黑科技很強(qiáng)大,但也很危險(xiǎn)庄拇,在你沒有足夠了解它的情況下注服,不能輕易去使用,更不能濫用措近。本文的講述旨在如何利用基礎(chǔ)知識(shí)來解決日常開發(fā)中遇到的問題溶弟,并不是硬式化地講解使用方式。
閱讀 281712 投訴
精選留言
寫留言
5
wh
放在 view did appear 每次調(diào)用那個(gè)頁面都會(huì)執(zhí)行一次吧 比如 返回這個(gè)頁面 也會(huì)調(diào)用一次
2天前
3
DENG
這個(gè)好瞭郑,mark
3天前
3
003
漲知識(shí)
3天前
以上留言由公眾號(hào)篩選后顯示
了解留言功能詳情
?