微博 Demo 性能優(yōu)化技巧

2018-02-24 15:29 更新

微博 Demo 性能優(yōu)化技巧

我為了演示 YYKit 的功能,實(shí)現(xiàn)了微博和 Twitter 的 Demo,并為它們做了不少性能優(yōu)化,下面就是優(yōu)化時(shí)用到的一些技巧。

預(yù)排版

當(dāng)獲取到 API JSON 數(shù)據(jù)后,我會(huì)把每條 Cell 需要的數(shù)據(jù)都在后臺(tái)線程計(jì)算并封裝為一個(gè)布局對(duì)象 CellLayout。CellLayout 包含所有文本的 CoreText 排版結(jié)果、Cell 內(nèi)部每個(gè)控件的高度、Cell 的整體高度。每個(gè) CellLayout 的內(nèi)存占用并不多,所以當(dāng)生成后,可以全部緩存到內(nèi)存,以供稍后使用。這樣,TableView 在請(qǐng)求各個(gè)高度函數(shù)時(shí),不會(huì)消耗任何多余計(jì)算量;當(dāng)把 CellLayout 設(shè)置到 Cell 內(nèi)部時(shí),Cell 內(nèi)部也不用再計(jì)算布局了。

對(duì)于通常的 TableView 來(lái)說(shuō),提前在后臺(tái)計(jì)算好布局結(jié)果是非常重要的一個(gè)性能優(yōu)化點(diǎn)。為了達(dá)到最高性能,你可能需要犧牲一些開(kāi)發(fā)速度,不要用 Autolayout 等技術(shù),少用 UILabel 等文本控件。但如果你對(duì)性能的要求并不那么高,可以嘗試用 TableView 的預(yù)估高度的功能,并把每個(gè) Cell 高度緩存下來(lái)。這里有個(gè)來(lái)自百度知道團(tuán)隊(duì)的開(kāi)源項(xiàng)目可以很方便的幫你實(shí)現(xiàn)這一點(diǎn):FDTemplateLayoutCell。

預(yù)渲染

微博的頭像在某次改版中換成了圓形,所以我也跟進(jìn)了一下。當(dāng)頭像下載下來(lái)后,我會(huì)在后臺(tái)線程將頭像預(yù)先渲染為圓形并單獨(dú)保存到一個(gè) ImageCache 中去。

對(duì)于 TableView 來(lái)說(shuō),Cell 內(nèi)容的離屏渲染會(huì)帶來(lái)較大的 GPU 消耗。在 Twitter Demo 中,我為了圖省事兒用到了不少 layer 的圓角屬性,你可以在低性能的設(shè)備(比如 iPad 3)上快速滑動(dòng)一下這個(gè)列表,能感受到雖然列表并沒(méi)有較大的卡頓,但是整體的平均幀數(shù)降了下來(lái)。用 Instument 查看時(shí)能夠看到 GPU 已經(jīng)滿負(fù)荷運(yùn)轉(zhuǎn),而 CPU 卻比較清閑。為了避免離屏渲染,你應(yīng)當(dāng)盡量避免使用 layer 的 border、corner、shadow、mask 等技術(shù),而盡量在后臺(tái)線程預(yù)先繪制好對(duì)應(yīng)內(nèi)容。

異步繪制

我只在顯示文本的控件上用到了異步繪制的功能,但效果很不錯(cuò)。我參考 ASDK 的原理,實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的異步繪制控件。這塊代碼我單獨(dú)提取出來(lái),放到了這里:YYAsyncLayer。YYAsyncLayer 是 CALayer 的子類,當(dāng)它需要顯示內(nèi)容(比如調(diào)用了 [layer setNeedDisplay])時(shí),它會(huì)向 delegate,也就是 UIView 請(qǐng)求一個(gè)異步繪制的任務(wù)。在異步繪制時(shí),Layer 會(huì)傳遞一個(gè)?BOOL(^isCancelled)()這樣的 block,繪制代碼可以隨時(shí)調(diào)用該 block 判斷繪制任務(wù)是否已經(jīng)被取消。

當(dāng) TableView 快速滑動(dòng)時(shí),會(huì)有大量異步繪制任務(wù)提交到后臺(tái)線程去執(zhí)行。但是有時(shí)滑動(dòng)速度過(guò)快時(shí),繪制任務(wù)還沒(méi)有完成就可能已經(jīng)被取消了。如果這時(shí)仍然繼續(xù)繪制,就會(huì)造成大量的 CPU 資源浪費(fèi),甚至阻塞線程并造成后續(xù)的繪制任務(wù)遲遲無(wú)法完成。我的做法是盡量快速、提前判斷當(dāng)前繪制任務(wù)是否已經(jīng)被取消;在繪制每一行文本前,我都會(huì)調(diào)用 isCancelled() 來(lái)進(jìn)行判斷,保證被取消的任務(wù)能及時(shí)退出,不至于影響后續(xù)操作。

目前有些第三方微博客戶端(比如 VVebo、墨客等),使用了一種方式來(lái)避免高速滑動(dòng)時(shí) Cell 的繪制過(guò)程,相關(guān)實(shí)現(xiàn)見(jiàn)這個(gè)項(xiàng)目:VVeboTableViewDemo。它的原理是,當(dāng)滑動(dòng)時(shí),松開(kāi)手指后,立刻計(jì)算出滑動(dòng)停止時(shí) Cell 的位置,并預(yù)先繪制那個(gè)位置附近的幾個(gè) Cell,而忽略當(dāng)前滑動(dòng)中的 Cell。這個(gè)方法比較有技巧性,并且對(duì)于滑動(dòng)性能來(lái)說(shuō)提升也很大,唯一的缺點(diǎn)就是快速滑動(dòng)中會(huì)出現(xiàn)大量空白內(nèi)容。如果你不想實(shí)現(xiàn)比較麻煩的異步繪制但又想保證滑動(dòng)的流暢性,這個(gè)技巧是個(gè)不錯(cuò)的選擇。

全局并發(fā)控制

當(dāng)我用 concurrent queue 來(lái)執(zhí)行大量繪制任務(wù)時(shí),偶爾會(huì)遇到這種問(wèn)題:

大量的任務(wù)提交到后臺(tái)隊(duì)列時(shí),某些任務(wù)會(huì)因?yàn)槟承┰颍ù颂幨?CGFont 鎖)被鎖住導(dǎo)致線程休眠,或者被阻塞,concurrent queue 隨后會(huì)創(chuàng)建新的線程來(lái)執(zhí)行其他任務(wù)。當(dāng)這種情況變多時(shí),或者 App 中使用了大量 concurrent queue 來(lái)執(zhí)行較多任務(wù)時(shí),App 在同一時(shí)刻就會(huì)存在幾十個(gè)線程同時(shí)運(yùn)行、創(chuàng)建、銷毀。CPU 是用時(shí)間片輪轉(zhuǎn)來(lái)實(shí)現(xiàn)線程并發(fā)的,盡管 concurrent queue 能控制線程的優(yōu)先級(jí),但當(dāng)大量線程同時(shí)創(chuàng)建運(yùn)行銷毀時(shí),這些操作仍然會(huì)擠占掉主線程的 CPU 資源。ASDK 有個(gè) Feed 列表的 Demo:SocialAppLayout,當(dāng)列表內(nèi) Cell 過(guò)多,并且非??焖俚幕瑒?dòng)時(shí),界面仍然會(huì)出現(xiàn)少量卡頓,我謹(jǐn)慎的猜測(cè)可能與這個(gè)問(wèn)題有關(guān)。

使用 concurrent queue 時(shí)不可避免會(huì)遇到這種問(wèn)題,但使用 serial queue 又不能充分利用多核 CPU 的資源。我寫了一個(gè)簡(jiǎn)單的工具?YYDispatchQueuePool,為不同優(yōu)先級(jí)創(chuàng)建和 CPU 數(shù)量相同的 serial queue,每次從 pool 中獲取 queue 時(shí),會(huì)輪詢返回其中一個(gè) queue。我把 App 內(nèi)所有異步操作,包括圖像解碼、對(duì)象釋放、異步繪制等,都按優(yōu)先級(jí)不同放入了全局的 serial queue 中執(zhí)行,這樣盡量避免了過(guò)多線程導(dǎo)致的性能問(wèn)題。

更高效的異步圖片加載

SDWebImage 在這個(gè) Demo 里仍然會(huì)產(chǎn)生少量性能問(wèn)題,并且有些地方不能滿足我的需求,所以我自己實(shí)現(xiàn)了一個(gè)性能更高的圖片加載庫(kù)。在顯示簡(jiǎn)單的單張圖片時(shí),利用 UIView.layer.contents 就足夠了,沒(méi)必要使用 UIImageView 帶來(lái)額外的資源消耗,為此我在 CALayer 上添加了 setImageWithURL 等方法。除此之外,我還把圖片解碼等操作通過(guò) YYDispatchQueuePool 進(jìn)行管理,控制了 App 總線程數(shù)量。

其他可以改進(jìn)的地方

上面這些優(yōu)化做完后,微博 Demo 已經(jīng)非常流暢了,但在我的設(shè)想中,仍然有一些進(jìn)一步優(yōu)化的技巧,但限于時(shí)間和精力我并沒(méi)有實(shí)現(xiàn),下面簡(jiǎn)單列一下:

列表中有不少視覺(jué)元素并不需要觸摸事件,這些元素可以用 ASDK 的圖層合成技術(shù)預(yù)先繪制為一張圖。

再進(jìn)一步減少每個(gè) Cell 內(nèi)圖層的數(shù)量,用 CALayer 替換掉 UIView。

目前每個(gè) Cell 的類型都是相同的,但顯示的內(nèi)容卻各部一樣,比如有的 Cell 有圖片,有的 Cell 里是卡片。把 Cell 按類型劃分,進(jìn)一步減少 Cell 內(nèi)不必要的視圖對(duì)象和操作,應(yīng)該能有一些效果。

把需要放到主線程執(zhí)行的任務(wù)劃分為足夠小的塊,并通過(guò) Runloop 來(lái)進(jìn)行調(diào)度,在每個(gè) Loop 里判斷下一次 VSync 的時(shí)間,并在下次 VSync 到來(lái)前,把當(dāng)前未執(zhí)行完的任務(wù)延遲到下一個(gè)機(jī)會(huì)去。這個(gè)只是我的一個(gè)設(shè)想,并不一定能實(shí)現(xiàn)或起作用。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)