在應(yīng)用的UI開(kāi)發(fā)中,使用列表是一種常規(guī)場(chǎng)景,因此,對(duì)列表性能進(jìn)行優(yōu)化是非常重要的。本文將針對(duì)應(yīng)用開(kāi)發(fā)列表場(chǎng)景的性能提升實(shí)踐方法展開(kāi)介紹。
簡(jiǎn)介
本文會(huì)介紹開(kāi)發(fā)列表場(chǎng)景時(shí)的4種推薦優(yōu)化方法,通過(guò)獨(dú)立使用或組合使用這些優(yōu)化方法,可以獲得在啟動(dòng)時(shí)間、內(nèi)存和系統(tǒng)資源方面的平衡,提升性能和用戶體驗(yàn)。
懶加載:提供列表數(shù)據(jù)按需加載能力,解決一次性加載長(zhǎng)列表數(shù)據(jù)耗時(shí)長(zhǎng)、占用過(guò)多資源的問(wèn)題,可以提升頁(yè)面響應(yīng)速度。
緩存列表項(xiàng):提供屏幕可視區(qū)域外列表項(xiàng)長(zhǎng)度的自定義調(diào)節(jié)能力,配合懶加載設(shè)置可緩存列表項(xiàng)參數(shù),通過(guò)預(yù)加載數(shù)據(jù)提升列表滑動(dòng)體驗(yàn)。
組件復(fù)用:提供可復(fù)用組件對(duì)象的緩存資源池,通過(guò)重復(fù)利用已經(jīng)創(chuàng)建過(guò)并緩存的組件對(duì)象,降低組件短時(shí)間內(nèi)頻繁創(chuàng)建和銷(xiāo)毀的開(kāi)銷(xiāo),提升組件渲染效率。
布局優(yōu)化:使用扁平化布局方案,減少視圖嵌套層級(jí)和組件數(shù),避免過(guò)度繪制,提升頁(yè)面渲染效率。
懶加載
原理機(jī)制
應(yīng)用框架為容器類組件的數(shù)據(jù)加載和渲染提供了2種方式:
方式1,提供ForEach實(shí)現(xiàn)一次性加載全量數(shù)據(jù)并循環(huán)渲染。
ForEach(
arr: any[], // 需要進(jìn)行數(shù)據(jù)迭代的列表數(shù)組
itemGenerator: (item: any, index?: number) => void, // 子組件生成函數(shù)
keyGenerator?: (item: any, index?: number) => string // (可選)鍵值生成函數(shù)
)
方式2,提供LazyForEach實(shí)現(xiàn)延遲加載數(shù)據(jù)并按需渲染。
LazyForEach(
dataSource: IDataSource, // 需要進(jìn)行數(shù)據(jù)迭代的數(shù)據(jù)源
itemGenerator: (item: any) => void, // 子組件生成函數(shù)
keyGenerator?: (item: any) => string // (可選) 鍵值生成函數(shù)
)
ForEach循環(huán)渲染的過(guò)程如下:
從列表數(shù)據(jù)源一次性加載全量數(shù)據(jù)。
為列表數(shù)據(jù)的每一個(gè)元素都創(chuàng)建對(duì)應(yīng)的組件,并全部掛載在組件樹(shù)上。即,F(xiàn)orEach遍歷多少個(gè)列表元素,就創(chuàng)建多少個(gè)ListItem組件節(jié)點(diǎn)并依次掛載在List組件樹(shù)根節(jié)點(diǎn)上。
列表內(nèi)容顯示時(shí),只渲染屏幕可視區(qū)內(nèi)的ListItem組件??梢晠^(qū)外的ListItem組件滑動(dòng)進(jìn)入屏幕內(nèi)時(shí),因?yàn)橐呀?jīng)完成數(shù)據(jù)加載和組件創(chuàng)建掛載,直接渲染即可。

ForEach循環(huán)渲染在列表數(shù)據(jù)量大、組件結(jié)構(gòu)復(fù)雜的情況下,會(huì)出現(xiàn)性能瓶頸。因?yàn)橐淮涡约虞d所有的列表數(shù)據(jù),創(chuàng)建所有組件節(jié)點(diǎn)并完成組件樹(shù)的構(gòu)建,在數(shù)據(jù)量大時(shí)會(huì)非常耗時(shí),從而導(dǎo)致頁(yè)面啟動(dòng)時(shí)間過(guò)長(zhǎng)。另外,屏幕可視區(qū)外的組件雖然不會(huì)顯示在屏幕上,但是仍然會(huì)占用內(nèi)存。在系統(tǒng)處于高負(fù)載的情況下,更容易出現(xiàn)性能問(wèn)題,極限情況下甚至?xí)?dǎo)致應(yīng)用異常退出。
為了規(guī)避上述可能出現(xiàn)的問(wèn)題,應(yīng)用框架進(jìn)一步提供了懶加載方式 。
LazyForEach懶加載的原理如下:
LazyForEach會(huì)根據(jù)屏幕可視區(qū)能夠容納顯示的組件數(shù)量按需加載數(shù)據(jù)。
并根據(jù)加載的數(shù)據(jù)量創(chuàng)建組件,掛載在組件樹(shù)上,構(gòu)建出一棵短小的組件樹(shù)。即,屏幕可以展示多少列表項(xiàng)組件,就按需創(chuàng)建多少個(gè)ListItem組件節(jié)點(diǎn)掛載在List組件樹(shù)根節(jié)點(diǎn)上。
屏幕可視區(qū)只展示部分組件。當(dāng)可視區(qū)外的組件需要在屏幕內(nèi)顯示時(shí),需要從頭完成數(shù)據(jù)加載、組件創(chuàng)建、掛載組件樹(shù)這一過(guò)程,直至渲染到屏幕上。

LazyForEach實(shí)現(xiàn)了按需加載,針對(duì)列表數(shù)據(jù)量大、列表組件復(fù)雜的場(chǎng)景,減少了頁(yè)面首次啟動(dòng)時(shí)一次性加載數(shù)據(jù)的時(shí)間消耗,減少了內(nèi)存峰值??梢燥@著提升頁(yè)面的能效比和用戶體驗(yàn)。
使用場(chǎng)景和限制
如果列表數(shù)據(jù)較長(zhǎng),一次性加載所有的列表數(shù)據(jù)創(chuàng)建、渲染頁(yè)面產(chǎn)生性能瓶頸時(shí),開(kāi)發(fā)者應(yīng)該考慮使用數(shù)據(jù)LazyForEach懶加載。
如果列表數(shù)據(jù)較少,數(shù)據(jù)一次性全量加載不是性能瓶頸時(shí),可以直接使用ForEach。
限制:ForEach、LazyForEach必須在List、Grid以及Swiper等容器組件內(nèi)使用,用于循環(huán)渲染具有相同布局的子組件。更多懶加載的信息,請(qǐng)參考官方資料LazyForEach:數(shù)據(jù)懶加載。
LazyForEach懶加載API提供了cachedCount屬性,用于配置可緩存列表項(xiàng)數(shù)量。除默認(rèn)加載界面可視部分外,還可以加載屏幕可視區(qū)外指定數(shù)量(cachedCount)的緩存數(shù)據(jù),詳見(jiàn)下面“緩存列表項(xiàng)”章節(jié)。
實(shí)現(xiàn)示例
在List、Grid等容器組件下使用LazyForEach懶加載的示意代碼如下:
// LazyForEach要遍歷的數(shù)據(jù)源,為實(shí)現(xiàn)接口IDataSource的實(shí)例
private dataList = ...
build() {
Column() {
List() {
LazyForEach(this.dataList, // 數(shù)據(jù)源
(item: ListItemData) => { // 根據(jù)列表項(xiàng)數(shù)據(jù)生成對(duì)應(yīng)的組件
ListItem() {
this.initItem(item)
}
},(item: ListItemData) => item.itemId) // 生成列表項(xiàng)鍵值
}
}
}
接下來(lái)將結(jié)合示例代碼,詳細(xì)介紹LazyForEach懶加載的實(shí)現(xiàn)過(guò)程,包含下圖所示的三部分內(nèi)容:
1、準(zhǔn)備數(shù)據(jù)源類
2、遍歷數(shù)據(jù)源創(chuàng)建列表組件項(xiàng)
3、為列表項(xiàng)指定唯一的鍵值編碼

代碼實(shí)現(xiàn)如下。首先,在使用LazyForEach數(shù)據(jù)懶加載之前,需要實(shí)現(xiàn)懶加載數(shù)據(jù)源接口類IDataSource。數(shù)據(jù)源接口類提供了獲取數(shù)據(jù)總量,返回指定索引位置的數(shù)據(jù),以及注冊(cè)、注銷(xiāo)數(shù)據(jù)監(jiān)聽(tīng)器的接口。編寫(xiě)一個(gè)實(shí)現(xiàn)數(shù)據(jù)源接口IDataSource的數(shù)據(jù)源類BasicDataSource,該類包含數(shù)據(jù)變更監(jiān)聽(tīng)器DataChangeListener類型的實(shí)例變量listeners,用于維護(hù)注冊(cè)的數(shù)據(jù)變更監(jiān)聽(tīng)器,在數(shù)據(jù)變更時(shí)調(diào)用相應(yīng)的回調(diào)函數(shù)。每一個(gè)listener實(shí)例對(duì)應(yīng)一個(gè)ArkUI框架側(cè)的LazyForEach實(shí)例,數(shù)據(jù)源數(shù)據(jù)發(fā)生變更時(shí),listener實(shí)例會(huì)通知LazyForEach需要觸發(fā)界面刷新。
BasicDataSource是一個(gè)抽象類,不同的具體列表頁(yè)面的數(shù)據(jù)源需要根據(jù)業(yè)務(wù)場(chǎng)景分別實(shí)現(xiàn)該抽象類。以聊天列表場(chǎng)景為例,數(shù)據(jù)源具體類ChatListData實(shí)現(xiàn)如下。其中,列表項(xiàng)數(shù)組變量chatList: Array用于為L(zhǎng)ist子組件提供數(shù)據(jù)。ChatModel類表示聊天列表中列表項(xiàng),包含聯(lián)系人信息、最后一條消息內(nèi)容、時(shí)間戳、未讀消息數(shù)量等信息;totalCount()和getData(index: number)是實(shí)現(xiàn)數(shù)據(jù)源接口類IDataSource中定義的方法,用于給LazyForEach提供數(shù)據(jù),應(yīng)用框架會(huì)調(diào)用這些方法;addData()和pushData()方法為數(shù)據(jù)源類中定義的方法,可用于給數(shù)據(jù)源增加數(shù)據(jù)。需要注意的是,在這2個(gè)方法中需要調(diào)用notifyDataAdd方法,用于調(diào)用DataChangeListener中的接口來(lái)觸發(fā)LazyForEach刷新。
class ChatListData extends BasicDataSource {
/**
* 聊天列表項(xiàng)數(shù)組
*/
private chatList: Array<ChatModel> = []
/**
* 數(shù)據(jù)源的數(shù)據(jù)總量
*/
public totalCount(): number {
return this.chatList.length
}
/**
* 返回指定索引位置的數(shù)據(jù)
*/
public getData(index: number): ChatModel {
return this.chatList[index]
}
/**
* 指定位置添加一條聊天列表數(shù)據(jù)
*/
public addData(index: number, data: ChatModel): void {
this.chatList.splice(index, 0, data)
this.notifyDataAdd(index)
}
/**
* 添加一條聊天列表數(shù)據(jù)
*/
public pushData(data: ChatModel): void {
this.chatList.push(data)
this.notifyDataAdd(this.chatList.length - 1)
}
}
接下來(lái),需要?jiǎng)?chuàng)建示例數(shù)據(jù)。在自定義組件ChatListDisplayView中,創(chuàng)建一個(gè)ChatListData類型的局部變量chatList_Lazy,并在aboutToAppear()方法中創(chuàng)建示例數(shù)據(jù)。
@Component
export struct ChatListDisplayView {
private chatList_Lazy: ChatListData = new ChatListData()
......
async aboutToAppear(): Promise<void> {
await makeDataLocal(this.chatList_Lazy)
......
}
}
最后,在List組件容器中,使用LazyForEach接口遍歷數(shù)據(jù)源this.chatList_Lazy循環(huán)生成ListItem列表項(xiàng)。其中,chatViewBuilder()方法用于布局頁(yè)面列表項(xiàng);代碼行(msg: ChatModel) => msg.user.userId使用用戶的編碼作為列表項(xiàng)唯一的鍵值編碼,用于區(qū)分不同的列表項(xiàng)。至此,使用懶加載代碼實(shí)現(xiàn)完成。
build() {
Column() {
List() {
......
LazyForEach(this.chatList_Lazy, (msg: ChatModel) => {
ListItem() {
......
this.chatViewBuilder(msg)
......
}
}, (msg: ChatModel) => msg.user.userId)
......
}
}
}
效果對(duì)比
在聊天示例程序中,通過(guò)模擬10000條聊天數(shù)據(jù),來(lái)對(duì)比測(cè)試在開(kāi)啟、關(guān)閉懶加載時(shí)的性能。測(cè)試項(xiàng)包含頁(yè)面啟動(dòng)完成時(shí)間和列表滑動(dòng)時(shí)幀率。
使用ForEach一次性加載時(shí),頁(yè)面啟動(dòng)完成時(shí)間為3530ms;開(kāi)懶加載時(shí),頁(yè)面啟動(dòng)完成時(shí)間為752ms。開(kāi)啟懶加載后,啟動(dòng)完成時(shí)間縮短為開(kāi)啟前的21.3%。
使用ForEach一次性加載時(shí),丟幀率為26.64%;開(kāi)懶加載時(shí),丟幀率降低到2.33%。

緩存列表項(xiàng)
原理機(jī)制
雖然需要盡量避免一次性加載全部列表數(shù)據(jù)項(xiàng),但合理的預(yù)先緩存當(dāng)前屏幕上下幾頁(yè)的列表項(xiàng)內(nèi)容會(huì)給用戶帶來(lái)更好的體驗(yàn),例如通過(guò)緩存避免“滑動(dòng)白塊”現(xiàn)象。
LazyForEach懶加載可以通過(guò)設(shè)置cachedCount屬性來(lái)指定緩存數(shù)量。在設(shè)置cachedCount后,除屏幕內(nèi)顯示的ListItem組件外,還會(huì)預(yù)先將屏幕可視區(qū)外指定數(shù)量的列表項(xiàng)數(shù)據(jù)緩存起來(lái)。
詳細(xì)過(guò)程如下:
當(dāng)列表滑動(dòng),緩存列表項(xiàng)需要從屏幕可視區(qū)外進(jìn)入可視區(qū)內(nèi)時(shí),只用創(chuàng)建、渲染組件即可,相比不設(shè)置cachedCount提升了顯示效率。
當(dāng)列表不斷滑動(dòng),屏幕可視區(qū)外緩存的列表項(xiàng)數(shù)量少于cachedCount設(shè)置數(shù)量時(shí),會(huì)觸發(fā)列表項(xiàng)數(shù)據(jù)加載事件,繼續(xù)預(yù)加載緩存列表項(xiàng)。比如,如果cachedCount設(shè)置為10,滑動(dòng)到第10項(xiàng)數(shù)據(jù)展示在屏幕上時(shí),會(huì)請(qǐng)求把第11~20列表項(xiàng)數(shù)據(jù)加載緩存起來(lái)。
當(dāng)上滑下滑間隔進(jìn)行時(shí),列表數(shù)據(jù)兩個(gè)方向的數(shù)據(jù)都會(huì)緩存起來(lái)。
如果不顯式設(shè)置cachedCount,默認(rèn)緩存1條數(shù)據(jù)。

使用場(chǎng)景和限制
緩存列表適合加載列表項(xiàng)數(shù)據(jù)比較耗時(shí)的場(chǎng)景。比如,需要從網(wǎng)絡(luò)上獲取視頻數(shù)據(jù)、圖片并通過(guò)ListItem展示。通過(guò)預(yù)先加載并緩存,縮短渲染前的準(zhǔn)備時(shí)間,提升列表響應(yīng)速度。
使用限制為:緩存列表項(xiàng)僅在使用LazyForEach懶加載時(shí)有效,F(xiàn)orEach循環(huán)渲染會(huì)一次性加載全量數(shù)據(jù),不需要設(shè)置緩存列表項(xiàng)。
實(shí)現(xiàn)示例
List/Grid容器組件的cachedCount屬性用于為L(zhǎng)azyForEach懶加載設(shè)置列表項(xiàng)ListItem的最少緩存數(shù)量。應(yīng)用可以通過(guò)增加cachedCount參數(shù),調(diào)整屏幕外預(yù)加載項(xiàng)的數(shù)量。提供一個(gè)開(kāi)關(guān)用于設(shè)置是否使能該屬性,如下所示。在設(shè)置cachedCount后,當(dāng)列表界面滑動(dòng)時(shí),除了獲取屏幕上展示的數(shù)據(jù),還會(huì)額外獲取指定數(shù)量的列表項(xiàng)數(shù)據(jù)緩存起來(lái)。
build() {
Column() {
List() {
...
...
LazyForEach(this.chatListData, (msg: ChatModel) => {
ListItem() {
ChatView({ chatItem: msg })
}
}, (msg: ChatModel) => msg.user.userId)
}
.backgroundColor(Color.White)
.listDirection(Axis.Vertical)
...
...
.cachedCount(this.list_cachedCount ? Constants.CACHED_COUNT : 0) // 緩存列表數(shù)量
}
}
效果對(duì)比
在示例程序中,屏幕上每頁(yè)展示9條數(shù)據(jù)?;谑纠绦?,測(cè)試了不同緩存數(shù)量對(duì)幀率的影響情況,不設(shè)置緩存數(shù)量時(shí),丟幀率為7.79%,當(dāng)逐漸增加緩存數(shù)量時(shí),丟幀率降低。當(dāng)設(shè)置當(dāng)前屏幕展示數(shù)量的一半,即緩存5個(gè)列表項(xiàng)時(shí),丟幀率最低。再增加緩存數(shù)量,丟幀率不再有顯著的下降,增加緩存數(shù)量太多時(shí),甚至?xí)绊憗G幀率。測(cè)試數(shù)據(jù)僅限于示例程序,不同的應(yīng)用程序設(shè)置的最佳緩存數(shù)量不一致,需要針對(duì)應(yīng)用程序測(cè)試得出最佳緩存數(shù)量。

應(yīng)該如何根據(jù)實(shí)際場(chǎng)景,設(shè)置緩存數(shù)量的值呢? 例如列表項(xiàng)中需要顯示網(wǎng)絡(luò)數(shù)據(jù),而網(wǎng)絡(luò)數(shù)據(jù)加載較慢,為了提升列表信息的瀏覽效率和瀏覽體驗(yàn),可以適當(dāng)?shù)亩嘣O(shè)置一些緩存數(shù)量;如果列表中需要加載一些大圖或者視頻等,這些數(shù)據(jù)占用的內(nèi)存較大,為了減少內(nèi)存占用,需要適當(dāng)減少緩存數(shù)量的設(shè)置;因此,在實(shí)際場(chǎng)景中,需要不斷嘗試驗(yàn)證,設(shè)置適當(dāng)?shù)木彺鏀?shù)量,來(lái)達(dá)到體驗(yàn)和內(nèi)存的平衡。
組件復(fù)用
原理機(jī)制
應(yīng)用框架提供了組件復(fù)用能力,可復(fù)用組件從組件樹(shù)上移除時(shí),會(huì)進(jìn)入到一個(gè)回收緩存區(qū)。后續(xù)創(chuàng)建新組件節(jié)點(diǎn)時(shí),會(huì)復(fù)用緩存區(qū)中的節(jié)點(diǎn),節(jié)約組件重新創(chuàng)建的時(shí)間。尤其在列表等場(chǎng)景下,其自定義子組件具有相同的組件布局結(jié)構(gòu),列表更新時(shí)僅有狀態(tài)變量等數(shù)據(jù)差異。通過(guò)組件復(fù)用可以提高列表頁(yè)面的加載速度和響應(yīng)速度。
組件復(fù)用機(jī)制如下:
標(biāo)記為@Reusable的組件從組件樹(shù)上被移除時(shí),組件和其對(duì)應(yīng)的JSView對(duì)象都會(huì)被放入復(fù)用緩存中。
當(dāng)列表滑動(dòng)新的ListItem將要被顯示,List組件樹(shù)上需要新建節(jié)點(diǎn)時(shí),將會(huì)從復(fù)用緩存中查找可復(fù)用的組件節(jié)點(diǎn)。
找到可復(fù)用節(jié)點(diǎn)并對(duì)其進(jìn)行更新后添加到組件樹(shù)中。從而節(jié)省了組件節(jié)點(diǎn)和JSView對(duì)象的創(chuàng)建時(shí)間。

@Reusable組件復(fù)用結(jié)合LazyForEach懶加載,可以進(jìn)一步解決列表滑動(dòng)場(chǎng)景的瓶頸問(wèn)題,提供滑動(dòng)場(chǎng)景下高性能創(chuàng)建組件的方式來(lái)提升滑動(dòng)幀率。
使用場(chǎng)景和限制
若業(yè)務(wù)實(shí)現(xiàn)中存在以下場(chǎng)景,并成為UI線程的幀率瓶頸,推薦使用組件復(fù)用:
一幀內(nèi)重復(fù)創(chuàng)建多個(gè)已經(jīng)被銷(xiāo)毀的自定義組件。
反復(fù)切換條件渲染的控制分支,且控制分支中的組件子樹(shù)結(jié)構(gòu)較重。
組件復(fù)用生效的條件是:
自定義組件被@Reusable裝飾器修飾,即標(biāo)志其具備組件復(fù)用的能力;
在一個(gè)自定義父組件下創(chuàng)建出來(lái)的具備組件復(fù)用能力的自定義子組件,在可復(fù)用自定義組件從組件樹(shù)上移除之后,會(huì)被加入到其父自定義組件的可復(fù)用節(jié)點(diǎn)緩存中;
在一個(gè)自定義父組件下創(chuàng)建可復(fù)用的子組件時(shí),若可復(fù)用子節(jié)點(diǎn)緩存中有對(duì)應(yīng)類型的可復(fù)用子組件,會(huì)通過(guò)更新可復(fù)用子組件的方式,快速創(chuàng)建可復(fù)用子組件;
ForEach循環(huán)渲染會(huì)一次性加載全量數(shù)據(jù),因此不支持組件復(fù)用。
使用規(guī)則如下:
@Reusable標(biāo)識(shí)自定義組件具備可復(fù)用的能力,它可以被添加到任意的自定義組件上,但是開(kāi)發(fā)者需要小心處理自定義組件的創(chuàng)建流程和更新流程以確保自定義組件在復(fù)用之后能展示出正確的行為;
可復(fù)用自定義組件的緩存和復(fù)用只能發(fā)生在同一父組件下,無(wú)法在不同的父組件下復(fù)用同一自定義節(jié)點(diǎn)的實(shí)例。e.g. A組件是可復(fù)用組件,其也是B組件的子組件,并進(jìn)入了B組件的可復(fù)用節(jié)點(diǎn)緩存中,但是在C組件中創(chuàng)建A組件時(shí),無(wú)法使用B組件緩存的A組件;
自定義組件的復(fù)用帶來(lái)的性能提升主要體現(xiàn)在節(jié)省了自定義組件的JS對(duì)象的創(chuàng)建時(shí)間并復(fù)用了自定義組件的組件樹(shù)結(jié)構(gòu),若應(yīng)用開(kāi)發(fā)者在自定義組件復(fù)用的前后使用渲染控制語(yǔ)法顯著的改變了自定義組件的組件樹(shù)結(jié)構(gòu),那么將無(wú)法享受到組件復(fù)用帶來(lái)的性能提升;
組件復(fù)用僅發(fā)生在存在可復(fù)用組件從組件樹(shù)上移除并再次加入到組件樹(shù)的場(chǎng)景中,若不存在上述場(chǎng)景,將無(wú)法觸發(fā)組件復(fù)用。e.g. 使用ForEach渲染控制語(yǔ)法創(chuàng)建可復(fù)用的自定義組件,由于ForEach渲染控制語(yǔ)法的全展開(kāi)屬性,不能觸發(fā)組件復(fù)用。
使用建議如下:
建議復(fù)用自定義組件時(shí)避免一切可能改變自定義組件的組件樹(shù)結(jié)構(gòu)和可能使可復(fù)用組件中產(chǎn)生重新布局的操作以將組件復(fù)用的性能提升到最高;
建議列表滑動(dòng)場(chǎng)景下組件復(fù)用能力和LazyForEach渲染控制語(yǔ)法搭配使用以達(dá)到性能最優(yōu)效果;
開(kāi)發(fā)者需要區(qū)分好自定義組件的創(chuàng)建和更新過(guò)程中的行為,并注意到自定義組件的復(fù)用本質(zhì)上是一種特殊的組件更新行為,組件創(chuàng)建過(guò)程中的流程與生命周期將不會(huì)在組件復(fù)用中發(fā)生,自定義組件的構(gòu)造參數(shù)將通過(guò)aboutToReuse生命周期回調(diào)傳遞給自定義組件。e.g. aboutToAppear生命周期和自定義組件的初始化傳參將不會(huì)在組件復(fù)用中發(fā)生;
避免在aboutToReuse生命周期回調(diào)中產(chǎn)生耗時(shí)操作,最佳實(shí)踐是僅在aboutToReuse中做自定義組件更新所需的狀態(tài)變量值的更新。
實(shí)現(xiàn)示例
在開(kāi)發(fā)應(yīng)用時(shí),自定義組件被@Reusable裝飾器修飾,表示該自定義組件可以復(fù)用。在自定義父組件下創(chuàng)建的可復(fù)用組件從組件樹(shù)上移除后,會(huì)被加入父組件的可復(fù)用節(jié)點(diǎn)緩存里。在父組件再次創(chuàng)建可復(fù)用組件時(shí),會(huì)通過(guò)更新可復(fù)用組件的方式,從緩存快速創(chuàng)建可復(fù)用組件。使用裝飾器@Reusable標(biāo)記一個(gè)組件屬于可復(fù)用組件后,還需要實(shí)現(xiàn)自定義組件的生命周期回調(diào)函數(shù)aboutToReuse(),其參數(shù)為可復(fù)用組件的狀態(tài)變量。調(diào)用可復(fù)用自定義組件時(shí),父組件會(huì)給子組件傳遞構(gòu)造數(shù)據(jù)。
示例代碼如下所示:
/**
* 可復(fù)用且優(yōu)化布局的聊天頁(yè)面組件
*/
@Reusable
@Component
struct ReusableOptLayoutChatView {
@State chatItem: ChatModel = new ChatModel(new ChatContact('', ''), '', '', 0);
aboutToReuse(params: Record<string, Object>): void {
this.chatItem = params.chatItem as ChatModel;
Logger.info(TAG, 'aboutToReuse=' + this.chatItem.toString());
}
build() {
OptLayoutChatView({ chatItem: this.chatItem });
}
}
效果對(duì)比
在示例程序中,對(duì)列表項(xiàng)中的組件進(jìn)行復(fù)用。經(jīng)測(cè)試發(fā)現(xiàn),因本示例復(fù)用組件的布局較簡(jiǎn)單,組件復(fù)用對(duì)本測(cè)試場(chǎng)景沒(méi)有明顯的性能提升效果。在實(shí)際場(chǎng)景中,應(yīng)該如何用好組件復(fù)用這個(gè)特性呢?在列表項(xiàng)的布局復(fù)雜度更高時(shí),組件復(fù)用的效果更好。因?yàn)楦邚?fù)雜度的組件布局,初始化時(shí)需要消耗更多的系統(tǒng)資源,因此在使用較高復(fù)雜的列表布局時(shí),建議使用組件復(fù)用這個(gè)特性。
布局優(yōu)化
常用布局類型
當(dāng)前ArkUI應(yīng)用框架提供了以下兩類常用的布局方式:
線性布局: 例如Stack、Column、Row和Flex等,會(huì)把布局中的組件按照線性方向進(jìn)行排布,如橫向、縱向、Z軸方向等;這種布局使用簡(jiǎn)單方便、易于理解,但是在復(fù)雜的場(chǎng)景下往往會(huì)使用更多的組件數(shù)和較深的嵌套層次,維護(hù)困難,同時(shí)也增加了系統(tǒng)的開(kāi)銷(xiāo);
高級(jí)布局: 往往可以使用更少的節(jié)點(diǎn)數(shù)和布局層級(jí),實(shí)現(xiàn)更加復(fù)雜的布局效果,具有扁平化的特性;包括List、Grid、RelativeContainer等,在列表、宮格和混排布局等場(chǎng)景提供了扁平化的布局方式,例如RelativeContainer可以根據(jù)錨點(diǎn)來(lái)進(jìn)行低嵌套層級(jí)復(fù)雜布局,而List和Grid又支持懶加載等提升性能的方法,同時(shí)降低了維護(hù)成本;因此,高級(jí)布局是更加推薦的布局方法。
使用場(chǎng)景和問(wèn)題
在開(kāi)發(fā)頁(yè)面時(shí),我們往往會(huì)習(xí)慣使用線性布局來(lái)實(shí)現(xiàn)頁(yè)面構(gòu)造,這種布局方法可能會(huì)導(dǎo)致組件樹(shù)和嵌套層數(shù)過(guò)多的問(wèn)題,在創(chuàng)建和布局階段產(chǎn)生較大的性能開(kāi)銷(xiāo),如下列示例場(chǎng)景:

布局中存在冗余布局,如build()函數(shù)下第一層的Column布局;例如GridContainer下的嵌套結(jié)構(gòu),使用了多個(gè)線性布局Column嵌套,層級(jí)較深。 還有下面的場(chǎng)景示例中也存在頻繁使用線性布局導(dǎo)致嵌套過(guò)深的情況:

構(gòu)建了10、20、30、40、50層的嵌套組件作為列表項(xiàng),在列表中插入100條該嵌套組件,測(cè)試這些嵌套組件在滑動(dòng)場(chǎng)景下對(duì)內(nèi)存的影響,數(shù)據(jù)如下所示:

嵌套組件的示意結(jié)構(gòu)如下所示:

從內(nèi)存數(shù)據(jù)可以得知,嵌套層級(jí)越深,會(huì)有更大的系統(tǒng)內(nèi)存開(kāi)銷(xiāo)。因此在開(kāi)發(fā)過(guò)程中,要盡可能減少布局嵌套,使布局更加扁平化。那么應(yīng)該如何進(jìn)行布局優(yōu)化呢?
布局優(yōu)化思路
對(duì)于這些常見(jiàn)問(wèn)題,將通過(guò)優(yōu)化一個(gè)聊天列表項(xiàng)的頁(yè)面布局,來(lái)展示布局優(yōu)化的方法和思路。使用DevEco Studio集成的ArkUI Inspector等工具可以查看視圖布局,場(chǎng)景預(yù)覽圖和優(yōu)化前的布局結(jié)構(gòu)如下所示:

從場(chǎng)景預(yù)覽圖中可以知道,列表項(xiàng)中包含了圖片、消息數(shù)、昵稱、聊天信息、時(shí)間這5個(gè)部分的內(nèi)容;使用線性布局的寫(xiě)法方式,就是一個(gè)橫向布局Row,嵌套了3個(gè)縱向布局Column,由于紅色消息字體需要和圖片進(jìn)行重疊,還使用了Stack布局進(jìn)行堆疊,最終的布局方式就如上圖所示,共使用組件10個(gè),嵌套4層,源碼如下所示:
build() {
Row() {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Image(this.chatItem.user.userImage) // 用戶頭像
...
if (this.chatItem.unreadMsgCount > 0) { // 紅點(diǎn)消息大于0條時(shí)渲染紅點(diǎn)
Text(`${this.chatItem.unreadMsgCount}`) // 消息數(shù)
...
}
}
}
.layoutWeight(1)
.padding({ right: 12 })
Column() {
Text(this.chatItem.user.userName) // 昵稱
.fontColor(Color.Black)
.fontSize(16)
.margin({ bottom: 3 })
Text(this.chatItem.lastMsg) // 聊天消息
.fontColor("#999999")
.maxLines(1)
.fontSize(14)
.margin({ top: 5 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(3)
Column() {
Text(this.chatItem.lastTime) // 時(shí)間
.width(50)
.fontColor("#999999")
.textAlign(TextAlign.End)
.maxLines(1)
.fontSize(12)
.margin({ right: 6, bottom: 27 })
}
.padding({ left: 15 })
.alignItems(HorizontalAlign.End)
.layoutWeight(1)
}
...
}
其實(shí)這4個(gè)部分是有明確的相對(duì)關(guān)系的,如圖片和消息數(shù)位于列表最左側(cè),時(shí)間位于列表最右側(cè),而昵稱和聊天信息是在圖片的右側(cè),并且上下分布,因此,就可以使用RelativeContainer布局來(lái)進(jìn)行優(yōu)化,優(yōu)化后可以把組件數(shù)減少到5個(gè),嵌套2層,大大減少了系統(tǒng)開(kāi)銷(xiāo),源碼如下所示:
build() {
RelativeContainer() { // 相對(duì)布局
Image(this.chatItem.user.userImage) // 用戶頭像
...
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.syncLoad(this.img_syncLoad ? true : false)
.id("figure")
if (this.chatItem.unreadMsgCount > 0) { // 紅點(diǎn)消息大于0條時(shí)渲染紅點(diǎn)
Text(`${this.chatItem.unreadMsgCount}`) // 消息數(shù)
...
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.opacity(this.chatItem.unreadMsgCount > 0 ? 1 : 0)
.id("badge")
}
Text(this.chatItem.user.userName) // 昵稱
...
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.id("user")
Text(this.chatItem.lastTime) // 時(shí)間
...
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
right: { anchor: '__container__', align: HorizontalAlign.End }
})
.id("time")
Text(this.chatItem.lastMsg) // 聊天信息
...
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start }
})
.id("msg")
}
...
}

| 優(yōu)化情況 | 組件總數(shù) | 視圖嵌套層數(shù) |
|---|---|---|
| 優(yōu)化前,使用線性布局 | 10 | 4 |
| 優(yōu)化后,使用扁平化布局 | 5 | 2 |
從上述案例中可以看到,選用正確的布局組件,不但去除了中間嵌套的組件層級(jí),減少了組件數(shù)量,使代碼更易于維護(hù),也避免系統(tǒng)繪制更多的布局組件,達(dá)到優(yōu)化性能、減少內(nèi)存占用的目的,這就是扁平化布局改造的思路。

系統(tǒng)還提供了更多的扁平化布局方案,例如絕對(duì)定位、自定義布局、Grid、GridRow等,適用更多不同的場(chǎng)景,具體的使用方法可以參考官方文檔。
總結(jié)
本文的聊天列表場(chǎng)景,分析了列表滑動(dòng)性能的優(yōu)化方法,包含懶加載、緩存列表項(xiàng)、組件復(fù)用、頁(yè)面布局優(yōu)化。對(duì)每個(gè)優(yōu)化方法詳細(xì)介紹了原理、使用場(chǎng)景,并基于示例程序給出了優(yōu)化效果和對(duì)比數(shù)據(jù)。在開(kāi)發(fā)類似列表場(chǎng)景時(shí),可以借鑒這些優(yōu)化方法。
寫(xiě)在最后
如果你覺(jué)得這篇內(nèi)容對(duì)你還蠻有幫助,我想邀請(qǐng)你幫我三個(gè)小忙:
- 點(diǎn)贊,轉(zhuǎn)發(fā),有你們的 『點(diǎn)贊和評(píng)論』,才是我創(chuàng)造的動(dòng)力。
- 關(guān)注小編,同時(shí)可以期待后續(xù)文章ing??,不定期分享原創(chuàng)知識(shí)。
- 想要獲取更多完整鴻蒙最新學(xué)習(xí)知識(shí)點(diǎn),請(qǐng)移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu
