一、概述
組件復用是優化用戶界面性能,提升應用流暢度的一種重要手段,通過復用已存在的組件節點而非創建新的節點,從而確保UI線程的流暢性與響應速度。
組件復用針對的是自定義組件,只要發生了相同自定義組件銷毀和再創建的場景,都可以使用組件復用,例如滑動列表場景,會出現大量重復布局的創建,使用組件復用可以大幅度降低了因頻繁創建與銷毀組件帶來的性能損耗。
然而,面對復雜的業務場景或者布局嵌套的場景下,組件復用使用不當,可能會導致復用失效或者性能提升不能最大化。例如列表中存在多種布局形態的列表項,無法直接復用。
本文基于對常見的布局類型進行劃分,通過合理使用組件復用方式,幫助開發者更好的理解和實施組件復用策略以優化應用性能。
二、原理介紹
組件復用機制如下:
- 標記為@Reusable的組件從組件樹上被移除時,組件和其對應的JSView對象都會被放入復用緩存中。
- 當列表滑動新的ListItem將要被顯示,List組件樹上需要新建節點時,將會從復用緩存中查找可復用的組件節點。
- 找到可復用節點并對其進行更新后添加到組件樹中。從而節省了組件節點和JSView對象的創建時間。
組件復用原理圖
1、@Reusable表示組件可以被復用,結合LazyForEach懶加載一起使用,可以進一步解決列表滑動場景的瓶頸問題,提供滑動場景下高性能創建組件的方式來提升滑動幀率。
2、CustomNode是一種自定義的虛擬節點,它可以用來緩存列表中的某些內容,以提高性能和減少不必要的渲染。通過使用CustomNode,可以實現只渲染當前可見區域內的數據項,將未顯示的數據項緩存起來,從而減少渲染的數量,提高性能。
3、RecycleManager是一種用于優化資源利用的回收管理器。當一個數據項滾出屏幕時,不會立即銷毀對應的視圖對象,而是將該視圖對象放入復用池中。當新的數據項需要在屏幕上展示時,RecycleManager會從復用池中取出一個已經存在的視圖對象,并將新的數據綁定到該視圖上,從而避免頻繁的創建和銷毀過程。通過使用RecycleManager,可以大大減少創建和銷毀視圖的次數,提高列表的滾動流暢度和性能表現。
4、CachedRecycleNodes是CustomNode的一個集合,常是用于存儲被回收的CustomNode對象,以便在需要時進行復用。
說明
需要注意的是,雖然這里是使用List組件進行舉例,但是不代表組件復用只能用在滾動容器里,只要是發生了相同自定義組件銷毀和再創建的場景,都可以使用組件復用。
三、使用規則
組件復用的示例代碼如下:
// xxx.ets
export class Message {
value: string | undefined;
constructor(value: string) {
this.value = value
}
}
@Entry
@Component
struct Index {
@State switch: boolean = true
build() {
Column() {
Button('Hello World')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.switch = !this.switch
})
if (this.switch) {
Child({ message: new Message('Child') })
// 如果只有一個復用的組件,可以不用設置reuseId
.reuseId('Child')
}
}
.height("100%")
.width('100%')
}
}
@Reusable
@Component
struct Child {
@State message: Message = new Message('AboutToReuse');
aboutToReuse(params: Record<string, ESObject>) {
console.info("Recycle Child")
this.message = params.message as Message
}
build() {
Column() {
Text(this.message.value)
.fontSize(20)
}
.borderWidth(2)
.height(100)
}
}
1.@Reusable:自定義組件被@Reusable裝飾器修飾,即表示其具備組件復用的能力。
2.aboutToReuse:當一個可復用的自定義組件從復用緩存中重新加入到節點樹時,觸發aboutToReuse生命周期回調,并將組件的構造參數傳遞給aboutToReuse。
3.reuseId:用于標記自定義組件復用組,當組件回收復用時,復用框架將根據組件的reuseId來劃分組件的復用組。如果只有一個復用的組件,可以不用設置reuseId。
四、復用類型詳解
組件復用基于不同的布局效果和復用的訴求,可以分為以下五種類型。
表1 組件復用類型說明
復用類型 | 描述 | 復用思路 |
---|---|---|
標準型 | 復用組件之間布局完全相同 | 標準復用 |
有限變化型 | 復用組件之間布局有所不同,但是類型有限 | 使用reuseId或者獨立成不同自定義組件 |
組合型 | 復用組件之間布局有不同,情況非常多,但是擁有共同的子組件 | 將復用組件改為@Builder,讓內部子組件相互之間復用 |
全局型 | 組件可在不同的父組件中復用,并且不適合使用@Builder | 使用BuilderNode自定義復用組件池,在整個應用中自由流轉 |
嵌套型 | 復用組件的子組件的子組件存在差異 | 采用化歸思想將嵌套問題轉化為上面四種標準類型來解決 |
下面將以滑動列表的場景為例介紹5種復用類型的使用場景,為了方便描述,下文將需要復用的自定義組件如ListItem的內容組件,叫做復用組件,將其下層的自定義組件叫做子組件、復用組件上層的自定義組件叫做父組件。為了更直觀,下面每一種復用類型都會通過簡易的圖形展示組件的布局方式,并且為了便于分辨,布局相同的子組件使用同一種形狀圖形表示。
1、標準型
這是一個標準的組件復用場景,一個滾動容器內的復用組件布局相同,只有數據不同,這種類型的組件復用可以直接參考資料組件復用。其緩存池如下,因為該場景只有一個復用組件,所以在緩存中只有一個復用組件list:
典型場景如下,列表Item布局基本完全相同。
標準型組件復用的示例代碼如下:
@Entry
@Component
struct ReuseType1 {
// ...
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: string) => {
ListItem() {
CardView({ item: item })
}
}, (item: string) => item)
}
}
}
}
// 復用組件
@Reusable
@Component
export struct CardView {
@State item: string = '';
aboutToReuse(params: Record<string, Object>): void {
this.item = params.item as string;
}
// ...
}
2、有限變化型
如上圖所示,有限變化型指的是父組件內存在多個類型的復用單元,這些類型的單元布局有所不同,根據業務邏輯的差異可以分為以下兩種情況:
類型1和類型2布局不同,業務邏輯不同:這種情況可以使用兩個不同的自定義組件進行復用。
類型1和類型2布局不同,但是很多業務邏輯公用:這種情況為了復用公用的邏輯代碼,減少代碼冗余,可以給同一個組件設置不同的reuseId來進行復用。
下面將分別介紹這兩種場景下的組件復用方法。
2.1、類型1和類型2布局不同,業務邏輯不同
類型1和類型2布局不同,業務邏輯不同:因為兩種類型的組件布局會對應應用不同的業務處理邏輯,建議將兩種類型的組件分別使用兩個不同的自定義組件,分別進行復用。給復用組件1和復用組件2設置不同的reuseId,此時組件復用池內的狀態如下圖所示,復用組件1和復用組件2處于不同的復用list中。
例如下面的列表場景,列表項布局差距比較大,有多圖片的列表項,有單圖片的列表項:
實現方式可參考以下示例代碼:
@Entry
@Component
struct ReuseType2A {
// ...
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: number) => {
ListItem() {
if (item % 2 === 0) { // 模擬業務條件判斷
SinglePicture({ item: item }) // 渲染單圖片列表項
} else {
MultiPicture({ item: item }) // 渲染多圖片列表項
}
}
}, (item: number) => item + '')
}
}
}
}
// 復用組件1
@Reusable
@Component
struct SinglePicture {
// ...
}
// 復用組件2
@Reusable
@Component
struct MultiPicture {
// ...
}
2.2、類型1和類型2布局不同,但是很多業務邏輯公用
類型1和類型2布局不同,但是很多業務邏輯公用:在這種情況下,如果將組件分為兩個自定義組件進行復用,會存在代碼冗余問題。根據布局的差異,可以給同一個組件設置不同的reuseId從而復用同一個組件,達到邏輯代碼的復用。
根據組件復用原理與使用可知,復用組件是依據reuseId來區分復用緩存池的,而自定義組件的名稱就是默認的reuseId。因此,為復用組件顯式設置兩個不同的reuseId與使用兩個自定義組件進行復用,對于 ArkUI 而言,復用邏輯完全相同,復用池也一樣,只不過復用池中復用組件的list以reuseId作為標識。
例如下面這個場景,布局差異比較小,業務邏輯一樣都是跳轉到頁面詳情。這種情況復用同一個組件,只需要使用if/else條件語句來控制布局的結構,就可以實現,同時可以復用跳轉詳情的公用邏輯代碼。但是這樣會導致在不同邏輯會反復去修改布局,造成性能損耗。開發者可以根據不同的條件,設置不同的reuseId來標識需要復用的組件,省去重復執行if的刪除重創邏輯,提高組件復用的效率和性能。
實現方式可以參考以下示例:
@Entry
@Component
struct ReuseType2B {
// ...
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: MemoInfo) => {
ListItem() {
MemoItem({ memoItem: item })// 使用reuseId進行組件復用的控制
.reuseId((item.imageSrc !== '') ? 'withImage' : 'noImage')
}
}, (item: MemoInfo) => JSON.stringify(item))
}
}
}
}
@Reusable
@Component
export default struct MemoItem {
@State memoItem: MemoInfo = MEMO_DATA[0];
aboutToReuse(params: Record<string, Object>) {
this.memoItem = params.memoItem as MemoInfo;
}
build() {
Row() {
// ...
if (this.memoItem.imageSrc !== '') {
Image($r(this.memoItem.imageSrc))
.width(90)
.aspectRatio(1)
.borderRadius(10)
}
}
// ...
}
}
3、組合型
這種類型中復用組件之間存在不同,并且情況比較多,但擁有共同的子組件。如果使用有限變化型的組件復用方式,將所有類型的復用組件寫成自定義組件分別復用,不同復用組件(組件名不同或者reuseld不同)之間相同子組件無法復用,因為它們在緩存池的不同List中。
對此可以將復用組件轉變為@Builder函數,使復用組件內部共同的子組件的緩存池在父組件上共享,此時組件復用池內的狀態如下圖所示。
典型場景如下圖,這個列表的Item有多種組合方式。但是每個Item上面和下面的布局是一樣的,中間部分的布局有所不同,有單一圖片、視頻、九宮等等。
示例代碼如下,列舉了單一圖片、視頻和九宮格圖片三種類型的列表項目,使用Builder函數后將子組件組合成三種不同的類型,使內部共同的子組件就處于同一個父組件FriendsMomentsPage下。對這些子組件使用組件復用時,他們的緩存池也會在父組件上共享,節省組件創建時的消耗。
@Entry
@Component
struct ReuseType3 {
// ...
@Builder
itemBuilderSingleImage(item: FriendMoment) { // 單大圖列表項
// ...
}
@Builder
itemBuilderGrid(item: FriendMoment) { // 九宮格列表項
// ...
}
@Builder
itemBuilderVideo(item: FriendMoment) { // 視頻列表項
// ...
}
build() {
Column() {
List() {
LazyForEach(this.momentDataSource, (item: FriendMoment) => {
ListItem() {
if (item.type === 1) { // 根據不同類型,使用不同的組合
this.itemBuilderSingleImage(item);
} else if (item.type === 2) {
this.itemBuilderGrid(item);
} else if (item.type === 3) {
this.itemBuilderVideo(item);
} else {
// ...
}
}
}, (moment: FriendMoment) => JSON.stringify(moment))
}
}
}
}
@Reusable
@Component
struct ItemTop {
// ...
}
@Reusable
@Component
struct ItemBottom {
// ...
}
@Reusable
@Component
struct MiddleSingleImage {
// ...
}
@Reusable
@Component
struct MiddleGrid {
// ...
}
@Reusable
@Component
struct MiddleVideo {
// ...
}
4、全局型
默認的組件復用行為,是將子組件放在父組件的緩存池里,受到這個限制,不同父組件中的相同子組件無法復用,推薦的解決方案是將父組件改為builder函數,讓子組件共享組件復用池,但是由于在一些應用場景下,父組件承載了復雜的帶狀態的業務邏輯,而builder是無狀態的,修改會導致難以維護,因此開發者可以使用BuilderNode自行管理組件復用池。
有時候應用在多個tab頁之間切換,tab頁之間結構類似,需要在tab頁之間復用組件,提升頁面切換性能。或者有些應用在組合型場景下,由于復用組件內部含有較多帶狀態的業務邏輯,所以不適合改為Builder函數。
針對這種類型的組件復用場景,可以通過BuilderNode自定義緩存池,將要復用的組件封裝在BuilderNode中,將BuilderNode的NodeController作為復用的最小單元,自行管理復用池。
5、嵌套型
嵌套型是指復用組件的子組件的子組件之間存在差異的復用場景。如上圖所示,列表項復用組件1之間的差異是子組件B的子組件不一樣,有子組件C、D、E三種。這種情況可以運行化歸的思想,將復雜的問題轉化為已知的、簡單的問題
嵌套型實際上是上面四種類型的組合,以上圖為例,可以通過有限變化型的方案,將子組件B變為子組件B1/B2/B3,這樣問題就變成了一個標準的有限變化型,A/B1/C、A/B2/D、A/B3/E會分別作為一個組合進行復用,復用池如下:
下面列舉一個簡單的示例介紹嵌套型的使用:
@Entry
@Component
struct ReuseType5A {
// ...
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: number) => {
ListItem() {
if (item % 2 === 0) { // 模擬類型一的條件
ReusableComponent({ item: item })
.reuseId('type1')
} else if (item % 3 === 0) { // 模擬類型二的條件
ReusableComponent({ item: item })
.reuseId('type2')
} else { // 模擬類型三的條件
ReusableComponent({ item: item })
.reuseId('type3')
}
}
}, (item: number) => item.toString())
}
}
}
}
// 復用組件
@Reusable
@Component
struct ReusableComponent {
@State item: number = 0;
build() {
Column() {
ComponentA()
if (this.item % 2 === 0) {
ComponentB1()
} else if (this.item % 3 === 0) {
ComponentB2()
} else {
ComponentB3()
}
}
}
}
@Component
struct ComponentA {
// ...
}
@Component
struct ComponentB1 {
build() {
Column() {
ComponentC()
}
}
}
@Component
struct ComponentB2 {
build() {
Column() {
ComponentD()
}
}
}
@Component
struct ComponentB3 {
build() {
Column() {
ComponentE()
}
}
}
@Component
struct ComponentC {
// ...
}
@Component
struct ComponentD {
// ...
}
@Component
struct ComponentE {
// ...
}