在ArkTS中,如何最佳化佈局以提高效能?

威哥爱编程發表於2024-12-03

大家好,我是 V 哥。在鴻蒙原生應用開發中,當一個複雜的介面載入資料或發生變化時,佈局可能會發生調整,為了提高佈局變化帶來的效能問題,V 哥在實際開發中,總結了一些最佳化技巧,來提高佈局效能,筆記分享給大家。

1. 避免不必要的二次佈局

  • 在Flex佈局中,如果子元件的主軸尺寸總和不等於容器的主軸尺寸長度,可能需要進行二次佈局。為了避免這種情況,可以確保子元件的主軸尺寸總和等於容器的主軸尺寸長度,或者合理設定flexGrowflexShrink屬性,以減少不必要的佈局重排。

在ArkTS中,二次佈局通常發生在容器的子元素尺寸或位置需要重新計算時。在測試中,V 哥發現以下一些情況可能導致二次佈局。

場景 1:動態改變子元素尺寸

示例程式碼

@Entry
@Component
struct DynamicResizeExample {
    private isLarge = false;

    build() {
        Column() {
            Text("Toggle Size").onClick(() => {
                this.isLarge = !this.isLarge;
            }).padding(10);
            Text("Dynamic Text").fontSize(this.isLarge ? 24 : 16).padding(10);
        }
    }
}

解析
在這個例子中,有一個文字元素的字型大小會根據按鈕的點選事件動態改變。當字型大小改變時,文字元素的尺寸也會隨之改變,這可能導致父容器(Column)需要重新佈局以適應新的尺寸,從而發生二次佈局。

場景 2:非同步載入內容導致的尺寸變化

示例程式碼

@Entry
@Component
struct AsyncContentExample {
    private content: string = '';

    build() {
        Column() {
            Text("Load Content").onClick(() => {
                this.loadContent();
            }).padding(10);
            Text(this.content).padding(10);
        }
    }

    private async loadContent() {
        // 模擬非同步載入資料
        this.content = await fetchContent();
        this.update(); // 手動觸發更新
    }

    private async fetchContent(): Promise<string> {
        // 模擬網路請求
        return new Promise(resolve => {
            setTimeout(() => {
                resolve("Loaded Content");
            }, 1000);
        });
    }
}

解析
在這個例子中,點選按鈕會非同步載入內容,載入完成後更新文字元素的內容。由於非同步載入的內容尺寸可能與原始內容不同,這會導致父容器需要重新佈局以適應新的尺寸,從而可能發生二次佈局。

場景 3:使用Flex佈局時子元素尺寸不確定

示例程式碼

@Entry
@Component
struct FlexLayoutExample {
    private items: string[] = ['Item 1', 'Item 2', 'Item 3'];

    build() {
        Flex() {
            this.items.forEach(item => {
                Text(item).width('auto').margin(5);
            });
        }
    }
}

解析
在這個例子中,使用了Flex佈局,並且每個文字元素的寬度設定為'auto',這意味著它們的寬度將根據內容自動調整。如果文字內容發生變化或者字型大小動態改變,這可能導致Flex容器需要重新計運算元元素的尺寸和位置,從而發生二次佈局。


這些場景在ArkTS中可能導致二次佈局的常見情況,通常與動態尺寸變化、非同步內容載入或不確定的子元素尺寸有關。為了避免二次佈局,可以採取一些最佳化措施。下面我們來看一下如何進行最佳化。

為了避免二次佈局帶來的效能問題,我們可以採取以下最佳化措施:

場景 1:動態改變子元素尺寸

最佳化措施

  • 使用matchContent()替代fontSize()動態變化,以避免觸發二次佈局。
  • 使用動畫平滑過渡尺寸變化,減少佈局的觸發。

最佳化後的程式碼

@Entry
@Component
struct DynamicResizeExample {
    private isLarge = false;

    build() {
        Column() {
            Text("Toggle Size").onClick(() => {
                this.isLarge = !this.isLarge;
            }).padding(10);
            Text("Dynamic Text").matchContent().fontSize(this.isLarge ? 24 : 16).padding(10).transition({ duration: 300 });
        }
    }
}

場景 2:非同步載入內容導致的尺寸變化

最佳化措施

  • 預先設定一個最大尺寸,以減少因內容變化導致的佈局調整。
  • 使用佔位符顯示內容載入中的狀態,避免內容突然變化導致的佈局調整。

最佳化後的程式碼

@Entry
@Component
struct AsyncContentExample {
    private content: string = '...Loading';

    build() {
        Column() {
            Text("Load Content").onClick(() => {
                this.loadContent();
            }).padding(10);
            Text(this.content).maxWidth(300).padding(10); // 預設最大寬度
        }
    }

    private async loadContent() {
        // 模擬非同步載入資料
        this.content = await fetchContent();
        this.update(); // 手動觸發更新
    }

    private async fetchContent(): Promise<string> {
        // 模擬網路請求
        return new Promise(resolve => {
            setTimeout(() => {
                resolve("Loaded Content");
            }, 1000);
        });
    }
}

場景 3:使用Flex佈局時子元素尺寸不確定

最佳化措施

  • 為Flex子項設定最小和最大寬度,減少因內容變化導致的佈局調整。
  • 使用wrapContent()替代width('auto'),以適應內容變化。

最佳化後的程式碼

@Entry
@Component
struct FlexLayoutExample {
    private items: string[] = ['Item 1', 'Item 2', 'Item 3'];

    build() {
        Flex() {
            this.items.forEach(item => {
                Text(item).wrapContent().minWidth(50).maxWidth(200).margin(5); // 設定最小和最大寬度
            });
        }
    }
}

透過這些最佳化措施,我們可以減少因動態變化導致的二次佈局,提高應用的效能和使用者體驗。這些措施包括使用動畫過渡、預設尺寸、設定最小和最大寬度等,以減少佈局的不確定性和頻繁變化。

2. 優先使用layoutWeight屬性

  • 替代flexGrow屬性和flexShrink屬性,layoutWeight屬性可以更有效地控制子元件的佈局權重,從而提高佈局效能。

原始程式碼案例(使用flexGrowflexShrink

在Flex佈局中,如果子元件的主軸尺寸長度總和小於容器主軸尺寸長度,且包含設定有效的flexGrow屬性的子元件,這些子元件會觸發二次佈局,拉伸佈局以填滿容器。同樣,如果子元件的主軸尺寸長度總和大於容器主軸尺寸長度,且包含設定有效的flexShrink屬性的子元件,這些子元件也會觸發二次佈局,壓縮佈局以填滿容器。

@Entry
@Component
struct OriginalFlexLayout {
  build() {
    Flex() {
      Text("Item 1").flexGrow(1)
      Text("Item 2").flexGrow(2)
    }
  }
}

如果容器空間不足以容納兩個文字元素,它們會根據flexGrow屬性進行拉伸,可能導致二次佈局。

最佳化後的程式碼(使用layoutWeight

使用layoutWeight屬性替代flexGrowflexShrink屬性可以更有效地控制子元件的佈局權重,從而提高佈局效能。layoutWeight屬性在分配剩餘空間時,兩次遍歷都只佈局一次元件,不會觸發二次佈局。

@Entry
@Component
struct OptimizedLayoutWithLayoutWeight {
  build() {
    Row() {
      Text("Item 1").layoutWeight(1)
      Text("Item 2").layoutWeight(2)
    }
  }
}

在這個最佳化後的程式碼中,我們使用了Row佈局和layoutWeight屬性來替代Flex佈局中的flexGrow屬性。這樣,當容器空間不足以容納兩個文字元素時,它們會根據layoutWeight屬性的比例分配空間,而不會引起二次佈局。

使用layoutWeight屬性的主要優勢在於它簡化了佈局計算過程。與flexGrowflexShrink相比,layoutWeight不需要在容器空間不足時進行額外的拉伸或壓縮計算,因為它直接根據權重分配空間。這種方法減少了佈局的複雜度,特別是在子元件數量較多或者佈局較為複雜的情況下,能夠顯著提高佈局效能。透過這種方式,我們可以避免不必要的二次佈局,從而提升應用的響應速度和使用者體驗。

3. 響應式佈局設計

  • 透過條件渲染和自定義Builder函式,可以建立適應不同裝置的介面,減少因裝置差異導致的佈局重排。

響應式佈局設計不當可能會導致佈局重排,特別是在不同螢幕尺寸或方向變化時。以下是一個沒有使用響應式設計,導致在不同裝置上可能出現佈局問題的原始程式碼案例:

原始程式碼案例

@Entry
@Component
struct ResponsiveLayoutIssue {
  build() {
    Column() {
      Text("Header").fontSize(24) // 大標題
      Row() {
        Text("Menu Item 1") // 選單項
        Text("Menu Item 2") // 選單項
        Text("Menu Item 3") // 選單項
      }.width('100%') // 強制選單寬度為100%
      Text("Content") // 主內容區域
    }
  }
}

在這個例子中,Row中的選單項在小螢幕上可能會顯示擁擠,因為它們被設定為width('100%'),這可能導致佈局重排和內容溢位。

最佳化後的程式碼

為了最佳化上述程式碼,我們可以採用條件渲染和自定義Builder函式來建立適應不同裝置的介面:

@Entry
@Component
struct ResponsiveLayoutOptimized {
  build() {
    Column() {
      Text("Header").fontSize(24) // 大標題
      if (this.isSmallScreen()) {
        // 小螢幕上,選單項垂直排列
        ForEach(['Menu Item 1', 'Menu Item 2', 'Menu Item 3'], (item) => {
          Text(item).width('100%') // 每個選單項寬度為100%
        })
      } else {
        // 大螢幕上,選單項水平排列
        Row() {
          Text("Menu Item 1") // 選單項
          Text("Menu Item 2") // 選單項
          Text("Menu Item 3") // 選單項
        }.width('100%') // 選單整體寬度為100%
      }
      Text("Content") // 主內容區域
    }
  }
  
  isSmallScreen() {
    // 假設這個函式根據螢幕尺寸返回true或false
    return window.width < 600; // 假設寬度小於600px為小螢幕
  }
}

效能最佳化解釋

使用響應式佈局設計的主要優勢在於它可以根據裝置的螢幕尺寸和方向動態調整佈局,減少因裝置差異導致的佈局重排:

  1. 減少佈局重排:透過條件渲染,我們可以為不同螢幕尺寸提供不同的佈局結構,從而減少因螢幕尺寸變化導致的佈局重排。
  2. 提高使用者體驗:適應不同裝置的介面可以提供更好的使用者體驗,尤其是在移動裝置和桌面裝置之間切換時。
  3. 最佳化效能:減少佈局計算和重排可以提高應用的效能,尤其是在動態內容更新和裝置方向變化時。

所以,透過響應式佈局設計,我們可以建立適應不同裝置的介面,減少因裝置差異導致的佈局重排,從而提高應用的效能和使用者體驗。

4. 懶載入

  • 對於複雜或資源密集型的元件,使用懶載入可以提高應用的啟動速度,僅在需要時才載入和渲染這些元件。

懶載入帶來的問題

在應用開發中,如果所有元件都在應用啟動時一次性載入,可能會導致啟動時間過長,特別是對於包含複雜或資源密集型元件的應用。這會影響使用者體驗,因為使用者需要等待所有資源載入完成後才能使用應用。

原始程式碼案例

以下是一個沒有使用懶載入的原始程式碼案例,其中包含一個大型列表,列表中的每個項都是一個複雜的元件:

@Entry
@Component
struct NonLazyLoadingExample {
  private items: any[] = this.generateItems(100); // 假設生成了100個複雜項

  generateItems(count: number): any[] {
    let items: any[] = [];
    for (let i = 0; i < count; i++) {
      items.push({
        id: i,
        // 假設每個項包含大量資料和資源
        data: this.generateLargeData()
      });
    }
    return items;
  }

  generateLargeData(): any {
    // 生成大量資料的模擬函式
    return new Array(1000).fill(null).map((_, index) => ({ index }));
  }

  build() {
    Column() {
      ForEach(this.items, (item) => {
        // 渲染複雜元件,假設每個元件都需要載入大量資源
        this.renderComplexItem(item);
      });
    }
  }

  renderComplexItem(item: any) {
    // 複雜元件的渲染邏輯
    Column() {
      Text(`Item ID: ${item.id}`).fontWeight(FontWeight.Bold);
      // 假設這裡有更多複雜的UI和資源載入
    }
  }
}

在這個例子中,所有複雜元件都在應用啟動時一次性渲染,這可能導致應用啟動緩慢。

最佳化後的程式碼

使用懶載入,我們可以只在元件需要顯示時才載入和渲染它們。以下是使用懶載入最佳化後的程式碼:

@Entry
@Component
struct LazyLoadingOptimizedExample {
  private items: any[] = this.generateItems(100); // 假設生成了100個複雜項

  generateItems(count: number): any[] {
    let items: any[] = [];
    for (let i = 0; i < count; i++) {
      items.push({
        id: i,
        // 假設每個項包含大量資料和資源
        data: this.generateLargeData()
      });
    }
    return items;
  }

  generateLargeData(): any {
    // 生成大量資料的模擬函式
    return new Array(1000).fill(null).map((_, index) => ({ index }));
  }

  build() {
    Column() {
      // 使用懶載入ForEach
      LazyForEach(this.items, (item) => {
        // 僅在需要時才渲染複雜元件
        this.renderComplexItem(item);
      });
    }
  }

  renderComplexItem(item: any) {
    // 複雜元件的渲染邏輯
    Column() {
      Text(`Item ID: ${item.id}`).fontWeight(FontWeight.Bold);
      // 假設這裡有更多複雜的UI和資源載入
    }
  }
}

效能最佳化解釋

使用懶載入的主要優勢在於:

  1. 提高啟動速度:應用不需要在啟動時載入所有資源,從而減少了啟動時間。
  2. 減少記憶體消耗:懶載入可以減少同時載入的資源數量,從而減少記憶體消耗。
  3. 按需載入:只有當使用者滾動到某個部分時,相關的元件才會被載入和渲染,這提高了資源的使用效率。
  4. 改善使用者體驗:使用者可以更快地看到應用的初始內容,而不需要等待所有資源載入完成。

所以要切記,透過使用懶載入,我們可以最佳化應用的啟動速度和效能,提高使用者體驗。

5. 最佳化大型物件的更新

  • 對於包含多個屬性的複雜物件,使用@Observed@ObjectLink可以實現細粒度的更新,只有發生變化的部分會觸發UI更新,而不是整個物件。

在ArkTS中,如果一個大型物件的屬性發生變化時,沒有使用細粒度更新,那麼整個物件可能會被重新渲染,這會導致效能問題,尤其是在物件屬性很多或者物件很大時。

原始程式碼案例

以下是一個沒有使用@Observed@ObjectLink的原始程式碼案例,其中包含一個大型物件,每次物件更新時,整個物件都會被重新渲染:

@Entry
@Component
struct LargeObjectUpdateIssue {
  private largeObject: any = this.createLargeObject();

  createLargeObject(): any {
    // 建立一個包含多個屬性的大型物件
    let obj: any = {};
    for (let i = 0; i < 100; i++) {
      obj[`property${i}`] = `value${i}`;
    }
    return obj;
  }

  build() {
    Column() {
      Text(`Property1: ${this.largeObject.property1}`)
      Text(`Property2: ${this.largeObject.property2}`)
      // ...更多屬性
    }
  }

  updateObject() {
    // 更新物件的某個屬性
    this.largeObject.property1 = 'new value';
    // 這將導致整個物件重新渲染
  }
}

在這個例子中,updateObject方法更新了largeObject的一個屬性,但由於沒有使用細粒度更新,整個物件都會被重新渲染。

最佳化後的程式碼

使用@Observed@ObjectLink,我們可以只更新發生變化的部分,而不是整個物件:

@Entry
@Component
struct LargeObjectUpdateOptimized {
  @Observed largeObject: any = this.createLargeObject();

  createLargeObject(): any {
    // 建立一個包含多個屬性的大型物件
    let obj: any = {};
    for (let i = 0; i < 100; i++) {
      obj[`property${i}`] = `value${i}`;
    }
    return obj;
  }

  build() {
    Column() {
      Text(`Property1: ${this.largeObject.property1}`)
      Text(`Property2: ${this.largeObject.property2}`)
      // ...更多屬性
    }
  }

  updateObject() {
    // 更新物件的某個屬性
    this.largeObject.property1 = 'new value';
    // 只有property1相關的UI會更新
  }
}

效能最佳化解釋

使用@Observed@ObjectLink的主要優勢在於:

  1. 減少不必要的渲染:只有發生變化的部分會觸發UI更新,而不是整個物件,這減少了不必要的渲染。
  2. 提高效能:減少了渲染次數,提高了應用的效能,尤其是在物件屬性很多或者物件很大時。
  3. 最佳化使用者體驗:使用者介面的響應更快,因為只有相關的部分被更新,而不是整個物件。
  4. 降低記憶體消耗:減少了因為渲染整個物件而可能產生的額外記憶體消耗。

所以,透過使用@Observed@ObjectLink,我們可以最佳化大型物件的更新,提高應用的效能和使用者體驗。

6. 記憶體管理和避免記憶體洩漏

  • 使用物件池模式管理頻繁建立和銷燬的物件,如粒子系統、遊戲中的敵人等,可以減少記憶體壓力並提高效能。

記憶體管理和避免記憶體洩漏的問題

在應用開發中,頻繁建立和銷燬物件(如粒子系統、遊戲中的敵人等)可能會導致記憶體壓力增大,並增加垃圾回收(GC)的頻率,從而影響應用效能。此外,如果物件沒有被正確銷燬,還可能導致記憶體洩漏。

原始程式碼案例

以下是一個沒有使用物件池模式的原始程式碼案例,其中包含頻繁建立和銷燬物件的操作:

@Entry
@Component
struct MemoryLeakExample {
  private enemies: any[] = [];

  createEnemy() {
    // 模擬建立一個敵人物件
    let enemy = {
      id: Date.now(),
      name: `Enemy ${this.enemies.length + 1}`,
      // 假設這裡有更多複雜的屬性和方法
    };
    this.enemies.push(enemy);
  }

  build() {
    Button("Create Enemy").onClick(() => {
      this.createEnemy();
    });
  }
}

在這個例子中,每次點選按鈕都會建立一個新的敵人物件,並將其新增到陣列中。如果這些物件沒有被正確管理,可能會導致記憶體洩漏。

最佳化後的程式碼

使用物件池模式,我們可以重用物件,減少記憶體壓力並提高效能:

@Entry
@Component
struct MemoryManagementOptimized {
  private enemyPool: any[] = []; // 物件池

  createEnemy() {
    if (this.enemyPool.length > 0) {
      // 從物件池中獲取一個物件
      let enemy = this.enemyPool.pop();
      enemy復活();
      this.enemies.push(enemy);
    } else {
      // 建立一個新的敵人物件
      let enemy = {
        id: Date.now(),
        name: `Enemy ${this.enemies.length + 1}`,
        // 假設這裡有更多複雜的屬性和方法
       復活: () => {
          // 復活邏輯,重置物件狀態
        }
      };
      this.enemies.push(enemy);
    }
  }

  destroyEnemy(enemy: any) {
    // 將敵人物件狀態重置後放回物件池
    enemy.復活();
    this.enemyPool.push(enemy);
  }

  build() {
    Button("Create Enemy").onClick(() => {
      this.createEnemy();
    });
    Button("Destroy Enemy").onClick(() => {
      if (this.enemies.length > 0) {
        let enemy = this.enemies.pop();
        this.destroyEnemy(enemy);
      }
    });
  }
}

效能最佳化解釋

使用物件池模式的主要優勢在於:

  1. 減少記憶體壓力:透過重用物件,減少了頻繁建立和銷燬物件導致的記憶體壓力。
  2. 提高效能:減少了垃圾回收(GC)的頻率,從而提高了應用的效能。
  3. 避免記憶體洩漏:透過正確管理物件的生命週期,避免了記憶體洩漏的風險。
  4. 提高資源利用率:物件池可以提高物件的利用率,減少資源浪費。

咱們透過使用物件池模式,可以最佳化記憶體管理,減少記憶體壓力,提高應用的效能,並避免記憶體洩漏。

最後

以上 V 哥總結的小小最佳化心得,可以有效地最佳化ArkTS中的佈局效能,提升應用的響應速度和使用者體驗。歡迎關注威哥愛程式設計,鴻蒙開發你我他。

相關文章