RecyclerView 数据预取
更快處理任務(wù),使?jié)L動(dòng)和滑動(dòng)更流暢
在我小時(shí)候,媽媽為了治療我的拖延癥,總是告訴我:“如果你現(xiàn)在打掃你的房間,就不用以后再打掃了。”但我從沒(méi)這樣做。我知道最好能拖延就盡量拖延。一個(gè)原因是:如果我現(xiàn)在打掃了,房間還會(huì)變臟,那時(shí)候我就必須再打掃一遍了。另外,如果我把這件事放下足夠久,媽媽可能會(huì)忘了它的。
拖延對(duì)我來(lái)說(shuō)總是有效。但我永遠(yuǎn)不用處理保持幀率的問(wèn)題,不像我的朋友 RecyclerView 一樣。
問(wèn)題
在一次滾動(dòng)或慣性滑動(dòng)中,RecyclerView 需要在新條目抵達(dá)屏幕時(shí)予以展示。這些新條目需要與數(shù)據(jù)相綁定(如果緩存中沒(méi)有相應(yīng)條目的話,還需要?jiǎng)?chuàng)建一個(gè))。接下來(lái),它們還需要被展開(kāi)并畫(huà)出來(lái)。如果所有這些都是被懶加載的,在需要展示之前才做,UI 線程就會(huì)在工作完成時(shí)陷入停頓。接下來(lái)渲染可以繼續(xù)并且滾動(dòng)(或者說(shuō)滑動(dòng),但我打算用滾動(dòng)來(lái)指代它們,以簡(jiǎn)化討論)可以平滑地繼續(xù),直到下一個(gè)條目進(jìn)入視野范圍。
一次典型的 RecyclerView 內(nèi)容滾動(dòng)中的各個(gè)渲染階段(在?Lollipop?版本時(shí)的情況)。在UI線程,我們處理輸入事件和動(dòng)畫(huà),完成布局,并且記錄繪圖操作。接下來(lái)渲染線程把指令送往GPU。 在一次滾動(dòng)的大多數(shù)幀中,RecyclerView 可以沒(méi)問(wèn)題地完成它需要做的事,因?yàn)椴恍枰幚硇碌膬?nèi)容。在這些幀中,UI 線程處理輸入事件和動(dòng)畫(huà),完成布局,記錄繪圖操作。接下來(lái)它把繪圖信息與渲染線程同步(在 Lollipop 版本時(shí)的情況,之前的版本在 UI 線程完成所有工作),渲染線程把指令送往 GPU。
新條目使得輸入階段耗時(shí)更長(zhǎng),因?yàn)樾碌?view 需要被創(chuàng)建、綁定并布局。這推遲了渲染階段的開(kāi)始,從而導(dǎo)致它可能在幀的邊界之后結(jié)束。在此情況下,就會(huì)發(fā)生掉幀。當(dāng)一個(gè)新的條目來(lái)到屏幕中時(shí),輸入階段就需要完成更多工作,以綁定(可能還要?jiǎng)?chuàng)建)正確的 view。這推遲了 UI 線程其余的工作,以及渲染線程接下來(lái)的工作。如果這些不能在幀邊界內(nèi)完成的話,就會(huì)發(fā)生卡頓。
輸入階段的調(diào)用棧表明:新的條目進(jìn)入視野范圍會(huì)導(dǎo)致一大塊時(shí)間被用于創(chuàng)建和綁定新的 view。 如果我們可以在其它地方完成這些工作,而不推遲所有其它事情,不就很好嗎??
在 view 可以被渲染之前,創(chuàng)建和綁定必須完成。這會(huì)在相應(yīng)的幀中消耗 UI 線程的寶貴時(shí)間。然而,UI 線程在前一幀中有大量時(shí)間無(wú)所事事。?Chris Craik(Android UI Toolkit 組的工程師)在用?Systraces?查看 RecyclerView 滾動(dòng)時(shí)發(fā)現(xiàn)了這一點(diǎn)。他特別注意到,我們?cè)谛枰褂靡粋€(gè)條目時(shí),會(huì)花費(fèi)大量時(shí)間準(zhǔn)備它。而在一幀之前,UI 線程花了大量時(shí)間休眠,因?yàn)樗茉缇屯瓿闪巳蝿?wù)。
解決方案
將創(chuàng)建和綁定工作移到前一幀,使 UI 線程能夠與渲染線程同時(shí)工作,從而避免接下來(lái)在渲染線程繪制結(jié)果之前同步完成這些工作。 顯然,這是優(yōu)化耗時(shí)的好時(shí)機(jī)。Chris 重新安排了默認(rèn) RecyclerView 布局時(shí)事件發(fā)生的順序,它現(xiàn)在在一個(gè)條目即將進(jìn)入視野時(shí)預(yù)取數(shù)據(jù),這樣我們可以在空閑期完成工作,避免拖到大家都在等待結(jié)果時(shí)才完成。 完成這些工作基本上沒(méi)有任何代價(jià),因?yàn)?UI 線程在兩幀之間的空隙不做任何工作。我們可以使用這些空閑時(shí)間來(lái)完成將來(lái)的工作,并使得未來(lái)的幀出現(xiàn)得更快,因?yàn)槔щy的部分已經(jīng)被完成了。
細(xì)節(jié),細(xì)節(jié)
這個(gè)系統(tǒng)的工作方式是,在 RecyclerView 開(kāi)始一個(gè)滾動(dòng)時(shí)安排一個(gè) Runnable。這個(gè) Runnable 負(fù)責(zé)根據(jù) layout manager 和滾動(dòng)的方向預(yù)取即將進(jìn)入視野的條目。預(yù)取不限于一個(gè)單獨(dú)的條目。它可以同時(shí)取出多個(gè)條目,例如在使用 GridLayoutManager 且新的一行馬上要出現(xiàn)的時(shí)候。在 25.1 版本中,預(yù)取操作被分為單獨(dú)的創(chuàng)建/綁定操作,從而比對(duì)整組條目做操作更容易被納入 UI 線程的空隙中。
有趣的是,系統(tǒng)必須預(yù)測(cè)操作需要多少時(shí)間,以及它們是否可以被放入空隙中。畢竟,如果預(yù)取把當(dāng)前幀推遲到截止時(shí)間之后,我們?nèi)匀粫?huì)因掉幀而感覺(jué)到卡頓,只是和不預(yù)取時(shí)原因不同而已。系統(tǒng)處理這些細(xì)節(jié)的方式是追蹤每種 view 類型的平均創(chuàng)建/綁定時(shí)間,從而使未來(lái)創(chuàng)建/綁定時(shí)間的合理預(yù)測(cè)成為可能。
對(duì)嵌套 RecyclerView(每一個(gè)條目自身都是 RecyclerView 的容器)完成這些工作更加復(fù)雜,因?yàn)榻壎▋?nèi)部 RecyclerView 并不涉及任何子控件的分配——RecyclerView 在被綁定和布局時(shí)按需取得子控件。預(yù)取系統(tǒng)仍然可以預(yù)先準(zhǔn)備內(nèi)層的 RecyclerView 內(nèi)部的子控件,但它必須知道有多少。這就是 25.1 版本中 LinearLayoutManager 新 API?setInitialItemPrefetchCount()的意義。它告訴系統(tǒng),在滾動(dòng)時(shí)需要預(yù)取多少條目來(lái)充滿 RecyclerView。
警告
你需要注意這些危險(xiǎn):
-預(yù)取數(shù)據(jù)可能做一些最終不被需要的工作。因?yàn)槲覀冊(cè)陬A(yù)取 view 時(shí),有可能會(huì)采取太激進(jìn)的策略,這樣 RecyclerView 就可能不會(huì)滾動(dòng)到我們預(yù)取的條目。這意味著我們的預(yù)取工作可能會(huì)被浪費(fèi)(雖然這些工作是被并行完成的,應(yīng)該不會(huì)浪費(fèi)太多時(shí)間。另外,浪費(fèi)是不太可能發(fā)生的,因?yàn)槲覀冊(cè)谛枰獢?shù)據(jù)之前不久才去預(yù)取,而且滾動(dòng)不太可能在兩幀之間停止或反轉(zhuǎn))。 -渲染線程:渲染線程是 Lollipop 版本引入的性能特性,它可以讓一個(gè)不同的線程分擔(dān)渲染工作,并且支持其他的一些改進(jìn),例如把不可變的動(dòng)畫(huà)(如漣漪、環(huán)形展現(xiàn)等)完全放在渲染線程,使其不受 UI 線程停頓的影響。這意味著運(yùn)行 Lollipop 之前的版本的設(shè)備將不會(huì)受益于這個(gè)優(yōu)化,因?yàn)槲覀儫o(wú)法并行完成這些工作。
我要一些 —— 去哪兒拿?
預(yù)取優(yōu)化是在?Support Library v25中引入,在?v25.1.0中改進(jìn)的。所以第一步是下載?最新版本的支持庫(kù)。
如果你使用 RecyclerView 提供的默認(rèn) layout manager,你將自動(dòng)獲得這種優(yōu)化。然而,如果你使用嵌套 RecyclerView 或者自己寫(xiě) layout manager,你需要改變你的代碼來(lái)利用這個(gè)特性。
對(duì)于嵌套 RecyclerView 而言,要獲取最佳的性能,在內(nèi)部的 LayoutManager 中調(diào)用 LinearLayoutManager 的setInitialItemPrefetchCount()方法(25.1版本起可用)。例如,如果你豎直方向的list至少展示三個(gè)條目,調(diào)用 setInitialItemPrefetchCount(4)。
如果你實(shí)現(xiàn)了自己的 LayoutManager,你需要重寫(xiě)?LayoutManager.collectAdjacentPrefetchPositions()方法。該方法在數(shù)據(jù)預(yù)取開(kāi)啟時(shí)被 RecyclerView 調(diào)用(LayoutManager 的默認(rèn)實(shí)現(xiàn)什么都不做)。第二,在嵌套的內(nèi)層 RecyclerView 中,如果你想讓你的 LayoutManager 預(yù)取數(shù)據(jù),你同樣應(yīng)當(dāng)實(shí)現(xiàn)?LayoutManager.collectInitialPrefetchPositions()。
和以前一樣,優(yōu)化你的創(chuàng)建和綁定步驟,做盡可能少的工作,是值得的。運(yùn)行的最快的代碼是根本不需要運(yùn)行的代碼;即使框架可以通過(guò)數(shù)據(jù)預(yù)取并行工作,它仍然消耗時(shí)間,而且耗時(shí)較長(zhǎng)的條目創(chuàng)建仍然可以導(dǎo)致卡頓。例如,一棵最小的 view 樹(shù)總比一棵復(fù)雜的更容易創(chuàng)建和綁定。本質(zhì)上,綁定應(yīng)該和調(diào)用 setter 一樣方便,一樣快。即使你用目前的代碼就可以在一幀的時(shí)間限制中完成工作,進(jìn)一步優(yōu)化意味著它將更可能在低端的用戶機(jī)型上運(yùn)行良好。此外,在高端設(shè)備上為這些常用場(chǎng)景節(jié)約性能,總是對(duì)電池有益的。如果你已經(jīng)盡可能縮短了創(chuàng)建和綁定的時(shí)間,預(yù)取將會(huì)幫助你縮短兩幀之間的剩余時(shí)間。
如果你想要見(jiàn)到實(shí)際的優(yōu)化,在默認(rèn)或自定義的 LayoutManager 中,你可以切換?LayoutManager.setItemPrefetchEnabled()并比較結(jié)果。你應(yīng)該能夠從視覺(jué)上直觀地看到差異;它確實(shí)如此顯著,特別是在條目需要大量時(shí)間創(chuàng)建和綁定的情況下。但如果你想知道在表面下發(fā)生過(guò)什么,在預(yù)取打開(kāi)和關(guān)閉時(shí)運(yùn)行Systrace, 或者打開(kāi)?GPU profiling。
Systrace 顯示數(shù)據(jù)預(yù)取在UI線程空閑時(shí)預(yù)取數(shù)據(jù)。
GOTO 結(jié)尾
查看?最新的 Support Library并和能預(yù)取數(shù)據(jù)的 RecyclerView 一起玩耍。同時(shí),我將繼續(xù)不清理我的房間。
原文發(fā)布時(shí)間為:2017年2月14日
本文來(lái)自云棲社區(qū)合作伙伴掘金,了解相關(guān)信息可以關(guān)注掘金網(wǎng)站。
總結(jié)
以上是生活随笔為你收集整理的RecyclerView 数据预取的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 1.4. Open Source and
- 下一篇: View 事件传递体系知识梳理(1)